Instalação
brew install sbt
ou instalações semelhantes sbt que, tecnicamente falando, consistem em
Quando você executa sbt
do 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 .sbtopts
arquivo na raiz do projeto. Para configurar a modificação de todo o sistema sbt /usr/local/etc/sbtopts
. A execução sbt -help
deve informar a localização exata. Por exemplo, para dar ao sbt mais memória como execução única sbt -mem 4096
, ou salvar -mem 4096
em .sbtopts
ousbtopts
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-http
a 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, fooTask
na 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-classes
deve 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-http
deve ser uma dependência da definição do projeto $9c2192aea3f1db3c251d
e não a dependência do projeto adequado. Portanto, ele precisa ser declarado em em project/build.sbt
vez 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.sbt
DSL. Por exemplo, o seguinte executafooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
under 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.sbt
O 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, hello
depois executar a
e, em seguida, realizar a b
tarefa. No entanto, isso realmente significa executar a
e b
em paralelo , e antes println("hello")
disso
a
b
hello
ou porque a ordem de a
e b
nã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.sequential
ou Def.taskDyn
para 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 sum
depende e tem que esperar a
e b
.
Em outras palavras
- para semântica de aplicativos , use
.value
- para semântica monádica use
sequential
outaskDyn
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 x
que 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
.value
sintaxe e sbt cuidará de estabelecer a relação entreSetting[T]
- Ocasionalmente, temos que ajustar manualmente uma parte do DAG e para isso usamos
Def.sequential
ouDef.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.extract
erunTask
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.sbt
arquivos, 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
val
e 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.sbt
para 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 .scala
arquivos
. // 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 enablePlugin
em 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