Alguém pode explicar a maneira correta de usar o SBT?

100

Estou saindo do armário por causa disso! Não entendo SBT. Pronto, eu disse, agora me ajude por favor.

Todos os caminhos levam a Roma, e que é o mesmo para SBT: Para começar com o SBTque há SBT, SBT Launcher, SBT-extras, etc, e, em seguida, há diferentes maneiras de incluir e decidir sobre repositórios. Existe uma 'melhor' maneira?

Estou perguntando porque às vezes fico um pouco perdida. A documentação do SBT é muito minuciosa e completa, mas não sei quando usar build.sbtou project/build.propertiesou project/Build.scalaou project/plugins.sbt.

Aí fica divertido, tem o Scala-IDEe SBT- Qual é a maneira correta de usá-los juntos? O que vem primeiro, a galinha ou o ovo?

O mais importante é provavelmente, como você encontra os repositórios e versões corretas para incluir em seu projeto?Eu simplesmente puxo uma machete e começo a abrir caminho para frente? Muitas vezes encontro projetos que incluem tudo e a pia da cozinha, e então eu percebo - eu não sou o único que fica um pouco perdido.

Como um exemplo simples, agora estou começando um novo projeto. Desejo usar os recursos mais recentes do SLICKe Scalaprovavelmente exigirá a versão mais recente do SBT. Qual é o ponto lógico para começar e por quê?Em que arquivo devo defini-lo e como deve aparecer? Eu sei que posso fazer isso funcionar, mas eu realmente gostaria de uma opinião de um especialista sobre onde tudo deve ir (por que deveria ir, será um bônus).

Eu tenho usado SBTpara pequenos projetos por mais de um ano. Usei SBTe então SBT Extras(pois fazia algumas dores de cabeça desaparecerem magicamente), mas não tenho certeza por que deveria usar um ou outro. Estou apenas ficando um pouco frustrado por não entender como as coisas se encaixam ( SBTe os repositórios), e acho que isso salvará o próximo cara que vier para cá de muitas dificuldades se isso puder ser explicado em termos humanos.

Jack
fonte
2
O que exatamente você quer dizer com "existe o Scala-IDE e o SBT"? Você define seu projeto com sbt e sbt pode gerar um projeto ide (eclipse oder intellij). Então o SBT vem primeiro ...
janeiro
2
@Jan Mencionei isso porque o Scala-IDE usa o SBT como gerenciador de compilação. Veja assembla.com/spaces/scala-ide/wiki/SBT-based_build_manager e mais abaixo na postagem que mencionam "Não há necessidade de definir seu arquivo de projeto SBT." que eu achei confuso.
Jack
Está bem. Como geralmente uso o intellij (ou sublime) para editar o scala, não sabia disso. Eu acho que o construtor gera suas próprias configurações de sbt?
janeiro de
2
@JacobusR O fato do Scala IDE usar o SBT para construir os códigos-fonte do seu projeto é um detalhe de implementação e os usuários não precisam se preocupar com isso. Existem realmente 0 implicações. Fora do Eclipse, os usuários podem construir um projeto com SBT, Maven, Ant, ..., e isso não fará nenhuma diferença para o IDE Scala. Mais uma coisa, mesmo se você tiver um projeto SBT, o Scala IDE não se importa, ou seja, ele não procura seu Build.scalapara configurar o classpath, e é por isso que você realmente precisa do sbteclipse para gerar o Eclipse .classpath. Espero que isto ajude.
Mirco Dotta
1
O IDE do @Jan Scala aumentou a confusão e, sim, a documentação que dá uma visão mais ampla sobre a configuração de um bom ambiente de desenvolvimento Scala e algumas orientações sólidas de fluxos de trabalho de programação adequados seria muito útil.
Jack

Respostas:

29

O mais importante é provavelmente, como você encontra os repositórios e versões corretas para incluir em seu projeto? Eu simplesmente puxo uma machete e começo a abrir caminho para frente? Muitas vezes encontro projetos que incluem tudo e a pia da cozinha

Para dependências baseadas em Scala, eu seguiria o que os autores recomendam. Por exemplo: http://code.google.com/p/scalaz/#SBT indica para usar:

libraryDependencies += "org.scalaz" %% "scalaz-core" % "6.0.4"

Ou https://github.com/typesafehub/sbteclipse/ tem instruções sobre onde adicionar:

addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0-RC1")

Para dependências baseadas em Java, eu uso http://mvnrepository.com/ para ver o que está disponível e, a seguir, clico na guia SBT. Por exemplo http://mvnrepository.com/artifact/net.sf.opencsv/opencsv/2.3 indica para usar:

libraryDependencies += "net.sf.opencsv" % "opencsv" % "2.3"

Em seguida, retire a machete e comece a abrir caminho para a frente. Se você tiver sorte, não acabará usando potes que dependem de alguns dos mesmos potes, mas com versões incompatíveis. Dado o ecossistema Java, muitas vezes você acaba incluindo tudo e a pia da cozinha e é preciso algum esforço para eliminar as dependências ou garantir que você não perca as dependências necessárias.

Como um exemplo simples, agora estou começando um novo projeto. Quero usar os recursos mais recentes do SLICK e do Scala e isso provavelmente exigirá a versão mais recente do SBT. Qual é o ponto lógico para começar e por quê?

Acho que o ponto mais lógico é construir imunidade ao sbt gradualmente .

Certifique-se de entender:

  1. formato de escopos {<build-uri>}<project-id>/config:key(for task-key)
  2. os 3 sabores de configurações ( SettingKey, TaskKey, InputKey) - leia a seção chamada "Tarefa Keys" em http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def

Mantenha essas 4 páginas abertas o tempo todo para que você possa pular e procurar várias definições e exemplos:

  1. http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def
  2. http://www.scala-sbt.org/release/docs/Detailed-Topics/index
  3. http://harrah.github.com/xsbt/latest/sxr/Keys.scala.html
  4. http://harrah.github.com/xsbt/latest/sxr/Defaults.scala.html

Fazer uso máximo de showe inspect e a conclusão guia para se familiarizar com os valores reais de configurações, suas dependências, definições e configurações relacionadas. Não acredito que os relacionamentos que você descobrirá usando inspectestejam documentados em qualquer lugar. Se existe uma maneira melhor, eu quero saber sobre isso.

Huynhjl
fonte
25

A forma como eu uso sbt é:

  1. Use sbt-extras - basta obter o script de shell e adicioná-lo à raiz do seu projeto
  2. Crie uma projectpasta com um MyProject.scalaarquivo para configurar o sbt. Eu prefiro muito mais isso do build.sbtque a abordagem - é uma escala e é mais flexível
  3. Crie um project/plugins.sbt arquivo e adicione o plugin apropriado para o seu IDE. Tanto sbt-eclipse, sbt-idea ou ensime-sbt-cmd para que você possa gerar arquivos de projeto para eclipse, intellij ou ensime.
  4. Inicie o sbt na raiz do seu projeto e gere os arquivos do projeto para o seu IDE
  5. Lucro

Não me incomodo em verificar os arquivos do projeto IDE, pois eles são gerados pelo sbt, mas pode haver motivos para você querer fazer isso.

Você pode ver um exemplo configurado como este aqui .

Channing Walton
fonte
Obrigado pela boa resposta. Aceitei a outra resposta, porque cobre mais terreno, e votei positivamente na sua causa, é muito boa também. Eu teria aceitado ambos se pudesse.
Jack
0

Use o Typesafe Activator, uma maneira sofisticada de chamar sbt, que vem com modelos e sementes de projeto: https://typesafe.com/activator

Activator new

Fetching the latest list of templates...

Browse the list of templates: http://typesafe.com/activator/templates
Choose from these featured templates or enter a template name:
 1) minimal-java
 2) minimal-scala
 3) play-java
 4) play-scala
(hit tab to see a list of all templates)
Andreas Neumann
fonte
5
Tenho parcialidade para a ideia de que, em caso de dúvida, adicionar mais magia à mistura provavelmente não resolverá seus problemas.
Cubic
0

Instalação

brew install sbt ou instalações semelhantes sbt que, tecnicamente falando, consistem em

Quando você executa sbtdo terminal, ele realmente executa o script bash do iniciador sbt. Pessoalmente, nunca tive que me preocupar com essa trindade, e apenas usar sbt como se fosse uma única coisa.

Configuração

Para configurar o sbt para um determinado projeto, salve o .sbtoptsarquivo na raiz do projeto. Para configurar a modificação de todo o sistema sbt /usr/local/etc/sbtopts. A execução sbt -helpdeve informar a localização exata. Por exemplo, para dar ao sbt mais memória como execução única sbt -mem 4096, ou salvar -mem 4096em .sbtoptsousbtopts para que o aumento da memória tenha efeito permanente.

 Estrutura do projeto

sbt new scala/scala-seed.g8 cria uma estrutura de projeto sbt Hello World mínima

.
├── README.md  // most important part of any software project
├── build.sbt  // build definition of the project
├── project    // build definition of the build (sbt is recursive - explained below)
├── src        // test and main source code
└── target     // compiled classes, deployment package

Comandos frequentes

test                                                // run all test
testOnly                                            // run only failed tests
testOnly -- -z "The Hello object should say hello"  // run one specific test
run                                                 // run default main
runMain example.Hello                               // run specific main
clean                                               // delete target/
package                                             // package skinny jar
assembly                                            // package fat jar
publishLocal                                        // library to local cache
release                                             // library to remote repository
reload                                              // after each change to build definition

Miríade de conchas

scala              // Scala REPL that executes Scala language (nothing to do with sbt)
sbt                // sbt REPL that executes special sbt shell language (not Scala REPL)
sbt console        // Scala REPL with dependencies loaded as per build.sbt
sbt consoleProject // Scala REPL with project definition and sbt loaded for exploration with plain Scala langauage

A definição de construção é um projeto Scala adequado

Este é um dos principais conceitos idiomáticos de sbt. Vou tentar explicar com uma pergunta. Digamos que você queira definir uma tarefa sbt que executará uma solicitação HTTP com scalaj-http. Intuitivamente, podemos tentar o seguinte dentrobuild.sbt

libraryDependencies +=  "org.scalaj" %% "scalaj-http" % "2.4.2"

val fooTask = taskKey[Unit]("Fetch meaning of life")
fooTask := {
  import scalaj.http._ // error: cannot resolve symbol
  val response = Http("http://example.com").asString
  ...
}

No entanto, ocorrerá um erro dizendo ausente import scalaj.http._. Como isso é possível quando, logo acima, adicionado scalaj-httpa libraryDependencies? Além disso, por que funciona quando, em vez disso, adicionamos a dependência a project/build.sbt?

// project/build.sbt
libraryDependencies +=  "org.scalaj" %% "scalaj-http" % "2.4.2"

A resposta é que, fooTaskna verdade, é parte de um projeto Scala separado do seu projeto principal. Este projeto Scala diferente pode ser encontrado em um project/diretório que possui seu próprio target/diretório onde residem suas classes compiladas. Na verdade, abaixo project/target/config-classesdeve haver uma classe que descompila para algo como

object $9c2192aea3f1db3c251d extends scala.AnyRef {
  lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
  lazy val root : sbt.Project = { /* compiled code */ }
}

Vemos que fooTaské simplesmente um membro de um objeto Scala normal chamado $9c2192aea3f1db3c251d. Claramente scalaj-httpdeve ser uma dependência da definição do projeto $9c2192aea3f1db3c251de não a dependência do projeto adequado. Portanto, ele precisa ser declarado em em project/build.sbtvez de build.sbt, porque projecté onde reside o projeto Scala de definição de construção.

Para conduzir ao ponto de que a definição de construção é apenas outro projeto Scala, execute sbt consoleProject. Isso carregará o Scala REPL com o projeto de definição de construção no classpath. Você deve ver uma importação ao longo das linhas de

import $9c2192aea3f1db3c251d

Portanto, agora podemos interagir diretamente com o projeto de definição de construção, chamando-o com Scala adequado em vez de build.sbtDSL. Por exemplo, o seguinte executafooTask

$9c2192aea3f1db3c251d.fooTask.eval

build.sbtunder root project é uma DSL especial que ajuda a definir a definição de build do projeto Scala em project/.

E o projeto Scala de definição de construção, pode ter seu próprio projeto Scala de definição de construção em project/project/e assim por diante. Dizemos que sbt é recursivo .

sbt é paralelo por padrão

sbt cria DAG fora de tarefas. Isso permite analisar dependências entre tarefas e executá-las em paralelo e até mesmo realizar a desduplicação. build.sbtO DSL foi projetado com isso em mente, o que pode levar a uma semântica inicialmente surpreendente. Qual você acha que é a ordem de execução no trecho a seguir?

def a = Def.task { println("a") }
def b = Def.task { println("b") }
lazy val c = taskKey[Unit]("sbt is parallel by-default")
c := {
  println("hello")
  a.value
  b.value
}

Intuitivamente, pode-se pensar que o fluxo aqui é primeiro imprimir, hellodepois executar ae, em seguida, realizar a btarefa. No entanto, isso realmente significa executar ae bem paralelo , e antes println("hello") disso

a
b
hello

ou porque a ordem de ae bnão é garantida

b
a
hello

Talvez paradoxalmente, em sbt seja mais fácil fazer paralelo do que serial. Se você precisar de pedido em série, terá que usar coisas especiais como Def.sequentialou Def.taskDynpara emular para compreensão .

def a = Def.task { println("a") }
def b = Def.task { println("b") }
lazy val c = taskKey[Unit]("")
c := Def.sequential(
  Def.task(println("hello")),
  a,
  b
).value

é similar a

for {
  h <- Future(println("hello"))
  a <- Future(println("a"))
  b <- Future(println("b"))
} yield ()

onde vemos que não há dependências entre os componentes, embora

def a = Def.task { println("a"); 1 }
def b(v: Int) = Def.task { println("b"); v + 40 }
def sum(x: Int, y: Int) = Def.task[Int] { println("sum"); x + y }
lazy val c = taskKey[Int]("")
c := (Def.taskDyn {
  val x = a.value
  val y = Def.task(b(x).value)
  Def.taskDyn(sum(x, y.value))
}).value

é similar a

def a = Future { println("a"); 1 }
def b(v: Int) = Future { println("b"); v + 40 }
def sum(x: Int, y: Int) = Future { x + y }

for {
  x <- a
  y <- b(x)
  c <- sum(x, y)
} yield { c }

onde vemos sumdepende e tem que esperar ae b.

Em outras palavras

  • para semântica de aplicativos , use.value
  • para semântica monádica use sequentialoutaskDyn

Considere outro snippet semanticamente confuso como resultado da natureza de construção de dependência de value, onde em vez de

`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
                ^

temos que escrever

val x = settingKey[String]("")
x := version.value

Observe que a sintaxe .valueé sobre relacionamentos no DAG e não significa

"me dê o valor agora"

em vez disso, significa algo como

"meu chamador depende de mim primeiro, e quando eu souber como todo o DAG se encaixa, poderei fornecer ao meu chamador o valor solicitado"

Portanto, agora pode ficar um pouco mais claro por xque não pode ser atribuído um valor ainda; ainda não há valor disponível na fase de construção de relacionamento.

Podemos ver claramente uma diferença na semântica entre Scala e a linguagem DSL em build.sbt. Aqui estão algumas regras de polegar que funcionam para mim

  • DAG é feito de expressões do tipo Setting[T]
  • Na maioria dos casos, simplesmente usamos a .valuesintaxe e sbt cuidará de estabelecer a relação entreSetting[T]
  • Ocasionalmente, temos que ajustar manualmente uma parte do DAG e para isso usamos Def.sequentialouDef.taskDyn
  • Uma vez que essas estranhezas sintáticas de ordenação / relacionamento tenham sido atendidas, podemos contar com a semântica usual do Scala para construir o resto da lógica de negócios das tarefas.

 Comandos vs Tarefas

Os comandos são uma forma preguiçosa de sair do DAG. Usando comandos, é fácil alterar o estado de compilação e serializar tarefas como desejar. O custo é que perdemos a paralelização e a desduplicação das tarefas fornecidas pelo DAG, de forma que as tarefas devem ser a escolha preferida. Você pode pensar nos comandos como uma espécie de gravação permanente de uma sessão que pode ser feita internamente sbt shell. Por exemplo, dado

vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value

considere o resultado da sessão seguinte

sbt:root> x
[info] 13
sbt:root> show f
[info] 14
sbt:root> set x := 41
[info] Defining x
[info] The new value will be used by f
[info] Reapplying settings...
sbt:root> show f
[info] 42

Em particular, não como alteramos o estado de construção com set x := 41. Os comandos nos permitem fazer uma gravação permanente da sessão acima, por exemplo

commands += Command.command("cmd") { state =>
  "x" :: "show f" :: "set x := 41" :: "show f" :: state
}

Também podemos tornar o tipo de comando seguro usando Project.extracterunTask

commands += Command.command("cmd") { state =>
  val log = state.log
  import Project._
  log.info(x.value.toString)
  val (_, resultBefore) = extract(state).runTask(f, state)
  log.info(resultBefore.toString)
  val mutatedState = extract(state).appendWithSession(Seq(x := 41), state)
  val (_, resultAfter) = extract(mutatedState).runTask(f, mutatedState)
  log.info(resultAfter.toString)
  mutatedState
}

Scopes

Os escopos entram em ação quando tentamos responder aos seguintes tipos de perguntas

  • Como definir a tarefa uma vez e disponibilizá-la para todos os subprojetos na construção de vários projetos?
  • Como evitar dependências de teste no caminho de classe principal?

sbt tem um espaço de escopo multi-eixo que pode ser navegado usando sintaxe de barra , por exemplo,

show  root   /  Compile         /  compile   /   scalacOptions
        |        |                  |             |
     project    configuration      task          key

Pessoalmente, raramente me vejo tendo que me preocupar com o escopo. Às vezes eu quero compilar apenas fontes de teste

Test/compile

ou talvez execute uma tarefa específica de um subprojeto específico sem primeiro ter que navegar para esse projeto com project subprojB

subprojB/Test/compile

Acho que as seguintes regras ajudam a evitar complicações de escopo

  • não tem vários build.sbtarquivos, mas apenas um único mestre no projeto raiz que controla todos os outros subprojetos
  • compartilhar tarefas por meio de plug-ins automáticos
  • fatorar configurações comuns em Scala simples vale adicioná-las explicitamente a cada subprojeto

Compilação de vários projetos

Em vez de vários arquivos build.sbt para cada subprojeto

.
├── README.md
├── build.sbt                  // OK
├── multi1
│   ├── build.sbt              // NOK
│   ├── src
│   └── target
├── multi2
│   ├── build.sbt              // NOK
│   ├── src
│   └── target
├── project                    // this is the meta-project
│   ├── FooPlugin.scala        // custom auto plugin
│   ├── build.properties       // version of sbt and hence Scala for meta-project
│   ├── build.sbt              // OK - this is actually for meta-project 
│   ├── plugins.sbt            // OK
│   ├── project
│   └── target
└── target

Tenha um único mestre build.sbtpara governar todos eles

.
├── README.md
├── build.sbt                  // single build.sbt to rule theme all
├── common
│   ├── src
│   └── target
├── multi1
│   ├── src
│   └── target
├── multi2
│   ├── src
│   └── target
├── project
│   ├── FooPlugin.scala
│   ├── build.properties
│   ├── build.sbt
│   ├── plugins.sbt
│   ├── project
│   └── target
└── target

Há uma prática comum de fatorar configurações comuns em compilações de vários projetos

defina uma sequência de configurações comuns em um val e adicione-as a cada projeto. Menos conceitos para aprender dessa forma.

por exemplo

lazy val commonSettings = Seq(
  scalacOptions := Seq(
    "-Xfatal-warnings",
    ...
  ),
  publishArtifact := true,
  ...
)

lazy val root = project
  .in(file("."))
  .settings(settings)
  .aggregate(
    multi1,
    multi2
  )
lazy val multi1 = (project in file("multi1")).settings(commonSettings)
lazy val multi2 = (project in file("multi2")).settings(commonSettings)

Navegação de projetos

projects         // list all projects
project multi1   // change to particular project

Plugins

Lembre-se de que a definição de construção é um projeto Scala adequado que reside em project/. É aqui que definimos um plugin criando .scalaarquivos

.                          // directory of the (main) proper project
├── project
│   ├── FooPlugin.scala    // auto plugin
│   ├── build.properties   // version of sbt library and indirectly Scala used for the plugin
│   ├── build.sbt          // build definition of the plugin
│   ├── plugins.sbt        // these are plugins for the main (proper) project, not the meta project
│   ├── project            // the turtle supporting this turtle
│   └── target             // compiled binaries of the plugin

Aqui está um plugin automático mínimo emproject/FooPlugin.scala

object FooPlugin extends AutoPlugin {
  object autoImport {
      val barTask = taskKey[Unit]("")
  }

  import autoImport._

  override def requires = plugins.JvmPlugin  // avoids having to call enablePlugin explicitly
  override def trigger = allRequirements

  override lazy val projectSettings = Seq(
    scalacOptions ++= Seq("-Xfatal-warnings"),
    barTask := { println("hello task") },
    commands += Command.command("cmd") { state =>
      """eval println("hello command")""" :: state
    }   
  )
}

A substituição

override def requires = plugins.JvmPlugin

deve habilitar efetivamente o plugin para todos os subprojetos sem ter que chamar explicitamente enablePluginem build.sbt.

IntelliJ e sbt

Habilite a seguinte configuração (que deve ser habilitada por padrão )

use sbt shell

debaixo

Preferences | Build, Execution, Deployment | sbt | sbt projects

Referências chave

Mario Galic
fonte