¿Alguien puede explicar la forma correcta de usar SBT?

100

¡Voy a salir del armario con esto! No entiendo SBT. Ahí, lo dije, ahora ayúdame por favor.

Todos los caminos conducen a Roma, y que es el mismo para SBT: Para empezar a utilizar SBTno es SBT, SBT Launcher, SBT-extras, etc, y luego hay diferentes maneras de incluir y decidir sobre los repositorios. ¿Existe una "mejor" forma?

Lo pregunto porque a veces me pierdo un poco. La documentación de SBT es muy completa y completa, pero no sé cuándo usar build.sbto project/build.propertieso project/Build.scalao project/plugins.sbt.

Entonces se vuelve divertido, está el Scala-IDEy SBT- ¿Cuál es la forma correcta de usarlos juntos? ¿Qué viene primero, el huevo o la gallina?

Lo más importante es probablemente, ¿cómo encuentra los repositorios y versiones correctos para incluir en su proyecto? ¿Saco un machette y empiezo a abrirme camino hacia adelante? A menudo encuentro proyectos que incluyen todo y el fregadero de la cocina, y luego me doy cuenta de que no soy el único que se pierde un poco.

Como ejemplo simple, ahora mismo estoy comenzando un nuevo proyecto. Quiero utilizar las funciones más recientes de SLICKy Scalaprobablemente requiera la última versión de SBT. ¿Cuál es el punto sensato para empezar y por qué? ¿En qué archivo debería definirlo y cómo debería verse? Sé que puedo hacer que esto funcione, pero realmente me gustaría una opinión experta sobre dónde debería ir todo (por qué debería ir allí habrá una bonificación).

Lo he estado usando SBTpara proyectos pequeños durante más de un año. Usé SBTy luego SBT Extras(ya que hizo que algunos dolores de cabeza desaparecieran mágicamente), pero no estoy seguro de por qué debería usar uno u otro. Me estoy frustrando un poco por no entender cómo encajan las cosas ( SBTy los repositorios), y creo que le ahorraría muchas dificultades al próximo que venga de esta manera si esto pudiera explicarse en términos humanos.

Jack
fuente
2
¿Qué quiere decir exactamente con "hay Scala-IDE y SBT"? Usted define su proyecto con sbt y sbt puede generar un proyecto ide (eclipse oder intellij). Así SBT es lo primero ...
Ene
2
@Jan Mencioné eso porque Scala-IDE usa SBT como administrador de compilación. Consulte assemblyla.com/spaces/scala-ide/wiki/SBT-based_build_manager y más abajo en la publicación mencionan "No es necesario definir su archivo de proyecto SBT". que encontré confuso.
Jack
Okay. Como suelo usar intellij (o sublime) para editar scala, no lo sabía. ¿Supongo que el constructor genera sus propias configuraciones de sbt?
Ene
2
@JacobusR El hecho de que Scala IDE use SBT para construir las fuentes de su proyecto es un detalle de implementación, y los usuarios no deben preocuparse por esto. Realmente hay 0 implicaciones. Fuera de Eclipse, los usuarios pueden construir un proyecto con SBT, Maven, Ant, ..., y eso no hará ninguna diferencia para Scala IDE. Una cosa más, incluso si tiene un proyecto SBT, al Scala IDE no le importa, es decir, no busca que usted Build.scalaconfigure el classpath, y es por eso que realmente necesita sbteclipse para generar el Eclipse .classpath. Espero que esto ayude.
Mirco Dotta
1
@Jan Scala IDE agregó a la confusión, y sí, la documentación que brinda una imagen más amplia sobre la configuración de un buen entorno de desarrollo de Scala y una guía sólida de flujos de trabajo de programación adecuados sería muy útil.
Jack

Respuestas:

29

Lo más importante es probablemente, ¿cómo encuentra los repositorios y versiones correctos para incluir en su proyecto? ¿Saco un machette y empiezo a abrirme camino hacia adelante? A menudo encuentro proyectos que incluyen todo y el fregadero de la cocina.

Para las dependencias basadas en Scala, iría con lo que recomiendan los autores. Por ejemplo: http://code.google.com/p/scalaz/#SBT indica utilizar:

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

O https://github.com/typesafehub/sbteclipse/ tiene instrucciones sobre dónde agregar:

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

Para las dependencias basadas en Java, utilizo http://mvnrepository.com/ para ver lo que hay, luego hago clic en la pestaña SBT. Por ejemplo, http://mvnrepository.com/artifact/net.sf.opencsv/opencsv/2.3 indica utilizar:

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

Luego saca el machette y comienza a abrir camino hacia adelante. Si tiene suerte, no terminará usando frascos que dependan de algunos de los mismos frascos pero con versiones incompatibles. Dado el ecosistema de Java, a menudo terminas incluyendo todo y el fregadero de la cocina y se necesita un poco de esfuerzo para eliminar las dependencias o asegurarte de que no faltan las dependencias necesarias.

Como ejemplo simple, ahora mismo estoy comenzando un nuevo proyecto. Quiero utilizar las últimas funciones de SLICK y Scala y probablemente esto requiera la última versión de SBT. ¿Cuál es el punto sensato para empezar y por qué?

Creo que el punto es crear inmunidad al sbt gradualmente .

Asegúrese de comprender:

  1. formato de alcances {<build-uri>}<project-id>/config:key(for task-key)
  2. los 3 sabores de configuración ( SettingKey, TaskKey, InputKey) - lea la sección denominada "llaves de la tarea" en http://www.scala-sbt.org/release/docs/Getting-Started/Basic-Def

Mantenga esas 4 páginas abiertas en todo momento para que pueda saltar y buscar varias definiciones y ejemplos:

  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

Hacer un uso máximo de showy inspect y la finalización pestaña para familiarizarse con los valores reales de los ajustes, sus dependencias, definiciones y ajustes relacionados. No creo que las relaciones que descubrirás usando inspectestén documentadas en ninguna parte. Si hay una forma mejor, quiero saberlo.

huynhjl
fuente
25

La forma en que uso sbt es:

  1. Use sbt-extras : solo obtenga el script de shell y agréguelo a la raíz de su proyecto
  2. Cree una projectcarpeta con un MyProject.scalaarchivo para configurar sbt. Prefiero esto al build.sbtenfoque: es escalable y es más flexible
  3. Cree un project/plugins.sbtarchivo y agregue el complemento apropiado para su IDE. Ya sea sbt-eclipse, sbt-idea o ensime-sbt-cmd para que pueda generar archivos de proyecto para eclipse, intellij o ensime.
  4. Inicie sbt en la raíz de su proyecto y genere los archivos del proyecto para su IDE
  5. Lucro

No me molesto en verificar los archivos del proyecto IDE ya que son generados por sbt, pero puede haber razones por las que desee hacerlo.

Puede ver un ejemplo configurado como este aquí .

Channing Walton
fuente
Gracias por la buena respuesta. Acepté la otra respuesta, porque cubre más terreno, y voté a favor de la suya porque también es muy buena. Habría aceptado ambos si pudiera.
Jack
0

Use Typesafe Activator, una forma elegante de llamar a sbt, que viene con plantillas de proyecto y semillas: 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
fuente
5
Soy parcial a la idea de que, en caso de duda, no es probable que agregar más magia a la mezcla resuelva sus problemas.
Cúbico
0

Instalación

brew install sbt o instalaciones similares sbt que técnicamente hablando consisten en

Cuando se ejecuta sbtdesde 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 .sbtoptsarchivo en la raíz del proyecto. Para configurar sbt en todo el sistema, modificar /usr/local/etc/sbtopts. La ejecución sbt -helpdeberí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 4096en .sbtoptso sbtoptspara 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-httpa 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 fooTaskrealidad 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-classesdeberí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 fooTaskes simplemente un miembro de un objeto Scala regular llamado $9c2192aea3f1db3c251d. Claramente scalaj-httpdebería ser una dependencia de la definición del proyecto $9c2192aea3f1db3c251dy no la dependencia del proyecto adecuado. Por lo tanto, debe declararse en project/build.sbtlugar de build.sbt, porque projectes 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.sbtDSL. Por ejemplo, lo siguiente se ejecutafooTask

$9c2192aea3f1db3c251d.fooTask.eval

build.sbtbajo 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.sbtDSL 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, helloluego ejecutar ay luego realizar una btarea. Sin embargo, esta realidad significa ejecutar ay ben paralelo , y antes de println("hello") lo que

a
b
hello

o porque el orden de ay bno 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.sequentialo Def.taskDynemular 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 sumdepende y hay que esperar ay b.

En otras palabras

  • para semántica aplicativa , utilice.value
  • para uso de semántica monádicasequential 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 .valuetrata 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é xno 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 .valuesintaxis 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.sequentialoDef.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.extractyrunTask

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.sbtarchivos, 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 valy 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.sbtpara 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 .scalaarchivos

.                          // 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 enablePluginen 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

Mario Galic
fuente