Installation
brew install sbt
ou similaire installe sbt qui consiste techniquement en
Lorsque vous exécutez à sbt
partir du terminal, il exécute en fait le script bash du lanceur sbt. Personnellement, je n'ai jamais eu à m'inquiéter de cette trinité, et j'ai juste utilisé sbt comme si c'était une seule chose.
Configuration
Pour configurer sbt pour un projet particulier, enregistrez le .sbtopts
fichier à la racine du projet. Pour configurer sbt à l'échelle du système, modifiez /usr/local/etc/sbtopts
. L'exécution sbt -help
devrait vous indiquer l'emplacement exact. Par exemple, pour donner plus de mémoire à sbt en tant qu'exécution unique sbt -mem 4096
, ou enregistrer -mem 4096
dans .sbtopts
ou sbtopts
pour que l'augmentation de la mémoire prenne effet de manière permanente.
Structure du projet
sbt new scala/scala-seed.g8
crée une structure de projet sbt Hello World minimale
.
├── 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
Commandes fréquentes
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
Myriade de coquillages
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 définition de construction est un projet Scala approprié
C'est l'un des concepts sbt idiomatiques clés. Je vais essayer d'expliquer avec une question. Supposons que vous souhaitiez définir une tâche sbt qui exécutera une requête HTTP avec scalaj-http. Intuitivement, nous pourrions essayer ce qui suit à l'intérieurbuild.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
...
}
Cependant, cela entraînera une erreur en disant qu'il manque import scalaj.http._
. Comment est - ce possible quand nous, juste au- dessus, ajouté scalaj-http
à libraryDependencies
? De plus, pourquoi cela fonctionne-t-il quand, à la place, nous ajoutons la dépendance project/build.sbt
?
// project/build.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
La réponse est que cela fooTask
fait en fait partie d'un projet Scala distinct de votre projet principal. Ce projet Scala différent se trouve dans le project/
répertoire qui a son propre target/
répertoire où résident ses classes compilées. En fait, sous project/target/config-classes
il devrait y avoir une classe qui se décompile en quelque chose comme
object $9c2192aea3f1db3c251d extends scala.AnyRef {
lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
lazy val root : sbt.Project = { /* compiled code */ }
}
Nous voyons que fooTask
c'est simplement un membre d'un objet Scala régulier nommé $9c2192aea3f1db3c251d
. De toute évidence, cela scalaj-http
devrait être une dépendance de la définition du projet $9c2192aea3f1db3c251d
et non la dépendance du projet approprié. Par conséquent, il doit être déclaré dans project/build.sbt
au lieu de build.sbt
, car project
c'est là que réside le projet Scala de définition de construction.
Pour indiquer que la définition de construction n'est qu'un autre projet Scala, exécutez sbt consoleProject
. Cela chargera Scala REPL avec le projet de définition de build sur le chemin de classe. Vous devriez voir une importation du type
import $9c2192aea3f1db3c251d
Nous pouvons donc maintenant interagir directement avec le projet de définition de construction en l'appelant avec Scala proprement dit au lieu de build.sbt
DSL. Par exemple, ce qui suit exécutefooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
sous le projet racine est un DSL spécial qui aide à définir la définition de construction du projet Scala sous project/
.
Et le projet Scala de définition de build, peut avoir son propre projet Scala de définition de build sous project/project/
et ainsi de suite. Nous disons que sbt est récursif .
sbt est parallèle par défaut
sbt construit le DAG à partir des tâches. Cela lui permet d'analyser les dépendances entre les tâches et de les exécuter en parallèle et même d'effectuer la déduplication. build.sbt
DSL est conçu dans cet esprit, ce qui pourrait conduire à une sémantique initialement surprenante. Selon vous, quel est l'ordre d'exécution dans l'extrait suivant?
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
}
Intuitivement, on pourrait penser que le flux consiste d'abord à imprimer hello
puis à exécuter a
, puis à effectuer une b
tâche. Cependant, cela signifie en fait exécuter a
et b
en parallèle , et avant println("hello")
cela
a
b
hello
ou parce que l'ordre de a
et b
n'est pas garanti
b
a
hello
Peut-être paradoxalement, en sbt, il est plus facile de faire du parallèle que de la série. Si vous avez besoin d'une commande en série, vous devrez utiliser des choses spéciales comme Def.sequential
ou Def.taskDyn
émuler pour la compréhension .
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
est similaire à
for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()
où nous voyons qu'il n'y a pas de dépendances entre les composants, tandis 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
est similaire à
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 }
où nous voyons sum
dépend et doit attendre a
et b
.
En d'autres termes
- pour la sémantique applicative , utilisez
.value
- pour une utilisation sémantique monadique
sequential
outaskDyn
Considérez un autre extrait de code sémantiquement déroutant en raison de la nature de construction de dépendances de value
, où au lieu de
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^
il faut écrire
val x = settingKey[String]("")
x := version.value
Notez que la syntaxe .value
concerne les relations dans le DAG et ne signifie pas
"donne-moi la valeur maintenant"
au lieu de cela, cela signifie quelque chose comme
"mon interlocuteur dépend d'abord de moi, et une fois que je saurai comment l'ensemble du DAG s'emboîte, je serai en mesure de fournir à mon interlocuteur la valeur demandée"
Alors maintenant, il pourrait être un peu plus clair pourquoi x
une valeur ne peut pas encore être attribuée; il n'y a pas encore de valeur disponible à l'étape de l'établissement de relations.
Nous pouvons clairement voir une différence de sémantique entre Scala proprement dit et le langage DSL dans build.sbt
. Voici quelques règles de base qui fonctionnent pour moi
- Le DAG est composé d'expressions de type
Setting[T]
- Dans la plupart des cas, nous utilisons simplement la
.value
syntaxe et sbt se chargera d'établir une relation entreSetting[T]
- Parfois, nous devons modifier manuellement une partie du DAG et pour cela nous utilisons
Def.sequential
ouDef.taskDyn
- Une fois que ces bizarreries syntatiques d'ordre / relation sont prises en compte, nous pouvons nous fier à la sémantique Scala habituelle pour construire le reste de la logique métier des tâches.
Commandes vs tâches
Les commandes sont un moyen paresseux de sortir du DAG. En utilisant des commandes, il est facile de modifier l'état de construction et de sérialiser les tâches comme vous le souhaitez. Le coût est que nous perdons la parallélisation et la déduplication des tâches fournies par DAG, ce qui devrait être le choix préféré des tâches. Vous pouvez considérer les commandes comme une sorte d'enregistrement permanent d'une session que l'on pourrait faire à l'intérieur sbt shell
. Par exemple, étant donné
vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value
considérez le résultat de la session suivante
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 particulier, pas comment nous modifions l'état de construction avec set x := 41
. Les commandes nous permettent de faire un enregistrement permanent de la session ci-dessus, par exemple
commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}
Nous pouvons également rendre la commande sécurisée en utilisant Project.extract
etrunTask
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
}
Portées
Les champs d'application entrent en jeu lorsque nous essayons de répondre aux types de questions suivants
- Comment définir une tâche une fois et la rendre disponible à tous les sous-projets en construction multi-projets?
- Comment éviter d'avoir des dépendances de test sur le chemin de classe principal?
sbt a un espace de portée multi-axes qui peut être parcouru à l'aide de la syntaxe slash , par exemple,
show root / Compile / compile / scalacOptions
| | | |
project configuration task key
Personnellement, je me retrouve rarement à me soucier de la portée. Parfois, je veux compiler uniquement des sources de test
Test/compile
ou peut-être exécuter une tâche particulière à partir d'un sous-projet particulier sans avoir à naviguer vers ce projet avec project subprojB
subprojB/Test/compile
Je pense que les règles empiriques suivantes aident à éviter les complications de portée
- n'ont pas plusieurs
build.sbt
fichiers mais seulement un seul maître sous le projet racine qui contrôle tous les autres sous-projets
- partager des tâches via des plugins automatiques
- factoriser les paramètres communs dans Scala simple
val
et l'ajouter explicitement à chaque sous-projet
Construction multi-projets
Au lieu de plusieurs fichiers build.sbt pour chaque sous-projet
.
├── 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
Avoir un seul maître build.sbt
pour les gouverner tous
.
├── 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
Il existe une pratique courante de prise en compte des paramètres communs dans les versions multi-projets
définir une séquence de paramètres communs dans un val et les ajouter à chaque projet. Moins de concepts à apprendre de cette façon.
par exemple
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)
Navigation des projets
projects // list all projects
project multi1 // change to particular project
Plugins
N'oubliez pas que la définition de construction est un projet Scala approprié qui réside sous project/
. C'est ici que nous définissons un plugin en créant des .scala
fichiers
. // 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
Voici un plugin automatique minimal sousproject/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
}
)
}
Le remplacement
override def requires = plugins.JvmPlugin
devrait permettre efficacement le plug - in pour tous les sous-projets sans avoir à appeler explicitement enablePlugin
dans build.sbt
.
IntelliJ et sbt
Veuillez activer le paramètre suivant (qui devrait vraiment être activé par défaut )
use sbt shell
sous
Preferences | Build, Execution, Deployment | sbt | sbt projects
Références clés