Instalación
brew install sbt
o instalaciones similares sbt que técnicamente hablando consisten en
Cuando se ejecuta sbt
desde la terminal, en realidad ejecuta el script bash del lanzador sbt. Personalmente, nunca tuve que preocuparme por esta trinidad, y usar sbt como si fuera una sola cosa.
Configuración
Para configurar sbt para un proyecto en particular, guarde el .sbtopts
archivo en la raíz del proyecto. Para configurar sbt en todo el sistema, modificar /usr/local/etc/sbtopts
. La ejecución sbt -help
debería indicarle la ubicación exacta. Por ejemplo, para dar más memoria a sbt como ejecución única sbt -mem 4096
, o guardar -mem 4096
en .sbtopts
o sbtopts
para que el aumento de memoria tenga efecto de forma permanente.
Estructura del proyecto
sbt new scala/scala-seed.g8
crea una estructura mínima de proyecto de Hello World sbt
.
├── 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 frecuentes
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
Gran cantidad 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
La definición de compilación es un proyecto Scala adecuado
Este es uno de los conceptos idiomáticos clave de sbt. Intentaré explicarlo con una pregunta. Supongamos que desea definir una tarea sbt que ejecutará una solicitud HTTP con scalaj-http. Intuitivamente, podríamos intentar lo siguiente 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
...
}
Sin embargo, este error indicará que falta import scalaj.http._
. ¿Cómo es esto posible cuando, justo encima, añadimos scalaj-http
a libraryDependencies
? Además, ¿por qué funciona cuando, en cambio, agregamos la dependencia a project/build.sbt
?
// project/build.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
La respuesta es que en fooTask
realidad es parte de un proyecto Scala separado de su proyecto principal. Este proyecto de Scala diferente se puede encontrar en un project/
directorio que tiene su propio target/
directorio donde residen sus clases compiladas. De hecho, debajo project/target/config-classes
debería haber una clase que se descompila en 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
es simplemente un miembro de un objeto Scala regular llamado $9c2192aea3f1db3c251d
. Claramente scalaj-http
debería ser una dependencia de la definición del proyecto $9c2192aea3f1db3c251d
y no la dependencia del proyecto adecuado. Por lo tanto, debe declararse en project/build.sbt
lugar de build.sbt
, porque project
es donde reside la definición de compilación del proyecto Scala.
Para señalar que la definición de compilación es solo otro proyecto de Scala, ejecute sbt consoleProject
. Esto cargará Scala REPL con el proyecto de definición de compilación en la ruta de clases. Debería ver una importación a lo largo de las líneas de
import $9c2192aea3f1db3c251d
Así que ahora podemos interactuar directamente con el proyecto de definición de compilación llamándolo con Scala en lugar de build.sbt
DSL. Por ejemplo, lo siguiente se ejecutafooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
bajo proyecto raíz hay un DSL especial que ayuda a definir la definición de compilación del proyecto Scala project/
.
Y la definición de compilación del proyecto Scala, puede tener su propia definición de compilación en el proyecto Scala project/project/
y así sucesivamente. Decimos que sbt es recursivo .
sbt es paralelo por defecto
sbt crea DAG a partir de tareas. Esto le permite analizar dependencias entre tareas y ejecutarlas en paralelo e incluso realizar la deduplicación. build.sbt
DSL está diseñado con esto en mente, lo que podría conducir a una semántica inicialmente sorprendente. ¿Cuál crees que es el orden de ejecución en el siguiente fragmento?
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, uno podría pensar que el flujo aquí es primero imprimir, hello
luego ejecutar a
y luego realizar una b
tarea. Sin embargo, esta realidad significa ejecutar a
y b
en paralelo , y antes de println("hello")
lo que
a
b
hello
o porque el orden de a
y b
no está garantizado
b
a
hello
Quizás paradójicamente, en sbt es más fácil hacerlo en paralelo que en serie. Si necesita pedidos en serie, tendrá que usar elementos especiales como Def.sequential
o Def.taskDyn
emular para la comprensión .
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
es parecido a
for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()
donde vemos que no hay dependencias entre componentes, mientras que
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
es parecido 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 }
donde vemos sum
depende y hay que esperar a
y b
.
En otras palabras
- para semántica aplicativa , utilice
.value
- para uso de semántica monádica
sequential
otaskDyn
Considere otro fragmento semánticamente confuso como resultado de la naturaleza de construcción de dependencias de value
, donde en lugar de
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^
tenemos que escribir
val x = settingKey[String]("")
x := version.value
Tenga en cuenta que la sintaxis .value
trata sobre las relaciones en el DAG y no significa
"dame el valor ahora mismo"
en cambio, significa algo como
"la persona que llama depende de mí primero, y una vez que sepa cómo encaja todo el DAG, podré proporcionarle a la persona que llama el valor solicitado"
Así que ahora podría ser un poco más claro por qué x
no se puede asignar un valor todavía; aún no hay ningún valor disponible en la etapa de construcción de relaciones.
Podemos ver claramente una diferencia en la semántica entre Scala propiamente dicho y el lenguaje DSL en build.sbt
. Aquí hay algunas reglas prácticas que me funcionan
- DAG está hecho de expresiones de tipo
Setting[T]
- En la mayoría de los casos, simplemente usamos
.value
sintaxis y sbt se encargará de establecer la relación entreSetting[T]
- Ocasionalmente tenemos que modificar manualmente una parte de DAG y para eso usamos
Def.sequential
oDef.taskDyn
- Una vez que se solucionan estas rarezas sintéticas de ordenación / relación, podemos confiar en la semántica habitual de Scala para construir el resto de la lógica empresarial de las tareas.
Comandos vs tareas
Los comandos son una forma perezosa de salir del DAG. Usando comandos, es fácil mutar el estado de compilación y serializar las tareas como desee. El costo es que perdemos la paralelización y la deduplicación de las tareas proporcionadas por DAG, de qué manera las tareas deberían ser la opción preferida. Puede pensar en los comandos como una especie de grabación permanente de una sesión que se puede hacer en el interior sbt shell
. Por ejemplo, dado
vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value
considerar el resultado de la siguiente sesión
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
En particular, no cómo mutamos el estado de construcción con set x := 41
. Comandos nos permite realizar una grabación permanente de la sesión anterior, por ejemplo
commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}
También podemos hacer que el comando sea seguro usando Project.extract
yrunTask
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
}
Alcances
Los alcances entran en juego cuando intentamos responder a los siguientes tipos de preguntas
- ¿Cómo definir la tarea una vez y ponerla a disposición de todos los subproyectos en la compilación multiproyecto?
- ¿Cómo evitar tener dependencias de prueba en el classpath principal?
sbt tiene un espacio de alcance de varios ejes que se puede navegar usando la sintaxis de barra , por ejemplo,
show root / Compile / compile / scalacOptions
| | | |
project configuration task key
Personalmente, rara vez tengo que preocuparme por el alcance. A veces quiero compilar solo fuentes de prueba
Test/compile
o quizás ejecutar una tarea en particular de un subproyecto en particular sin tener que navegar primero a ese proyecto con project subprojB
subprojB/Test/compile
Creo que las siguientes reglas generales ayudan a evitar complicaciones en la determinación del alcance
- no tiene varios
build.sbt
archivos, sino solo uno maestro bajo el proyecto raíz que controla todos los demás subproyectos
- compartir tareas a través de complementos automáticos
- factorizar la configuración común en Scala simple
val
y agregarlo explícitamente a cada subproyecto
Construcción multiproyecto
En lugar de varios archivos build.sbt para cada subproyecto
.
├── 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
Tener un solo maestro build.sbt
para gobernarlos a todos
.
├── 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
Existe una práctica común de factorizar configuraciones comunes en compilaciones multiproyecto
defina una secuencia de configuraciones comunes en un val y agréguelas a cada proyecto. Menos conceptos para aprender de esa manera.
por ejemplo
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)
Navegación de proyectos
projects // list all projects
project multi1 // change to particular project
Complementos
Recuerde que la definición de compilación es un proyecto Scala adecuado que reside debajo project/
. Aquí es donde definimos un complemento creando .scala
archivos
. // 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
Aquí hay un complemento automático mínimo enproject/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
}
)
}
La anulación
override def requires = plugins.JvmPlugin
debe permitir efectivamente el plug-in para todos los sub-proyectos sin tener que llamar explícitamente enablePlugin
en build.sbt
.
IntelliJ y sbt
Habilite la siguiente configuración (que realmente debería estar habilitada de forma predeterminada )
use sbt shell
debajo
Preferences | Build, Execution, Deployment | sbt | sbt projects
Referencias clave