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