Installation
brew install sbt
oder ähnliches installiert sbt, aus dem technisch gesehen besteht
Wenn Sie sbt
vom Terminal ausführen , wird tatsächlich das Bash-Skript sbt launcher ausgeführt. Persönlich musste ich mir nie Sorgen um diese Dreifaltigkeit machen und sbt einfach so verwenden, als wäre es eine einzige Sache.
Aufbau
Um sbt für ein bestimmtes Projekt zu konfigurieren, speichern Sie die .sbtopts
Datei im Stammverzeichnis des Projekts. Um sbt systemweit zu konfigurieren, ändern Sie /usr/local/etc/sbtopts
. Die Ausführung sbt -help
sollte Ihnen den genauen Ort mitteilen. Zum Beispiel zu geben sbt mehr Speicher als Einmal ausführen sbt -mem 4096
oder speichern -mem 4096
in .sbtopts
oder sbtopts
für Speichererhöhungseffekt dauerhaft zu übernehmen.
Projektstruktur
sbt new scala/scala-seed.g8
erstellt eine minimale Hello World sbt-Projektstruktur
.
├── 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
Häufige Befehle
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
Unzählige Muscheln
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
Die Build-Definition ist ein richtiges Scala-Projekt
Dies ist eines der wichtigsten idiomatischen sbt-Konzepte. Ich werde versuchen, mit einer Frage zu erklären. Angenommen, Sie möchten eine sbt-Task definieren, die eine HTTP-Anforderung mit scalaj-http ausführt. Intuitiv könnten wir das Folgende versuchenbuild.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
...
}
Dies führt jedoch zu einem Fehler bei der Meldung "fehlt" import scalaj.http._
. Wie ist das möglich , wenn wir direkt oben, hinzugefügt scalaj-http
zu libraryDependencies
? Warum funktioniert es außerdem, wenn wir stattdessen die Abhängigkeit hinzufügen project/build.sbt
?
// project/build.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
Die Antwort ist, dass dies fooTask
tatsächlich Teil eines von Ihrem Hauptprojekt getrennten Scala- Projekts ist. Dieses andere Scala-Projekt befindet sich im project/
Verzeichnis, das über ein eigenes target/
Verzeichnis verfügt, in dem sich die kompilierten Klassen befinden. In der Tat project/target/config-classes
sollte es unter eine Klasse geben, die sich zu so etwas dekompiliert
object $9c2192aea3f1db3c251d extends scala.AnyRef {
lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
lazy val root : sbt.Project = { /* compiled code */ }
}
Wir sehen, dass dies fooTask
einfach ein Mitglied eines regulären Scala-Objekts mit dem Namen ist $9c2192aea3f1db3c251d
. Es scalaj-http
sollte klar sein, dass eine Abhängigkeit vom Projekt definiert wird $9c2192aea3f1db3c251d
und nicht die Abhängigkeit des richtigen Projekts. Daher muss es project/build.sbt
anstelle von deklariert werden build.sbt
, da project
sich dort das Build-Definitions-Scala-Projekt befindet.
Führen Sie aus, um den Punkt zu bestimmen, dass die Build-Definition nur ein weiteres Scala-Projekt ist sbt consoleProject
. Dadurch wird Scala REPL mit dem Build-Definitionsprojekt im Klassenpfad geladen. Sie sollten einen Import nach dem Vorbild von sehen
import $9c2192aea3f1db3c251d
Jetzt können wir direkt mit dem Build-Definitionsprojekt interagieren, indem wir es mit Scala anstelle von build.sbt
DSL aufrufen . Zum Beispiel wird Folgendes ausgeführtfooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
Unter Root-Projekt befindet sich ein spezielles DSL, mit dessen Hilfe die Build-Definition des Scala-Projekts definiert werden kann project/
.
Und Build Definition Scala-Projekt, kann ein eigenes Build Definition Scala-Projekt unter project/project/
und so weiter haben. Wir sagen, sbt ist rekursiv .
sbt ist standardmäßig parallel
sbt baut DAG aus Aufgaben auf. Auf diese Weise können Abhängigkeiten zwischen Aufgaben analysiert und parallel ausgeführt und sogar eine Deduplizierung durchgeführt werden. build.sbt
DSL wurde unter diesem Gesichtspunkt entwickelt, was zu einer zunächst überraschenden Semantik führen kann. Was denkst du, ist die Reihenfolge der Ausführung im folgenden Ausschnitt?
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
}
Intuitiv könnte man denken, dass der Fluss hier darin besteht, zuerst zu drucken, hello
dann auszuführen a
und dann eine b
Aufgabe auszuführen . Allerdings bedeutet dies tatsächlich ausführen a
und b
in parallel , und vor println("hello")
so
a
b
hello
oder weil die Reihenfolge von a
und b
nicht garantiert ist
b
a
hello
Vielleicht paradoxerweise ist es in sbt einfacher, parallel als seriell zu arbeiten. Wenn Sie eine Serienbestellung benötigen, müssen Sie spezielle Dinge wie Def.sequential
oder Def.taskDyn
zum Verständnis nachahmen .
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
ist ähnlich wie
for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()
wo wir sehen, gibt es keine Abhängigkeiten zwischen Komponenten, während
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
ist ähnlich wie
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 }
wo wir sehen , sum
hängt davon ab , und hat zu warten a
und b
.
Mit anderen Worten
- Verwenden Sie für die anwendungsbezogene Semantik
.value
- für monadische Semantik verwenden
sequential
odertaskDyn
Betrachten Sie ein anderes semantisch verwirrendes Snippet als Ergebnis der Abhängigkeitsbildung von value
, wo statt
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^
wir müssen schreiben
val x = settingKey[String]("")
x := version.value
Beachten Sie, dass sich die Syntax .value
auf Beziehungen in der DAG bezieht und nicht bedeutet
"Gib mir jetzt den Wert"
stattdessen bedeutet es so etwas wie
"Mein Anrufer hängt zuerst von mir ab, und sobald ich weiß, wie die gesamte DAG zusammenpasst, kann ich meinem Anrufer den gewünschten Wert liefern."
Jetzt ist es vielleicht etwas klarer, warum x
noch kein Wert zugewiesen werden kann. In der Phase des Beziehungsaufbaus ist noch kein Wert verfügbar.
Wir können deutlich einen Unterschied in der Semantik zwischen Scala und der DSL-Sprache in erkennen build.sbt
. Hier sind einige Faustregeln, die für mich funktionieren
- DAG besteht aus Typausdrücken
Setting[T]
- In den meisten Fällen verwenden wir einfach die
.value
Syntax und sbt kümmert sich um die Herstellung der Beziehung zwischenSetting[T]
- Gelegentlich müssen wir einen Teil der DAG manuell anpassen und dafür verwenden wir
Def.sequential
oderDef.taskDyn
- Sobald diese Ordnungs- / Beziehungssyntatik-Kuriositäten behoben sind, können wir uns auf die übliche Scala-Semantik verlassen, um den Rest der Geschäftslogik von Aufgaben aufzubauen.
Befehle gegen Aufgaben
Befehle sind ein fauler Ausweg aus der DAG. Mit Befehlen können Sie den Build-Status einfach mutieren und Aufgaben nach Ihren Wünschen serialisieren. Die Kosten sind, dass wir die Parallelisierung und Deduplizierung der von der DAG bereitgestellten Aufgaben verlieren. Auf diese Weise sollten Aufgaben die bevorzugte Wahl sein. Sie können sich Befehle als eine Art permanente Aufzeichnung einer Sitzung vorstellen, die Sie möglicherweise im Inneren ausführen sbt shell
. Zum Beispiel gegeben
vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value
Betrachten Sie die Ausgabe der folgenden Sitzung
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
Insbesondere nicht, wie wir den Build-Status mutieren set x := 41
. Mit Befehlen können wir beispielsweise die obige Sitzung permanent aufzeichnen
commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}
Wir können den Befehl auch mit Project.extract
und typsicher machenrunTask
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
}
Geltungsbereich
Bereiche kommen ins Spiel, wenn wir versuchen, die folgenden Fragen zu beantworten
- Wie definiere ich eine Aufgabe einmal und stelle sie allen Teilprojekten im Multi-Projekt-Build zur Verfügung?
- Wie vermeide ich Testabhängigkeiten vom Hauptklassenpfad?
sbt verfügt über einen mehrachsigen Gültigkeitsbereich , der beispielsweise mithilfe der Schrägstrichsyntax navigiert werden kann.
show root / Compile / compile / scalacOptions
| | | |
project configuration task key
Persönlich muss ich mir selten Sorgen um den Umfang machen. Manchmal möchte ich nur Testquellen kompilieren
Test/compile
oder führen Sie möglicherweise eine bestimmte Aufgabe aus einem bestimmten Teilprojekt aus, ohne zuvor mit zu diesem Projekt navigieren zu müssen project subprojB
subprojB/Test/compile
Ich denke, die folgenden Faustregeln helfen dabei, Komplikationen beim Scoping zu vermeiden
- Sie haben nicht mehrere
build.sbt
Dateien, sondern nur eine einzige Master-Datei unter dem Root-Projekt, die alle anderen Unterprojekte steuert
- Aufgaben über Auto-Plugins teilen
- Faktor aus allgemeinen Einstellungen in Klar Scala
val
und es explizit zu jedem Teilprojekt hinzufügen
Multi-Projekt-Build
Anstelle mehrerer build.sbt-Dateien für jedes Teilprojekt
.
├── 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
Haben Sie einen einzigen Meister build.sbt
, der sie alle regiert
.
├── 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
Es ist üblich , gemeinsame Einstellungen in Builds mit mehreren Projekten herauszufiltern
Definieren Sie eine Folge allgemeiner Einstellungen in einem Wert und fügen Sie sie jedem Projekt hinzu. Weniger Konzepte, um so zu lernen.
beispielsweise
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)
Projektnavigation
projects // list all projects
project multi1 // change to particular project
Plugins
Denken Sie daran, dass die Build-Definition ein richtiges Scala-Projekt ist, unter dem sich befindet project/
. Hier definieren wir ein Plugin, indem wir .scala
Dateien erstellen
. // 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
Hier ist ein minimales Auto-Plugin unterproject/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
}
)
}
Die Außerkraftsetzung
override def requires = plugins.JvmPlugin
sollte das Plugin effektiv für alle Unterprojekte aktivieren, ohne explizit aufrufen enablePlugin
zu müssen build.sbt
.
IntelliJ und sbt
Bitte aktivieren Sie die folgende Einstellung (die eigentlich standardmäßig aktiviert sein sollte )
use sbt shell
unter
Preferences | Build, Execution, Deployment | sbt | sbt projects
Wichtige Referenzen