Blog

Das Scala Build Tool - Es dreht sich alles um Einstellungen und Aufgaben

Dennis Vriend

Aktualisiert Oktober 22, 2025
74 Minuten

Das Scala Build Tool, kurz sbt, ist ein Build-Tool zum Erstellen von Quellcode. Es ist ein sehr fortschrittliches Tool, das auf einer Workflow-Engine basiert. Im Gegensatz zu anderen Build-Tools ist Scala sehr einfach, wenn Sie nur ein paar Konzepte kennen.

Konzepte

Lassen Sie uns die Kernkonzepte unseres Builds vorstellen. Wenn wir ein Projekt zum Beispiel mit dem folgenden Befehl erstellen:

sbt new dnvriend/scala-seed.g8

Sbt wird eine Verzeichnisstruktur wie die folgende erstellen:

study-sbt
├── LICENSE
├── README.md
├── build.sbt
└── src
    ├── main
    │   └── scala
    │       └── com
    │           └── github
    │               └── dnvriend
    │                   └── HelloWorld.scala
    └── test
        └── scala
            └── com
                └── github
                    └── dnvriend
                        ├── PersonTest.scala
                        └── TestSpec.scala

Wir sehen die bekannte Verzeichnisstruktur src/main, die sowohl unseren zu erstellenden Scala- und Java-Quellcode als auch eine Datei build.sbt enthält, die unseren Build beschreibt. Die Datei build.sbt ist optional. Wenn wir keine build.sbt Datei haben, verwendet sbt Standardwerte, um den Quellcode zu erstellen.
Wenn wir als Entwickler nichts anderes angeben (z.B. in der Datei build.sbt), geht sbt davon aus, dass nur ein einziges Projekt erstellt werden soll. Dieses einzelne Projekt wird 'das Standardprojekt' genannt und sbt nimmt an, dass das Basisverzeichnis dieses einzelnen Projekts das aktuelle Verzeichnis ist, also '{.}'.
Im obigen Beispiel und bei 90% aller Projekte wird dies der Fall sein. Eine Verzeichnisstruktur wie oben, höchstwahrscheinlich mit einer build.sbt-Datei, die einige Einstellungen wie z.B. einen Namen und eine Version definiert.

Bauen Sie

Was ist der Build? Nun, der Build ist einfach eine Sammlung von Projekten, die Sbt erstellen soll. Wir als Entwickler geben Sbt an, welche Projekte es gibt, wo sie sich befinden und wie sie heißen. Wenn wir keine Projekte angeben, geht Sbt von einem einzigen Projekt im aktuellen Verzeichnis aus, aber wir als Entwickler können andere Projekte definieren, die Sbt bauen soll. Der Build sagt also etwas über ein oder mehrere Projekte aus, die Sbt bauen soll.

Projekt

Ein Build bezieht sich auf ein oder mehrere Projekte, und ein Projekt bezieht sich auf die Einstellungen. Zum Beispiel, mit welchen Bibliotheken das Projekt kompiliert werden soll, welche Scala-Version verwendet werden soll und vielleicht auch, welche Version das Projekt ist, z.B. die 1.0.0-SNAPSHOT-Version usw.

Einstellungen

Ein Build erstellt also Projekte, Projekte definieren sich über Einstellungen. Nun, eine Einstellung ist ein Schlüssel -> Wertepaar. Sie lautet etwa so:

name := "my-project"
version := "1.0.0-SNAPSHOT"
libraryDependencies += "foo" %% "bar" %% "1.0.0"

Eine Einstellung ist also einfach ein Schlüssel -> Wertepaar. Das Besondere an den Einstellungen ist, dass das Schlüssel -> Wertepaar beim Start von sbt initialisiert wird, also wenn Sie SBT starten. Die Werte werden also nur einmal initialisiert.

Aufgaben

Ein Build erstellt also Projekte, Projekte definieren sich über Einstellungen, und Einstellungen sind Schlüssel -> Wertepaare, die nur einmal beim Start von Sbt initialisiert werden. Was sind dann Tasks? Ein Task ist ein Schlüssel -> Wertepaar, das bei Bedarf ausgewertet wird. Ein Task existiert, um jedes Mal ausgewertet zu werden, wenn er benötigt wird. Meistens werden Tasks für Seiteneffekte verwendet, wie z.B. der Task 'clean' oder der Task 'compile'.
Da ein Task ein Schlüssel -> Wertepaar ist, genau wie eine Einstellung (die auch ein Schlüssel -> Wertepaar ist), können Sie einen Task 'aufrufen', indem Sie einfach den Namen des Schlüssels eingeben und Sbt wird den Schlüssel zu einem Wert auswerten.
Wir können das Ergebnis einer Einstellung oder eines Tasks mit Hilfe des Befehls 'show' in der sbt-Konsole anzeigen. Wenn wir zum Beispiel 'show name' eingeben, wertet sbt den Schlüssel 'name' aus und gibt den ausgewerteten Wert zurück. Da es sich bei 'name' um eine Einstellung handelt, wurde die Initialisierung bereits beim Start von Sbt durchgeführt, so dass der Wert sofort zurückgegeben wird:

> name
[info] study-sbt

Wir können auch einen Task auswerten. Wie ich bereits erwähnt habe, ist ein Task nur ein Schlüssel -> Wertepaar, das bei Bedarf ausgewertet wird. Er muss also ausgewertet werden, wenn wir ihn brauchen und meistens verwenden wir einen Task, um Nebeneffekte wie den Task 'clean' auszuführen:

> show clean
[info] ()
[success] Total time: 0 s, completed 17-feb-2017 9:06:18

Der Task clean ergibt den Wert '()' vom Typ Unit, der zurückgegeben wird, da er Nebeneffekte wie das Löschen des Inhalts des Verzeichnisses './target' hat.

Rekapitulation bis jetzt

Ein Build enthält also ein oder mehrere Projekte. Ein Projekt definiert sich über Einstellungen. Eine Einstellung ist nur ein Schlüssel -> Wertepaar, das nur einmal initialisiert wird, und eine Aufgabe ist ein Schlüssel -> Wertepaar, das bei Bedarf ausgewertet wird.

Konfigurationen

Ich gehe davon aus, dass Sie bereits ein wenig wissen, wie sbt funktioniert und bereits damit arbeiten, so dass Sie wissen, dass sbt Tests unterstützt. Für Unit-Tests benötigen Sie z.B. ScalaTest und wenn Sie reaktive Anwendungen erstellen, die akka-testkit-Bibliothek als Abhängigkeit. Außerdem haben wir den Code, der zum Testen dient, von unserem Geschäftscode getrennt. Die Codebasen haben unterschiedliche Pfade, z.B. befindet sich der Testcode in './src/main/test' und diese Codebasis hat eine Abhängigkeit mit den Testbibliotheken, die unser Geschäftscode nicht hat.
Sbt verwendet Configurations, um Einstellungen zu segmentieren, damit es weiß, welche Einstellung bei der Ausführung einer bestimmten Aufgabe verwendet werden soll. In Sbt sind viele Konfigurationen definiert und Sie können auch Ihre eigenen definieren. Wir werden uns diese später etwas genauer ansehen.
Der Schlüssel sourceDirectories listet beispielsweise alle Verzeichnisse auf, die von Sbt zur Erstellung des Projekts verwendet werden sollen. Wenn wir zum Beispiel 'test' eingeben, werden die sourceDirectories für die Testkonfiguration verwendet. Nehmen wir an, wir möchten unseren eigenen Test-Task erstellen, den wir 'mytest' nennen:

// first define a task key
lazy val mytest = taskKey[Unit]("My test key to show how scoped settings work")

// then implement the task key
mytest := {
    val dirs = (sourceDirectories in Test).value
    println(dirs)
}

Wenn wir 'mytest' ausführen, lautet die Ausgabe:

> mytest
List(
 /Users/dennis/projects/study-sbt/src/test/scala-2.12, 
 /Users/dennis/projects/study-sbt/src/test/scala, 
 /Users/dennis/projects/study-sbt/src/test/java, 
 /Users/dennis/projects/study-sbt/target/scala-2.12/src_managed/test
)
[success] Total time: 0 s, completed 17-feb-2017 12:52:31

Angenommen, wir möchten unseren eigenen Kompilier-Task erstellen, den wir 'mycompile' nennen, dann könnte er so aussehen:

lazy val mycompile = taskKey[Unit]("My compile key to show how scoped settings work")

mycompile := {
    val dirs = (sourceDirectories in Compile).value
    println(dirs)
}
> mycompile
List(
 /Users/dennis/projects/study-sbt/src/main/scala-2.12, 
 /Users/dennis/projects/study-sbt/src/main/scala, 
 /Users/dennis/projects/study-sbt/src/main/java, 
 /Users/dennis/projects/study-sbt/target/scala-2.12/src_managed/main
)
[success] Total time: 0 s, completed 17-feb-2017 12:56:24

Der Schlüssel 'sourceDirectories' hat also für verschiedene Bereiche einen anderen Wert und es hängt von der Implementierung des Tasks ab, wo er nach dem Wert eines Schlüssels sucht. In unseren Beispielen suchen wir speziell nach einem Wert für (sourceDirectories in Test).value, um den Wert zu erhalten, und für (sourceDirectories in Compile).value.
Wir können sbt auch nach diesen Werten abfragen, ohne einen eigenen Task zu erstellen. Um beispielsweise den Wert des Schlüssels 'sourceDirectories' in der Konfiguration 'Test' zu erhalten, geben wir ein:

> test:sourceDirectories
[info] * /Users/dennis/projects/study-sbt/src/test/scala-2.12
[info] * /Users/dennis/projects/study-sbt/src/test/scala
[info] * /Users/dennis/projects/study-sbt/src/test/java
[info] * /Users/dennis/projects/study-sbt/target/scala-2.12/src_managed/test

Und für die Konfiguration 'Kompilieren' geben wir ein:

> compile:sourceDirectories
[info] * /Users/dennis/projects/study-sbt/src/main/scala-2.12
[info] * /Users/dennis/projects/study-sbt/src/main/scala
[info] * /Users/dennis/projects/study-sbt/src/main/java
[info] * /Users/dennis/projects/study-sbt/target/scala-2.12/src_managed/main

Natürlich muss eine Einstellung oder eine Aufgabe nicht in einer bestimmten Konfiguration vorhanden sein, wie z.B. die Aufgabe 'test' in der Konfiguration 'test' (natürlich):

> test:test
[info] Done updating.
[success] Total time: 1 s, completed Oct 31, 2017 7:05:17 AM

Aber die Aufgabe 'test' existiert nicht in der Konfiguration 'compile':

> compile:test
[error] No such setting/task
[error] compile:test
[error]

Aber wenn wir das Folgende eingeben:

> test
[success] Total time: 0 s, completed Oct 31, 2017 7:09:01 AM

Die Aufgabe 'test' funktioniert, ohne dass die 'test'-Konfiguration angegeben wird, so wie wir oben 'test:test' eingegeben haben, wie kommt das? Sie können Einstellungen und Aufgaben speziell für eine Konfiguration wie 'Test' oder 'Kompilieren' definieren, aber Sie können auch Einstellungen und Aufgaben definieren, die für alle Konfigurationen gelten. Die Aufgabe 'test' wird in allen Bereichen verfügbar gemacht. In diesem Fall ist das sinnvoll, denn es ist sehr praktisch, einfach 'test' einzugeben und die Aufgabe 'test' ausführen zu lassen.
In Sbt können Sie das Symbol '*' verwenden und das bedeutet 'alle'. Wenn wir also den Wert des Schlüssels 'name' in allen Konfigurationen abrufen wollen, können wir das auch eingeben:

> *:name
[info] study-sbt

Wenn wir z.B. nur 'name' eingeben, geht sbt davon aus, dass wir den Wert des Schlüssels '*:name' wollen, deshalb funktioniert es.
Nehmen wir an, wir wollen etwas Seltsames tun, wie z.B. den Namen des Projekts auf einen anderen Namen nur für die Konfiguration 'Test' setzen und sagen wir, dass der Wert in dieser Konfiguration 'study-sbt-in-test' sein wird, dann würden wir folgendes zu build.sbt hinzufügen:

name in Test := "study-sbt-in-test"

Alternativ können wir auch Folgendes in einer Sbt-Sitzung eingeben. In diesem Fall ist die Einstellung nicht dauerhaft, sondern nur für die Dauer der Sbt-Konsolensitzung:

set name in Test := "study-sbt-in-test"
[info] Defining test:name
[info] The new value will be used by test:packageBin::packageOptions, test:packageSrc::packageOptions
[info] Reapplying settings...
[info] Set current project to study-sbt (in build file:/Users/dennis/projects/study-sbt/)

Und lassen Sie uns den Namen in der Konfiguration 'Kompilieren' ändern:

set name in Compile := "study-sbt-in-compile"
[info] Defining test:name
[info] The new value will be used by test:packageBin::packageOptions, test:packageSrc::packageOptions
[info] Reapplying settings...
[info] Set current project to study-sbt (in build file:/Users/dennis/projects/study-sbt/)

Wir werden nun den Wert für name für verschiedene Bereiche abfragen:

> name
[info] study-sbt
> *:name
[info] study-sbt
> test:name
[info] study-sbt-in-test
> compile:name
[info] study-sbt-in-compile

Hinweis: In Sbt, das älter als v1.0 ist, war die Auflösung des Fallback-Wertes für den Schlüssel in der Konfiguration ein wenig fehlerhaft und die Auflösung des Fallback-Wertes funktionierte nicht immer wie erwartet.

Konfiguration nach Aufgabe

Eine Konfiguration kann auch auf einen bestimmten Task beschränkt werden. Fügen Sie z.B. Folgendes zu build.sbt hinzu:

lazy val mysetting = settingKey[String]("My setting")

mysetting := "mysetting for the current project, all configurations and all tasks"

mysetting in Test := "mysetting for the current project, for the Test configuration and all tasks"

mysetting in Test in MyTask := "mysetting for the current project, for the Test configuration for the task MyTask only"

lazy val MyTask = taskKey[Unit]("My task")

MyTask := {
    val str = (mysetting in Test in MyTask).value
    println(str)
}
> MyTask
mysetting for the current project, for the Test configuration for the task MyTask only
[success] Total time: 0 s, completed 17-feb-2017 13:15:28

Die Aufgabe MyTask wird speziell nach einem Wert für die Einstellung 'mysetting' suchen und zwar in der Konfiguration 'Test' und für die Aufgabe 'MyTask'. Wir haben dies durch die Eingabe von (mysetting in Test in MyTask).value spezifiziert, das ist also sehr spezifisch.
Wenn die Einstellung nicht gefunden wird, sucht sbt natürlich nach Ausweichalternativen, wenn Sie also die Zeile auskommentieren:

mysetting in Test in MyTask := "mysetting for the current project, for the Test configuration for the task MyTask only"

Sbt verwendet den nächsten Fallback und so weiter:

> MyTask
mysetting for the current project, for the Test configuration and all tasks
[success] Total time: 0 s, completed 17-feb-2017 13:18:38

Und nun kommentieren Sie auch die Zeile aus:

mysetting in Test := "mysetting for the current project, for the Test configuration and all tasks"

Sbt verwendet den nächsten Fallback und so weiter:

> MyTask
mysetting for the current project, all configurations and all tasks
[success] Total time: 0 s, completed 17-feb-2017 13:18:38

Konfiguration durch Aufgabe 'initialCommands in Konsole'

Wie wir gesehen haben, können Konfigurationen nach Aufgaben geordnet werden. In sbt wird diese Art der Konfiguration verwendet, wenn die REPL
über die 'Konsole' gestartet wird, z.B.: 'sbt console'. Wir müssen den 'initialCommands' settingKey konfigurieren, der vom Typ 'String' ist und
den Bereich auf den taskKey 'console' setzen:

initialCommands in console :=
"""
import scalaz._
import Scalaz._
import com.github.dnvriend._
val xs = List(1, 2, 3, 4, 5)
"""

Wenn wir die REPL von sbt aus starten, werden die folgenden Ausdrücke ausgewertet.

Tasten

Um alles im Build konfigurieren zu können, von einer Einstellung bis zu einem Task, spielen Schlüssel eine wichtige Rolle, denn mit einem Schlüssel können wir einen Wert an einen Namen binden. Wie wir gesehen haben, ist ein Schlüssel einfach ein Name, der mit den Methoden 'settingKey' und 'taskKey' erstellt werden kann. Anschließend können Sie den neu erstellten Schlüssel verwenden und diesen Schlüssel an einen bestimmten Wert in einer bestimmten Konfiguration und Aufgabe binden. Zum Beispiel:

name := "study-sbt"

name in Test := "study-sbt-in-test"

name in Compile := "study-sbt-in-compile"

name in Compile in compile := "study-sbt-in-compile-for-the-task-compile"

Wir können diese Einstellungen abfragen:

> name
[info] study-sbt
> *:name
[info] study-sbt
> test:name
[info] study-sbt-in-test
> compile:name
[info] study-sbt-in-compile
> compile:compile::name
[info] study-sbt-in-compile-for-the-task-compile

Die letzte Syntax ist neu und muss wie folgt gelesen werden:

Give me the value for the key 'name' in the configuration 'Compile' for the task 'compile'.

Überprüfen der Sbt-Einstellungen

Sie haben eine Menge über Sbt gelernt, was ein Build ist, was Tasks und Einstellungen sind und auch über Scopes. Ich möchte, dass Sie sich jetzt 15 Minuten Zeit nehmen, um die Erklärung zu Inspecting Settings aus der Sbt-Dokumentation zu lesen. Wenn Sie Ihre Einstellungen inspizieren können und verstehen, welche Einstellungen und Aufgaben effektiv verwendet werden, werden Sie mit der Entwicklung von Sbt im Allgemeinen besser zurechtkommen.
Schauen wir uns die Inspektion einer Aufgabe genauer an. Das folgende Beispiel zeigt drei Aufgaben, wobei Aufgabe3 von Aufgabe2 und Aufgabe1 abhängig ist:

lazy val task1 = taskKey[Unit]("task 1")
lazy val task2 = taskKey[Unit]("task 2")
lazy val task3 = taskKey[Unit]("task 3")

task1 := println("Task 1")
task2 := println("Task 2")
task3 := println("Task 3")

task3 := (task3 dependsOn task2 dependsOn task1).value

Wenn wir den Build überprüfen, sehen wir die folgenden Abhängigkeiten:

sbt:study-sbt> inspect task3
[info] Task: Unit
[info] content:
[info]  task 3
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task3
[info] Dependencies:
[info]  *:task2
[info]  *:task1

Wir sehen, dass Aufgabe3 von Aufgabe2 und Aufgabe2 abhängig ist und dies ist auch die Auswertungsreihenfolge. Wenn wir die Abhängigkeit so ändern, dass Aufgabe3 von Aufgabe1 abhängig ist, die wiederum von Aufgabe2 abhängt, sehen wir Folgendes:

task3 := (task3 dependsOn task1 dependsOn task2).value

Nach dem Neuladen können Sie task3 untersuchen:

sbt:study-sbt> inspect task3
[info] Task: Unit
[info] content:
[info]  task 3
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task3
[info] Dependencies:
[info]  *:task1
[info]  *:task2

Wir haben die Reihenfolge der Abhängigkeiten geändert.
Werfen wir nun einen kurzen Blick auf eine vollständige Inspektionsausgabe. Sie enthält das Folgende:

  • Bereitgestellt von: zeigt den tatsächlichen Bereich (den vollständigen Bereich), in dem die Einstellung definiert ist
  • Abhängigkeiten: listet alle Eingaben für eine Aufgabe auf. Die Auflistung ist die Reihenfolge der Auswertung und wenn möglich, wird sbt versuchen, die Abhängigkeiten parallel auszuwerten. Die Einträge zeigen die Konfiguration, Aufgabe und Einstellung, die eine Eingabe für die Aufgabe ist. Bitte beachten Sie, dass es sich bei diesen Einträgen um die definierten Eingaben mit Umfang handelt. Um die 'tatsächlichen' Werte zu sehen, verwenden Sie bitte den Befehl inspect actual <key>,
  • Delegierte: Eine Einstellung hat einen Schlüssel und einen Bereich. Eine Anfrage nach einem Schlüssel in einem Bereich 'A' kann an einen anderen Bereich delegiert werden, wenn 'A' keinen Wert für den Schlüssel definiert. Die Delegationskette ist klar definiert und wird im Abschnitt Delegates des Befehls inspect angezeigt. Der Abschnitt Delegates zeigt die Reihenfolge, in der die Bereiche durchsucht werden, wenn für den angeforderten Schlüssel kein Wert definiert ist,
  • Verwandte: listet alle Definitionen eines Schlüssels auf. Lesen Sie diese Auflistungen also als 'es gibt auch diese Schlüssel in diesen Bereichen, die Sie sich ansehen können',

Umfang Delegation

Diese Funktion ermöglicht es Ihnen, einen Wert einmal in einem allgemeineren Bereich festzulegen, so dass mehrere spezifischere Bereiche diesen Wert erben können.
Lassen Sie uns die Bereiche kurz rekapitulieren:

  • Ein Bereich ist ein Tupel von Komponenten in drei Achsen: (die Teilprojektachse, die Konfigurationsachse und die Aufgabenachse).
  • Es gibt eine spezielle Scope-Komponente * (auch Global genannt) für jede der Scope-Achsen.
  • Es gibt eine spezielle Bereichskomponente ThisBuild (in der Shell als '{.}' geschrieben) nur für die Achse der Unterprojekte.
  • Test erweitert Runtime und Runtime erweitert Compile Konfiguration.
  • Ein Schlüssel, der in build.sbt platziert wird, ist standardmäßig auf (${aktuelles Unterprojekt}, *, *) beschränkt.
  • Ein Schlüssel kann mit der Methode .in(...) weiter eingegrenzt werden.
    Die Regeln für die Delegation des Geltungsbereichs sind:
  • Regel 1: Umfangsachsen haben den folgenden Vorrang: die Teilprojektachse, die Konfigurationsachse und dann die Aufgabenachse.
  • Regel 2: Bei einem Bereich werden die Delegatenbereiche durch Ersetzen der Aufgabenachse in der folgenden Reihenfolge gesucht: der angegebene Aufgabenbereich und dann * (Global), die nicht aufgabenbezogene Version des Bereichs.
  • Regel 3: Wenn ein Bereich gegeben ist, werden die Delegatenbereiche gesucht, indem die Konfigurationsachse in der folgenden Reihenfolge ersetzt wird: die gegebene Konfiguration, ihre Eltern, deren Eltern usw. und dann * (Global, wie bei der nicht abgedeckten Konfigurationsachse).
  • Regel 4: Bei einem Bereich werden die Delegatenbereiche durch Ersetzen der Unterprojektachse in der folgenden Reihenfolge gesucht: das angegebene Unterprojekt, ThisBuild und dann * (Global).

Regel 5: Ein delegierter Schlüssel und seine abhängigen Einstellungen/Aufgaben werden ausgewertet, ohne den ursprünglichen Kontext mitzunehmen.

Benutzerdefinierte Konfigurationen

Wir können auch unsere eigenen Konfigurationen erstellen. Beginnen wir gleich mit der Definition einer Konfiguration namens 'my-config', die
von der Aufgabe 'MyOtherTask' verwendet wird:

lazy val MyConfig = config("my-config")

lazy val myOtherSetting = settingKey[String]("My other setting")

myOtherSetting := "mysetting for the current project, all configurations and all tasks"

myOtherSetting in MyConfig := "mysetting for the current project, for the MyConfig configuration and all tasks"

myOtherSetting in MyConfig in MyOtherTask := "mysetting for the current project, for the MyConfig configuration for the task MyOtherTask only"

lazy val MyOtherTask = taskKey[Unit]("My other task")

MyOtherTask := {
    val str = (myOtherSetting in MyConfig in MyOtherTask).value
    println(str)
}

Wir können unsere benutzerdefinierte Konfiguration wie jede andere verwenden:

> myOtherSetting
[info] mysetting for the current project, all configurations and all tasks
> my-config:myOtherSetting
[info] mysetting for the current project, for the MyConfig configuration and all tasks
> my-config:MyOtherTask::myOtherSetting
[info] mysetting for the current project, for the MyConfig configuration for the task MyOtherTask only

> MyOtherTask
mysetting for the current project, for the MyConfig configuration for the task MyOtherTask only
[success] Total time: 0 s, completed 17-feb-2017 14:02:51

Abhängige Aufgaben

Wir können Aufgaben voneinander abhängig machen. Lassen Sie uns zwei Aufgaben erstellen:

  • 'task1' gibt den String "Hallo" zurück,
  • 'task2' verwendet den Wert von 'task1', ist also von task1 abhängig und ruft daher 'task1' auf, um sein Ergebnis zu erhalten. Sie sehen hier, dass Aufgaben, wie Einstellungen, einen Wert zurückgeben, dies aber bei Bedarf tun und jedes Mal ausgewertet werden, wenn sie aufgerufen werden. Die Aufgabe 'task2' wird die Zeichenkette "Hello World!" zurückgeben, das ist zumindest die Idee! Schauen wir mal, ob es wie vorgesehen funktioniert:
lazy val task1 = taskKey[String]("task 1")

lazy val task2 = taskKey[String]("task 2")

task1 := {
    println("Evaluating task1")
    "Hello"
}

task2 := {
  println("Evaluating task2")
  s"${task1.value} World!"
}

Lassen Sie es uns ausprobieren:

> show task1
Evaluating task1
[info] Hello
[success] Total time: 0 s, completed 17-feb-2017 14:05:38

> show task2
Evaluating task1
Evaluating task2
[info] Hello World!
[success] Total time: 0 s, completed 17-feb-2017 14:05:40

Es funktioniert! Sehen Sie, sbt ist gar nicht so schwierig!

Aufgaben für eine bestimmte Konfiguration

Wie eine Einstellung kann auch eine Aufgabe einen anderen Wert für eine andere Konfiguration haben:

lazy val task1 = taskKey[String]("task 1")

lazy val task2 = taskKey[String]("task 2")

task1 := {
    println("Evaluating task1 for current project for all configurations")
    "Hello all config"
}

task1 in Test := {
    println("Evaluating task1 for current project for Test config")
    "Hello test config"
}

task1 in Compile := {
    println("Evaluating task1 for current project for Compile config")
    "Hello compile config"
}

task2 := {
  println("Evaluating task2 for current project for all configurations")
  val task1Value = (task1 in Test).value
  s"$task1Value World!"
}

Lassen Sie es uns ausprobieren:

> show task1
Evaluating task1 for current project for all configurations
[info] Hello all config
[success] Total time: 0 s, completed 18-feb-2017 12:52:08

> show compile:task1
Evaluating task1 for current project for Compile config
[info] Hello compile config
[success] Total time: 0 s, completed 18-feb-2017 12:52:13

> show test:task1
Evaluating task1 for current project for Test config
[info] Hello test config
[success] Total time: 0 s, completed 18-feb-2017 12:52:16

> show task2
Evaluating task1 for current project for Test config
Evaluating task2 for current project for all configurations
[info] Hello test config World!
[success] Total time: 0 s, completed 18-feb-2017 12:52:22

Abhängigkeiten von Aufgaben

In den Beispielen haben wir Abhängigkeiten zwischen zwei Aufgaben, task1 und task2, geschaffen. In den Beispielen würde task2 task1 nach seinem Wert fragen. In der Tat wird die Abhängigkeit in der Implementierung der Aufgabe wie folgt erstellt:

lazy val task1 = taskKey[String]("task 1")

lazy val task2 = taskKey[String]("task 2")

task1 := {
    println("Evaluating task1")
    "Hello"
}

task2 := {
  println("Evaluating task2")
  s"${task1.value} World!"
}

Im obigen Beispiel benötigen wir den ausgewerteten Wert von task1, um eine eigene Berechnung durchzuführen. Aber was ist, wenn wir nur Aufgaben haben, die einige Nebeneffekte haben und alle Unit zurückgeben. Was, wenn wir eine Sequenz zwischen ihnen erstellen müssen, wie machen wir das?
Wir haben zum Beispiel die folgenden drei Aufgaben:

lazy val task1 = taskKey[Unit]("task 1")

lazy val task2 = taskKey[Unit]("task 2")

lazy val task3 = taskKey[Unit]("task 3")

task1 := println("Task 1")

task2 := println("Task 2")

task3 := println("Task 3")

Lassen Sie sie uns ausprobieren:

task1> task1
Task 1
[success] Total time: 0 s, completed 18-feb-2017 13:24:22
> task2
Task 2
[success] Total time: 0 s, completed 18-feb-2017 13:24:23
> task3
Task 3
[success] Total time: 0 s, completed 18-feb-2017 13:24:25

Sagen wir, wenn wir task3 eingeben, sollte folgendes passieren:

  • zuerst sollte Aufgabe1 ausgeführt werden,
  • dann Aufgabe2
  • dann Aufgabe3

Wie können wir das tun? Lassen Sie es uns herausfinden.

Abhängigkeitsschlüssel Operator

Wie Sie vielleicht wissen, wird Sbt vereinfacht, was bedeutet, dass viele 'exotische Operatoren' wegfallen und nur noch einige wenige Operatoren verwendet werden, die im Kontext eines bestimmten Anwendungsfalls eingesetzt werden können. Einige dieser Operatoren kennen Sie bereits, z.B. ':=', '+=', '++=' und so weiter.
Hinweis:

  • Für Benutzer von SBT < v1.0: Aus einem technischen Grund #1444 müssen wir immer noch den '"=' Operator verwenden, der der 'Dependency Key' Operator für einige Abhängigkeiten ist. Kein Problem, wenn Sie wissen, was das ist und was es bewirkt.
  • Für SBT >= 1.0 Benutzer: Hinweis: Bitte ersetzen Sie '"=' durch ':=' als Abhängigkeitsoperator.
    Mit Sbt können wir die folgenden Abhängigkeiten zwischen Aufgaben definieren:
  • dependsOn: eine Aufgabe hängt von einer anderen Aufgabe ab,
  • triggeredBy: eine Aufgabe wird durch eine andere Aufgabe ausgelöst,
  • runBefore: eine Aufgabe wird vor einer anderen Aufgabe ausgeführt

Wir haben zum Beispiel die drei zuvor definierten Aufgaben und auch eine Abhängigkeit zwischen ihnen definiert:

lazy val task1 = taskKey[Unit]("task 1")

lazy val task2 = taskKey[Unit]("task 2")

lazy val task3 = taskKey[Unit]("task 3")

task1 := println("Task 1")

task2 := println("Task 2")

task3 := println("Task 3")

task3 := (task3 dependsOn task2 dependsOn task1).value

Wenn wir task3 ausführen, der neben der Implementierung auch eine Abhängigkeitsregel definiert hat, wie wir oben sehen können, geschieht Folgendes:

> task3
Task 1
Task 2
Task 3
[success] Total time: 0 s, completed 18-feb-2017 13:24:55

Die Regel task3 := (task3 dependsOn task2 dependsOn task1).value ist die neue Syntax und wird von neueren Versionen von sbt unterstützt. Die folgende Syntax funktioniert ebenfalls, ist aber veraltet:
Hinweis: Wenn Sie SBT 1.0 oder höher verwenden, ersetzen Sie bitte '"=' durch ':='.

// define a dependency rule using the '<<=' syntax which is deprecated
task3 <<= task3 dependsOn task2 dependsOn task1

Nehmen wir an, wir möchten Folgendes definieren: Ich möchte, dass task1 und dann task3 ausgeführt werden, wenn ich task1 eingebe. Wie können wir das tun?

lazy val task1 = taskKey[Unit]("task 1")

lazy val task2 = taskKey[Unit]("task 2")

lazy val task3 = taskKey[Unit]("task 3")

task1 := println("Task 1")

task2 := println("Task 2")

task3 := println("Task 3")

// when I type 'task1': task1 -> task3, because task3 is triggeredBy task1
task3 := (task3 triggeredBy task1).value

Lassen Sie es uns ausprobieren:

> task1
Task 1
Task 3
[success] Total time: 0 s, completed 18-feb-2017 13:42:53
> task2
Task 2
[success] Total time: 0 s, completed 18-feb-2017 13:42:55
> task3
Task 3
[success] Total time: 0 s, completed 18-feb-2017 13:42:56

Hinweis: Ersetzen Sie ab SBT v1.0 '"=' durch ':='.
Was hier passiert ist, ist, dass die Regel task3 <<= task3 triggeredBy task1, die den veralteten '"=' 'Abhängigkeitsschlüssel'-Operator verwendet und die wir aus technischen Gründen verwenden müssen, zur Folge hat, dass, wenn wir 'task1' eingeben, zuerst 'task1' ausgeführt wird, und weil 'task1' ausgeführt wird, wird 'task3' ausgelöst, was dazu führt, dass auch task3 ausgeführt wird.
Nehmen wir an, ich möchte Folgendes definieren: Wenn ich 'task3' eintippe, soll zuerst task1 und dann task3 ausgeführt werden, also muss task1 vor task3 ausgeführt werden. Wie kann ich das tun?

lazy val task1 = taskKey[Unit]("task 1")

lazy val task2 = taskKey[Unit]("task 2")

lazy val task3 = taskKey[Unit]("task 3")

task1 := println("Task 1")

task2 := println("Task 2")

task3 := println("Task 3")

task1 := (task1 runBefore task3).value

Lassen Sie es uns ausprobieren:

> task1
Task 1
[success] Total time: 0 s, completed 18-feb-2017 13:45:44
> task2
Task 2
[success] Total time: 0 s, completed 18-feb-2017 13:45:45
> task3
Task 1
Task 3
[success] Total time: 0 s, completed 18-feb-2017 13:45:47

Aufgaben-Grafiken

Die Sbt-Dokumentation hat eine sehr gute Dokumentation über den Task-Graphen, aber hier ist die kurze Erklärung.
Die Methode .value wird verwendet, um eine Abhängigkeit von einer anderen Aufgabe oder Einstellung auszudrücken. Die Methode .value ist speziell und darf nur im Argument von :=, += oder ++= aufgerufen werden.
Die Methode .value ist keine normale Scala-Methode und es ist wichtig, dies zu verstehen. Sbt sucht nach diesen .value Methoden und hebt diese Operationen außerhalb des Aufgabenkörpers und werden alle zuerst ausgewertet. Für Entwickler kann die Arbeit mit den .value Methoden innerhalb von Methoden zu unerwartetem Verhalten führen, denn unabhängig davon, wo der .value Aufruf in einer Methode oder sogar innerhalb eines if-then-when Ausdrucks platziert ist, werden alle .value Methoden außerhalb des Aufgabenkörpers angehoben und zuerst ausgewertet.
Außerdem ist die Auswertungsreihenfolge all dieser .value Methoden nicht determiniert. Wie kann man also am besten mit der Methode .value arbeiten, um Aufgaben zu schreiben? Es gibt einige Möglichkeiten:

  1. Inlinern .value
  2. Kein Inlining .value

Einfügung von '.value'

Sehen wir uns die erste Strategie 'Inlining .value' an. Wir können die Methode .value einbinden, indem wir alle Aufrufe von .value an den Anfang der Aufgabe stellen, wie in den folgenden Beispielen. Der Effekt ist, dass der Entwickler keinen Fehler machen kann, indem er die Aufrufe von .value innerhalb eines if-then-else-Aufrufs verwendet. Außerdem sind alle Aufrufe von .value am Anfang der Aufgabe gruppiert, was die Trennung deutlich macht. Lassen Sie uns diese Art von Aufgaben aufbauen:

lazy val task1 = taskKey[Unit]("")

task1 := {
  // put all .value calls here at the top

  // put your task logic down below here
  println("Hello")
}

Der vorige Code erstellt eine Aufgabe task1, die keine Abhängigkeiten hat, lassen Sie uns einen Blick darauf werfen:

sbt:study-sbt> inspect task1
[info] Task: Unit
[info] content:
[info]
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task1
[info] Defined at:
[info]  /Users/dennis/projects/study-sbt/build.sbt:7
[info] Delegates:
[info]  *:task1
[info]  {.}/*:task1
[info]  */*:task1

Machen wir task1 abhängig von clean:

lazy val task1 = taskKey[Unit]("")

task1 := {
  // put all .value calls here at the top
  clean.value

  // put your task logic down below here
  println("Hello")
}

Wir sehen jetzt eine Abhängigkeit von der Aufgabe clean:

sbt:study-sbt> inspect task1
[info] Task: Unit
[info] content:
[info]
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task1
[info] Defined at:
[info]  /Users/dennis/projects/study-sbt/build.sbt:7
[info] Dependencies:
[info]  *:clean
[info] Delegates:
[info]  *:task1
[info]  {.}/*:task1
[info]  */*:task1

Fügen wir eine weitere Abhängigkeit hinzu, zum Beispiel zu update:

lazy val task1 = taskKey[Unit]("")

task1 := {
  // put all .value calls here at the top
  clean.value
  val updateReport: UpdateReport = update.value

  // put your task logic down below here
  println(updateReport.allConfigurations)
}

Jetzt sehen wir eine zweite Abhängigkeit, die ebenfalls von der Aktualisierungsaufgabe abhängt:

sbt:study-sbt> inspect task1
[info] Task: Unit
[info] content:
[info]
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task1
[info] Defined at:
[info]  /Users/dennis/projects/study-sbt/build.sbt:7
[info] Dependencies:
[info]  *:update
[info]  *:clean
[info] Delegates:
[info]  *:task1
[info]  {.}/*:task1
[info]  */*:task1

Durch die Aufteilung des Codes in 'alle Aufrufe von .value' und 'Aufgabenlogik' muss sich der Entwickler nicht um die Auswertungsreihenfolge aller Werte kümmern und die Sbt-Makro-Logik und die Aufgabenlogik vermischen.

Nicht-Einfügung von '.value'

Sehen wir uns die zweite Strategie an, das Nicht-Inlinen von .value Methoden. Hier werden wir eine klare Trennung zwischen der Aufgabenlogik und der Aufgabe selbst vornehmen. Wir packen die gesamte Logik in ein Modul, das von der Aufgabe aufgerufen wird. Die Abhängigkeiten von den Werten werden Teil des Build-Skripts in Form einer kurzen Implementierung sein, die nur .value aufruft. Werfen wir einen Blick auf diese Strategie:

lazy val task1 = taskKey[Unit]("")
task1 := task1Impl(update.value, streams.value.log, name.value, scalaVersion.value)

def task1Impl(updateReport: UpdateReport, log: Logger, projectName: String, scalaVersion: String): Unit = {
  log.info(
    s"""
      |ProjectName: $projectName
      |ScalaVersion: $scalaVersion
      |Report: ${updateReport.stats}
    """.stripMargin)
}

Wir haben die Aufgabe in eine Methode aufgeteilt, die alle 'materialisierten' Werte aus der Sbt-Umgebung enthält. Der Entwickler muss sich nun nicht mehr um die Makro-Details von Sbt kümmern. Die Sequenzauswertung funktioniert wie erwartet innerhalb der Methode task1Impl. Die Aufgabenimplementierung, d.h. die Zeile 'task1 := task1Impl(update.value, streams.value.log, name.value, scalaVersion.value)', ist dafür verantwortlich, die Abhängigkeit von allen anderen Aufgaben und Einstellungen herzustellen und alle Werte auszuwerten. Wenn alle Werte ausgewertet sind, wird die Methode task1Impl aufgerufen.
Sehen wir uns die Abhängigkeiten an:

sbt:study-sbt> inspect task1
[info] Task: Unit
[info] content:
[info]
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task1
[info] Defined at:
[info]  /Users/dennis/projects/study-sbt/build.sbt:7
[info] Dependencies:
[info]  *:scalaVersion
[info]  *:name
[info]  *:task1::streams
[info]  *:update
[info] Delegates:
[info]  *:task1
[info]  {.}/*:task1
[info]  */*:task1

Wie erwartet haben wir Abhängigkeiten mit allen Aufgaben und Einstellungen, die wir .value aufgerufen haben.
Eine zweite Strategie ist die Verwendung eines Scala-Objekts, das als Container für die Methode verwendet wird. Der einzige Nachteil dieser Strategie ist, dass das Objekt in das Verzeichnis project Ihres Projekts gelegt werden muss. Legen Sie es einfach neben Ihre Datei build.properties. Lassen Sie uns die Datei project/Task1Module.scala mit dem folgenden Inhalt erstellen:

import sbt.{Logger, UpdateReport}

object Task1Module {
  def task1Impl(updateReport: UpdateReport, log: Logger, projectName: String, scalaVersion: String): String = {
    s"""
       |ProjectName: $projectName
       |ScalaVersion: $scalaVersion
       |Report: ${updateReport.stats}
    """.stripMargin
  }
}

Diese Implementierung gibt einen String zurück. Im vorherigen Beispiel gab die Methode nichts zurück. Sehen wir uns den Inhalt von build.sbt an:

val task1 = taskKey[String]("")
task1 := Task1Module.task1Impl(update.value, streams.value.log, name.value, scalaVersion.value)

Bitte beachten Sie, dass ich task1 ein wenig geändert habe. Es erwartet jetzt einen String zurück, den wir mit dem Befehl show task1 auf der Sbt-Konsole anzeigen können:

sbt:study-sbt> show task1
[info]
[info] ProjectName: study-sbt
[info] ScalaVersion: 2.12.4
[info] Report: Resolve time: 64 ms, Download time: 4 ms, Download size: 0 bytes
[info]

Lassen Sie uns task1 untersuchen:

sbt:study-sbt> inspect task1
[info] Task: java.lang.String
[info] content:
[info]
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task1
[info] Defined at:
[info]  /Users/dennis/projects/study-sbt/build.sbt:3
[info] Dependencies:
[info]  *:scalaVersion
[info]  *:name
[info]  *:task1::streams
[info]  *:update
[info] Delegates:
[info]  *:task1
[info]  {.}/*:task1
[info]  */*:task1

Wie erwartet, ist die Aufgabe nach wie vor von anderen Einstellungen und Aufgaben abhängig.

Parallele und sequentielle Ausführung von Aufgaben

Sbt versucht standardmäßig, Aufgaben parallel auszuführen. Die meisten Aufgaben können parallel ausgewertet werden, wie zum Beispiel das folgende Beispiel:

lazy val task1 = taskKey[String]("t1")
lazy val task2 = taskKey[String]("t2")
lazy val task3 = taskKey[String]("t3")
lazy val runAll = taskKey[String]("all parallel (the default behavior)")

task1 := {
  Thread.sleep(1000)
  println("t1")
  "task1"
}

task2 := {
  Thread.sleep(750)
  println("t2")
  "task2"
}

task3 := {
  Thread.sleep(850)
  println("t3")
  "task3"
}

runAll := {
  val t1 = task1.value
  val t2 = task2.value
  val t3 = task3.value
  val all = s"$t1 - $t2 - $t3"
  println(all)
  all
}

Wenn die Aufgabe runAll ausgewertet wird, wertet Sbt die Aufgaben parallel aus und der Wert wird in den Variablen gespeichert. Wenn alle Aufgaben ausgewertet wurden,
wird der String ausgewertet und in all gespeichert und kann schließlich auf der Konsole ausgegeben werden.
Das Standardverhalten parallel ist eine Funktion von Sbt und kann nicht einfach deaktiviert werden. Selbstverständlich bleiben alle Abhängigkeiten der Aufgaben von anderen Aufgaben erhalten, wenn
die Aufgaben ausgeführt werden.

Sequentielle Ausführung von Aufgaben

Wenn es notwendig ist, Aufgaben sequentiell auszuwerten, z.B. bei der Orchestrierung eines Einsatzes oder wenn Sie die sequentielle Ausführung von Aufgaben erzwingen möchten, bietet Sbt ab Version 0.13 mit der Aufgabe Def.sequential Unterstützung dafür:

lazy val runAllSequential = taskKey[String]("all sequential (forced by use of Def.sequential().value")

runAllSequential := Def.sequential(task1, task2, task3).value

Wenn die Aufgabe runAllSequential ausgewertet wird, werden die Aufgaben sequentiell ausgeführt. Diese Operation wird mit Scala macros erstellt, d.h.
können Sie nur Def.sequential so verwenden, wie wir es oben getan haben. Sie können sie nicht innerhalb einer anderen Def.task oder Def.taskDyn usw. verwenden.

Rückgabe einer Aufgabe basierend auf einer Einstellung

Eine Aufgabe kann eine andere Aufgabe basierend auf dem Wert einer Einstellung zurückgeben:

lazy val choice = settingKey[String]("The task to execute")
choice := "t1"

lazy val staticChoice = taskKey[Unit]("")
staticChoice := Def.taskDyn {
  choice.value match {
    case "t1" => task1.toTask
    case "t2" => task2.toTask
    case "t3" => task3.toTask
    case "all" => runAll.toTask
    case _ => runAllSequential.toTask
  }
}.value

Basierend auf dem Wert von choice gibt die Funktion Def.taskDyn eine Aufgabe zurück, die ausgewertet werden soll. Bitte beachten Sie, dass der Def.sequential Task
über seinen Schlüsselnamen referenziert wird, der in diesem Beispiel runAllSequential lautet. Sie können Def.sequentialnicht inline verwenden.
Bitte beachten Sie den Aufruf .value am Ende von Def.taskDyn, er kann leicht vergessen werden.

Rückgabe einer Aufgabe basierend auf einer Benutzereingabe

Eine Aufgabe kann je nach Benutzereingabe eine andere Aufgabe zurückgeben:

lazy val inputChoice = inputKey[Unit]("")
inputChoice := Def.inputTaskDyn {
  Def.spaceDelimited("choice").parsed.head match {
    case "t1" => task1.toTask
    case "t2" => task2.toTask
    case "t3" => task3.toTask
    case "all" => runAll.toTask
    case "seq" => runAllSequential.toTask
    case unknown => Def.task {
      streams.value.log.info(s"(inputChoice): Unknown task: '$unknown'")
      unknown
    }
  }
}.evaluated

Auf der Grundlage der Benutzereingabe gibt die Funktion Def.inputTaskDyn eine Aufgabe zurück, die ausgewertet wird. Bitte beachten Sie, dass die Def.sequential Aufgabe
über ihren Schlüsselnamen referenziert wird, der in diesem Beispiel runAllSequential lautet. Sie können Def.sequentialnicht inline verwenden.
Bitte beachten Sie den Aufruf .evaluated am Ende von Def.inputTaskDyn, er kann leicht vergessen werden.

Rückgabe von Klassen aus dem classDirectory

Die kompilierten Klassen sind in der Einstellung classDirectory in Compile verfügbar. Der folgende Code kann helfen, eine Liste der kompilierten Klassen als String und auch
als Seq[Class[_]] zu erhalten:

lazy val allClassesInClassDirectory = taskKey[Seq[(String, String)]]("Returns all classes in the classDirectory")
allClassesInClassDirectory := {
  import scala.tools.nsc.classpath._
  val baseDir: File = (classDirectory in Compile).value
  val allClassFilesInClassDir: Seq[File] = (baseDir ** "*.class").get
  val relativizer = IO.relativize(baseDir, _: File)
  allClassFilesInClassDir
    .flatMap(relativizer(_).toSeq)
    .map(FileUtils.stripClassExtension)
    .map(_.replace("/", "."))
    .map(PackageNameUtils.separatePkgAndClassNames)
}

lazy val allObjectsInClassDirectory = taskKey[Seq[(String, String)]]("Returns all objects in the classDirectory")
allObjectsInClassDirectory := {
  allClassesInClassDirectory.value.filterNot {
    case (_, className) => className.endsWith("$")
  }
}

lazy val onlyClassesInClassDirectory = taskKey[Seq[(String, String)]]("Returns only classes in the classDirectory")
onlyClassesInClassDirectory := {
  allClassesInClassDirectory.value.filterNot {
    case (_, className) => className.contains("$")
  }
}

lazy val allClassesInClassDirectoryAsClass = taskKey[Seq[Class[_]]]("Returns all classes in the classDirectory as Class[_]")
allClassesInClassDirectoryAsClass := {
  val cp: Seq[File] = (fullClasspath in Compile).value.map(_.data)
  val cl = sbt.internal.inc.classpath.ClasspathUtilities.makeLoader(Seq((classDirectory in Compile).value) ++ cp, scalaInstance.value)
  allClassesInClassDirectory.value.map {
    case (packageName, className) => cl.loadClass(s"$packageName.$className")
  }
}

lazy val onlyClassesInClassDirectoryAsClass = taskKey[Seq[Class[_]]]("Returns only classes in the classDirectory as Class[_]")
onlyClassesInClassDirectoryAsClass := {
  val cl = sbt.internal.inc.classpath.ClasspathUtilities.makeLoader(Seq((classDirectory in Compile).value), scalaInstance.value)
  onlyClassesInClassDirectory.value.map {
    case (packageName, className) => cl.loadClass(s"$packageName.$className")
  }
}

Annotierte Klassen aus dem Klassenpfad holen

In Scala gibt es verschiedene Arten von Anmerkungen:

  • Einfache Anmerkungen, die sich nur im Code befinden: Auf diese kann von Makros in der Kompilierungseinheit zugegriffen werden, in der das Makro aufgerufen wird, wobei das Makro Zugriff auf den AST erhält.
  • StaticAnnotations, die über Kompilierungseinheiten hinweg gemeinsam genutzt werden: auf diese kann über die scala reflection api zugegriffen werden
  • ClassfileAnnotations: Diese stellen Anmerkungen dar, die als Java-Annotationen gespeichert sind. Wenn Sie über die Java Reflection API auf sie zugreifen möchten, müssen Sie sie allerdings in Java definieren.

Eine gute Lektüre zu diesem Thema ist: Wie ist der (aktuelle) Stand der Reflection-Fähigkeiten von Scala, insbesondere in Bezug auf Annotationen, in Version 2.11?
Java-Annotationen sind eine Form von Metadaten, die zusätzliche Informationen über ein Programm liefern, aber nicht Teil des Programms selbst sind. Annotationen haben keine direkte Auswirkung auf den Code, den sie annotieren.
Annotationen haben eine Reihe von Verwendungsmöglichkeiten, unter anderem:

  • Korrekturen der Kodierung:
  • Informationen für den Compiler: Anmerkungen können vom Compiler verwendet werden, um Fehler zu erkennen oder Warnungen zu unterdrücken,
  • Zeigen Sie Warnungen vor Fehlentwicklungen an,
  • Validierung der Korrektur von Kodierungen wie der '@tailrec'-Anmerkung zur Überprüfung auf tail-recursive Methoden,
  • Verarbeitung zur Kompilierzeit und zur Bereitstellungszeit: Software-Tools können Informationen über Anmerkungen verarbeiten, um Code, Property-Dateien usw. zu generieren,
  • Verarbeitung zur Laufzeit: Einige Anmerkungen können zur Laufzeit überprüft werden.

Wie in Eine Tour durch Scala beschrieben : Annotations beschrieben, assoziieren Annotationen Metainformationen mit Definitionen. Das Erstellen einer Annotation ist ganz einfach. Lassen Sie uns zum Beispiel unsere eigene Java Annotation erstellen:

package main;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MyMarker {
    String name() default "N/A";
    String author() default "Dennis Was Here";
}

Anmerkungen können nützlich sein, aber standardmäßig führt die JVM einige speicherschonende Optimierungen durch, was bedeutet, dass eine Anmerkung zur Laufzeit standardmäßig nicht verfügbar ist. Das bedeutet, dass wir
haben, um die Annotation mit einer Annotation zu 'annotieren', die dem Compiler mitteilt, dass die Annotation für die Verwendung zur Laufzeit aufbewahrt werden muss, was wir ja wollen.
Die standardmäßige Aufbewahrungsrichtlinie lautet RetentionPolicy.CLASS, was bedeutet, dass die Annotationsinformationen standardmäßig nicht zur Laufzeit aufbewahrt werden:

Anmerkungen werden vom Compiler in der Klassendatei aufgezeichnet, müssen aber von der VM zur Laufzeit nicht beibehalten werden. Dies ist das Standardverhalten.

Verwenden Sie stattdessen RetentionPolicy.RUNTIME:

Die Anmerkungen werden vom Compiler in der Klassendatei aufgezeichnet und von der VM zur Laufzeit beibehalten, so dass sie reflektierend gelesen werden können.

Wir werden die Methode getClass[AnnotatedClass].getAnnotations verwenden, die nur Java-Annotationen zurückgibt. Um herauszufinden, welche Klassen mit der Annotation MyMarker versehen wurden, können wir Folgendes tun:

lazy val findMarked = taskKey[Seq[Class[_]]]("Returns the classes that have been annotated with the 'MyMarker' annotation")
findMarked := {
  allClassesInClassDirectoryAsClass.value
      .filter(_.getDeclaredAnnotations.toList.exists(_.annotationType().getName.contains("MyMarker")))
}

Als Randbemerkung: class.getDeclaredAnnotations und class.getAnnotations ist, dass getDeclaredAnnotations geerbte Anmerkungen ignoriert, also nur Anmerkungen zurückgibt, die in der Klasse selbst deklariert sind, während die Methode getAnnotations alle Anmerkungen zurückgibt, auch geerbte.
Wir können die Anmerkung auch so analysieren:

lazy val parseMarked = taskKey[Seq[(Class[_], String)]]("Returns the classes that have been annotated with the 'MyMarker' annotation with the JSON representation of the fields of the annotation")
parseMarked := {
  def getMarkedAnnotationValues(cl: Class[_]): Option[(Class[_], String)] = {
    cl.getDeclaredAnnotations.toList.find(_.annotationType().getName.contains("MyMarker")).map { anno =>
      val name = anno.annotationType().getMethod("name").invoke(anno)
      val author = anno.annotationType().getMethod("author").invoke(anno)
      s"""{"name":"$name","author":"$author"}"""
    }.map(json => (cl, json))
  }
  findMarked.value.flatMap(getMarkedAnnotationValues)
}

Ungebundene Einstellungen und Aufgaben

Die Schlüssel von Einstellungen und Aufgaben müssen nicht gebunden sein, d.h. sie müssen keine Implementierung haben. Sbt verfügt über Operatoren, die feststellen können, ob
die Schlüssel gebunden sind oder nicht, und wenn nicht, können wir alternative Schlüssel oder Implementierungen auswählen, die wir verwenden möchten:

lazy val s1 = settingKey[String]("s1")
// there is no implementation of s1
// so the key 's1' is "not bound"
//s1 := "foo"

lazy val s2 = settingKey[String]("s2")
s2 := "bar"

lazy val t1 = taskKey[Unit]("")
t1 := {
  // s1 is not bound, so maybeS1 is None
  val maybeS1: Option[String] = s1.?.value
  assert(maybeS1.isEmpty)

  // s1 is not bound, if so, use 'quz' as string
  val alternativeValue: String = s1.??("quz").value
  assert(alternativeValue == "quz")

  // if s1 is not bound, use the value of setting 's2'
  val effectiveSetting: String = s1.or(s2).value
  assert(effectiveSetting == "bar")
}

Dynamische Änderung der Einstellung

Eine Einstellung kann einen anderen Wert zurückgeben, basierend auf einem Ergebnis, z.B. einem Wert oder einer Einstellung, oder sie könnte eine andere Aufgabe aufrufen und basierend auf einem
beobachteten Effekt (eine vorhandene Datei oder etwas anderes) den Wert der Einstellung ändern:

lazy val s2 = settingKey[String]("s2")
s2 := "bar"

lazy val s3 = settingKey[String]("s2")
s3 := Def.settingDyn {
  s2.value match {
    case "bar" => Def.setting("foo")
    case _ => Def.setting("bar")
  }
}.value

lazy val t1 = taskKey[Unit]("")
t1 := {
  println("effective: " + s3.value)
}

Bitte beachten Sie den Aufruf .value am Ende von Def.settingDyn, das kann man leicht vergessen. Beachten Sie auch, dass wir hier Def.setting verwenden. Bis jetzt haben wir nur
'Def.task' verwendet, aber das funktioniert hier nicht.

Geltungsbereiche

Schlüssel -> Wertepaare spielen in Sbt eine wichtige Rolle, da sie uns ermöglichen, Einstellungen zu definieren und Einstellungen ermöglichen uns, unsere Projekte zu konfigurieren und ein Build besteht aus einem oder mehreren Projekten. Schlüssel können ganz einfach so konfiguriert werden, dass sie einen Wert in einer bestimmten Konfiguration, Task oder (Konfiguration, Task) Kombination haben.
Sbt gibt uns Kürzel an die Hand, mit denen wir Schlüssel ganz einfach zuordnen können. Es gibt zwei Scopes 'Global' und 'ThisBuild'.

ThisBuild Umfang

Lassen Sie uns zunächst über 'ThisBuild' sprechen. Wenn ein Build aus mehreren Projekten besteht, dann ist der Scope 'ThisBuild' praktisch. Für ein einzelnes Projekt 'build.sbt' macht der Scope 'ThisBuild' nicht viel Sinn, da die Konfiguration für das einzelne (Standard-)Projekt gilt.
Nehmen wir an, wir haben ein Multiprojekt build.sbt wie folgt:

lazy val project1 = project in file("project1")

lazy val project2 = project in file("project2")

Wenn wir die Einstellungen für ein bestimmtes Projekt abfragen möchten, geben wir Folgendes in die sbt-Konsole ein:

project1/name
[info] project1

> project1/scalaVersion
[info] 2.12.4

Bis jetzt haben wir diese Syntax noch nicht gesehen. Der Projektname ist auch Teil eines Schlüssels. Der vollständig qualifizierte Name eines Schlüssels ist also wirklich:

(project/config:task::setting)

Natürlich können wir Teile davon auch so lassen:

> project1/scalaVersion
[info] 2.12.4
> project1/test:scalaVersion
[info] 2.12.4
> project1/test:test::scalaVersion
[info] 2.12.4

Angenommen, wir möchten die scalaVersion nur für 'project1' konfigurieren, dann würden wir Folgendes eingeben:

set scalaVersion in project1 := "2.11.8"
[info] Reapplying settings...
> project1/scalaVersion
[info] 2.11.8

> project2/scalaVersion
[info] 2.12.4

Wir könnten auch die scalaVersion für project1 in der build.sbt wie folgt festlegen:

lazy val project1 = (project in file("project1")).settings(scalaVersion := "2.12.1")

lazy val project2 = project in file("project2")

Dann fragen Sie nach:

> project1/scalaVersion
[info] 2.12.1

Angenommen, wir möchten die scalaVersion für alle Projekte in unserem Build festlegen, dann würden wir konfigurieren:

> set scalaVersion in ThisBuild := "2.11.8"
[info] Reapplying settings...

> project1/scalaVersion
[info] 2.12.1

> project2/scalaVersion
[info] 2.11.8

Da wir die spezifische Konfiguration, die wir für 'project1' festgelegt haben, nicht entfernt haben, ist die scalaVersion immer noch '2.12.1', aber die Einstellung für 'project2' hat sich geändert. Der Bereich 'ThisBuild' ist eine Kurzform für die folgende Definition:

All projects and all configuration and all tasks in the current build only.

Globale Reichweite

Der Bereich 'Global' ist praktisch, um Einstellungen zu definieren, die für alle Projekte überall auf Ihrem Computer oder in Ihrem Unternehmen gelten, und zwar für alle Konfigurationen und Aufgaben. Ich denke, damit ist 'Global' abgedeckt. Dieser Bereich ist nur dann sinnvoll, wenn Sie Plugins erstellen und die Einstellung für alle Projekte überall hinzufügen möchten. Wenn Sie sich daran erinnern, dass Schlüssel auf Achsen skaliert werden, also (Projekt/Konfiguration:Aufgabe), dann besteht der Unterschied zwischen dem Bereich 'ThisBuild' und 'Global' darin, dass für 'ThisBuild' die Achse wie ({.}/:) und für Global die Achse wie(/:*) aussieht.

Benutzereingaben erhalten

Es gibt viele Möglichkeiten, die Benutzereingaben zu erhalten, z.B. mit der Parser Combinator Libary von sbt, aber der folgende Weg ist auch sehr einfach und wurde
dem AWS Lambda Plugin
Beispiel entnommen:

lazy val t1 = taskKey[Unit]("")
t1 := {
  val name = readInput("What is your name?")
  val age = readInput("What is your age?")
  streams.value.log.info(s"Hello '$name', you are '$age' years old!")
}

def readInput(prompt: String): String = {
  SimpleReader.readLine(s"$promptn") getOrElse {
    val badInputMessage = "Unable to read input"
    val updatedPrompt = if (prompt.startsWith(badInputMessage)) prompt else s"$badInputMessagen$prompt"
    readInput(updatedPrompt)
  }
}

Konsolenausgabe:

sbt:study-sbt> t1
What is your name?
Dennis
What is your age?
42
[info] Hello 'Dennis', you are '42' years old!

Parsing von Benutzereingaben

SBT unterstützt das Parsen von Benutzereingaben als Teil einer Aufgabe. Um Benutzereingaben zu parsen, verwenden wir die inputKey z.B:

import sbt.complete.DefaultParsers._

lazy val hello = inputKey[String]("Hello World")

hello := {
  val name: String = (Space ~> StringBasic).parsed
  val greeting = s"Hello $name"
  streams.value.log.info(greeting)
  greeting
}

Sbt verwendet die Bibliothek sbt-parser-combinator und das Parsen von Benutzereingaben erfolgt mit einer Kombination aus Parsern, die in der Standardbibliothek
definiert sind, und Ihren eigenen benutzerdefinierten Parsern. Ich habe ein Studienprojekt, das zeigt, wie Sie die sbt-Parser-Bibliothek verwenden und Ihre eigenen Parser erstellen können.
Es ist möglich, die inputTask durch eine andere Aufgabe wiederzuverwenden, dazu könnten Sie folgendes tun:

lazy val useHello = taskKey[String]("Using hello")

useHello := {
  val result = hello.toTask(" Dennis").value
  val msg = s"useHello: '$result'"
  streams.value.log(msg)
  msg
}

Wir müssen nicht unbedingt ein Leerzeichen verwenden, da unser Parser angibt, dass die Benutzereingabe mit einem Leerzeichen beginnen sollte, wie in:

val name: String = (Space ~> StringBasic).parsed

Alternativ können wir Def.spaceDelimited verwenden, um die Benutzereingabe zu parsen. Dies ist ein Standard-Parser für die Aufteilung der Eingabe in durch Leerzeichen getrennte Argumente:

lazy val hello = inputKey[String]("Hello World")

hello := {
  val names: Seq[String] = Def.spaceDelimited("Type names").parsed
  val namesString: String = names.mkString(",")
  val greeting = s"Hello $namesString"
  streams.value.log.info(greeting)
  greeting
}

lazy val useHello = taskKey[Unit]("Using hello")

useHello := {
  val result = hello.toTask(" a b c d").value
  assert(result == "Hello a,b,c,d")
}

Parsen der Eingabe mit Parsern

Wenn wir einen sbt.internal.util.complete.Parser[T] betrachten, handelt es sich im Grunde um eine Funktion String => Option[T], die einen String zum Parsen akzeptiert und einen in Some verpackten Wert erzeugt, wenn das Parsen erfolgreich war oder None, wenn es fehlgeschlagen ist.
Sbt verfügt über mehrere integrierte Parser, die in sbt.complete.DefaultParsers definiert sind. Einige häufig verwendete integrierte Parser sind:

  • Space, NotSpace, OptSpace und OptNotSpace: für die Analyse von Leerzeichen oder Nicht-Leerzeichen, ob erforderlich oder nicht.
  • StringBasic: für die Analyse von Text, der in Anführungszeichen gesetzt werden kann.
  • IntBasic: für das Parsen eines vorzeichenbehafteten Int-Werts.
  • Digit und HexDigit: für das Parsen einer einzelnen Dezimal- oder Hexadezimalziffer.
  • Bool: zum Parsen eines booleschen Wertes

Registerkarte Vervollständigung

Die Tabulatorvervollständigung ist eine Funktion von Parsern, die Beispiele für mögliche Eingaben anzeigt:

lazy val task1 = inputKey[Unit]("")
task1 := {
    val names: Seq[String] = Def.spaceDelimited("Type names")
      .examples(
          "Jean Luc Picard",
          "Jonathan Archer",
          "Benjamin Sisko",
      ).parsed

    println(names)
}

Speichern von zuvor berechneten Werten und Verwendung zum Ausfüllen von Registerkarten

Ausgewertete Aufgaben können entweder auf der Festplatte oder im Arbeitsspeicher gespeichert und z.B. in Parsern zur Vervollständigung von Tabulatoren wiederverwendet werden:

// task1 will be triggered by some other task
// or will be evaluated manually
lazy val task1 = taskKey[Seq[String]]("")
task1 := Seq("a", "b", "c", "d")
task1 := task1.keepAs(task1).value

// independent task, will only load state when stored by
// some other task
lazy val task2 = inputKey[Unit]("")
task2 := {
    import sbt.complete.DefaultParsers._
    val parser = Defaults.getForParser(task1)((state, maybeSeqString) => {
        val strings = maybeSeqString.getOrElse(Seq("a1", "b1", "c1"))
        Space ~> StringBasic.examples(strings: _*)
    })
    Def.inputTask {
        val result: String = parser.parsed
        println(result)
    }
}.evaluated

// see: https://gitter.im/sbt/sbt/archives/2015/08/07
// ==> And the two are a pair. I think getForParser/keepAs are in memory and loadForParser/storeAs is in disk

Allgemeine SBT-Befehle

Die folgenden sbt-Befehle sind praktisch zu wissen. Natürlich können Sie Ihre eigenen Aufgaben erstellen und abfragen, welche Aufgaben verfügbar sind
, indem Sie sbt tasks -V eingeben:

sbt help                                     # Prints a help summary.
sbt about                                    # Displays basic information about sbt and the build.
sbt tasks                                    # Displays the main tasks defined directly or indirectly for the current project.
sbt tasks -V                                 # Displays all tasks
sbt settings                                 # Displays the main settings defined directly or indirectly for the current project.
sbt settings -V                              # Displays all settings
sbt projects                                 # List the names of available builds and the projects defined in those builds.
sbt project                                  # Displays the name of the current project.
sbt project /                                # Changes to the initial project.
sbt project name                             # Changes to the project with the provided name.
sbt run                                      # Runs a main class, passing along arguments provided on the command line
sbt runMain com.github.dnvriend.HelloWorld   # Runs the main class selected by the first argument, passing the remaining arguments to the main method.
sbt console                                  # Starts the Scala interpreter with the project classes on the classpath.
sbt compile                                  # Compiles sources.
sbt clean                                    # Deletes files produced by the build, such as generated sources, compiled classes, and task caches.
sbt test                                     # Executes all tests.
sbt testOnly PersonTest                      # Executes the tests provided as arguments or all tests if no arguments are provided.
sbt ";clean;compile;run"                     # Runs the specified commands.

SourceGenerators

Die Einstellung sourceGenerators definiert eine Liste von
Aufgaben, die Quellen erzeugen. Eine Aufgabe zur Erzeugung von Quellen sollte Quellen in einem Unterverzeichnis von sourceManaged erzeugen und eine Folge von erzeugten Dateien zurückgeben.
Der Schlüssel, zu dem die Aufgabe hinzugefügt werden soll, heißt sourceGenerators. Da wir die Aufgabe und nicht den Wert nach ihrer Ausführung hinzufügen wollen, verwenden wir taskValue anstelle des üblichen value. Er sollte je nachdem, ob es sich bei den erzeugten Dateien um Haupt- (Compile) oder Testquellen (Test) handelt, skaliert werden.
Angenommen, wir möchten eine Datei BuildInfo.scala erzeugen, die Informationen über unseren Build enthält. Wir können wie folgt vorgehen:

  • eine Aufgabe 'getBuildInfo' erstellen, die Informationen über unseren Build sammelt,
  • Erstellen Sie einen Task 'makmakeBuildInfo', der die Datei 'Information.scala' im sourceManaged-Verzeichnis erstellt und die von 'getBuildInfo' zusammengetragenen
    Informationen in dieser Datei speichert.
  • eine Konsolenanwendung 'main.Main' erstellen, die die Datei 'BuildInfo.scala' verwendet
  • die Konsolenanwendung ausführen
lazy val getCommitSha = taskKey[String]("Returns the current git commit SHA")

getCommitSha := {
  Process("git rev-parse HEAD").lines.head
}

lazy val getCurrentDate = taskKey[String]("Get current date")

getCurrentDate := {
  new java.text.SimpleDateFormat("yyyy-HH-mm'T'hh:MM:ss.SSSSXX").format(new java.util.Date())
}

lazy val getBuildInfo = taskKey[String]("Get information about the build")

getBuildInfo := {
  s"""Map(
     |  "name" -> "${name.value}",
     |  "organization" -> "${organization.value}",
     |  "version" -> "${version.value}",
     |  "date" -> "${getCurrentDate.value}",
     |  "commit" -> "${getCommitSha.value}",
     |  "scalaVersion" -> "${scalaVersion.value}",
     |  "libraryDependencies" -> "${libraryDependencies.value}"
     |)
   """.stripMargin
}

lazy val makeBuildInfo = taskKey[Seq[File]]("Makes the BuildInfo.scala file")

makeBuildInfo := {
  val resourceDir: File = (sourceManaged in Compile).value
  val configFile: File = new File(resourceDir, "BuildInfo.scala")
  val content =
    s"""
       |package build
       |
       |object BuildInfo {
       |  val info: Map[String, String] = ${getBuildInfo.value}
       |}
     """.stripMargin
  IO.write(configFile, content)
  Seq(configFile)
}

sourceGenerators in Compile += makeBuildInfo.taskValue

Wir brauchen eine Konsolenanwendung, um sie zu testen, also fügen Sie die folgende Klasse in 'src/main/scala/main' ein:

package main

object Main extends App {
  println(build.BuildInfo.info)
}

Führen Sie die Anwendung mit 'sbt run' aus.

RessourcenGeneratoren

Die Einstellung resourceGenerators definiert eine Liste von
Aufgaben, die Ressourcen erzeugen. Eine Ressourcengenerierungsaufgabe sollte Ressourcen in einem Unterverzeichnis von resourceManaged
und geben eine Folge von erzeugten Dateien zurück.
Der Schlüssel, zu dem die Aufgabe hinzugefügt werden soll, heißt resourceGenerators. Da wir die Aufgabe hinzufügen wollen und nicht den Wert nach ihrer Ausführung,
verwenden wir taskValue anstelle des üblichen value. Die Skalierung sollte sich danach richten, ob es sich bei den generierten Dateien um Haupt- (Compile)
oder Testressourcen (Test) handelt.
Wenn wir beispielsweise den Git-Commit-Hash unseres Projekts abrufen und in einer Typesafe-Konfigurationsdatei mit dem Namen
'version.config' im Verzeichnis 'resourceManaged' speichern möchten, können wir wie folgt vorgehen:

  • Erstellen Sie eine Aufgabe 'gitCommitSha', die 'git' abfragt, die Antwort parst und den Git-Hash als String zurückgibt
  • eine Aufgabe 'makeVersionConfig' erstellen, die die Datei 'version.config' im resourceManaged-Verzeichnis erstellt und den
    git commit hash in dieser Datei speichert
  • eine Konsolenanwendung 'main.Main' erstellen, die die Datei 'version.config' verwendet
  • die Konsolenanwendung ausführen
    Lassen Sie uns zunächst die beiden Tasks erstellen. Sie können Folgendes in die 'build.sbt' einfügen:
// we need the typesafe-config library
libraryDependencies += "com.typesafe" % "config" % "1.3.1"

// 'gitCommitSha' will query 'git' for the SHA of HEAD
lazy val gitCommitSha = taskKey[String]("Returns the current git commit SHA")

gitCommitSha := {
  Process("git rev-parse HEAD").lines.head
}

// 'makeVersionConfig' will create the 'version.config' file
lazy val makeVersionConfig = taskKey[Seq[File]]("Makes a version config file")

makeVersionConfig := {
  println("Creating makeVersionConfig")
  val resourceDir: File = (resourceManaged in Compile).value
  val configFile: File = new File(resourceDir, "version.config")
  val gitCommitValue: String = gitCommitSha.value
  val content = s"""commit-hash="$gitCommitValue""""
  IO.write(configFile, content)
  Seq(configFile)
}

// add the 'makeVersionConfig' Task to the list of resourceGenerators.
// resourceGenerators is of type: SettingKey[Seq[Task[Seq[File]]]] which
// means that we can add, well, resourceGenerators to it,
// like our 'makeVersionConfig' which is a resourceGenerator.
resourceGenerators in Compile += makeVersionConfig.taskValue

Wir brauchen eine Konsolenanwendung, um sie zu testen, also fügen Sie die folgende Klasse in 'src/main/scala/main' ein:

package main

import com.typesafe.config.ConfigFactory

import scala.io.Source

object Main extends App {
  val config = ConfigFactory.parseURL(getClass.getResource("/version.config"))
  val hashFromConfig = config.getString("commit-hash")
  val versionConfigFileAsString = Source.fromURL(getClass.getResource("/version.config")).mkString
  println(
    s"""
      |versionConfigFile: $versionConfigFileAsString
      |hashFromConfig: $hashFromConfig
    """.stripMargin)
}

Führen Sie die Anwendung mit 'sbt run' aus.

Server, Client, Shell

Sbt-Server ist eine Funktion von sbt 1.x, die den Netzwerkzugriff auf eine einzelne laufende Instanz von Sbt ermöglicht. Dadurch können mehrere Clients
eine Verbindung zu einer einzigen Sbt-Sitzung herstellen. Der primäre Anwendungsfall ist die Integration von Werkzeugen wie Editoren und IDEs.
Es gibt drei neue Befehle:

  • sbt shell: bietet eine interaktive Eingabeaufforderung, von der aus Befehle ausgeführt werden können
  • sbt client 127.0.0.1:: bietet eine interaktive Eingabeaufforderung, mit der Befehle auf einem Server ausgeführt werden können
  • sbt startServer: startet den sbt-Server, wenn er noch nicht gestartet wurde.
    Standardmäßig wird der interaktive Modus von sbt gestartet, wenn keine Befehle auf der CLI angegeben werden. Die interaktive Shell wird auch gestartet, wenn der Befehl shell mit dem Befehl
    sbt shell aufgerufen wird. Mit dem Befehl sbt shell wird nicht nur eine interaktive Shell gestartet, sondern auch der sbt-Server. Wenn Sie nur sbt -Dsbt.server.autostart=false,
    eingeben, wird nicht der Server, sondern die interaktive Shell gestartet.
    Wenn Sie eine sbt-Sitzung durch Eingabe von sbt starten, werden die interaktive Shell und der sbt-Server gestartet. Der Server gibt den Port aus, auf dem er läuft:
$ sbt
[info] Loading settings from idea.sbt,sbt-updates.sbt ...
[info] Loading global plugins from /Users/dennis/.sbt/1.0/plugins
[info] Loading settings from plugins.sbt ...
[info] Loading project definition from /Users/dennis/projects/study-sbt/project
[info] Loading settings from build.sbt ...
[info] Set current project to study-sbt (in build file:/Users/dennis/projects/study-sbt/)
[info] sbt server started at 127.0.0.1:5829

Der Server läuft auf '127.0.0.1:5829'.
Wenn wir eine neue Terminalsitzung starten und ein beliebiges Verzeichnis auf Ihrem System eingeben: sbt client 127.0.0.1:5829 eingeben, erhalten wir die folgende Ausgabe auf dem Server:

sbt:study-sbt> [info] new client connected from: 50745

und wir erhalten die folgende Ausgabe auf dem Client:

$ sbt client 127.0.0.1:5829
[info] Loading settings from idea.sbt,sbt-updates.sbt ...
[info] Loading global plugins from /Users/dennis/.sbt/1.0/plugins
[info] Updating {file:/Users/dennis/.sbt/1.0/plugins/}global-plugins...
[info] Done updating.
[info] Loading project definition from /Users/dennis/projects/project
[info] Updating {file:/Users/dennis/projects/project/}projects-build...
[info] Done updating.
[info] Set current project to projects (in build file:/Users/dennis/projects/)
client on port 5829
ChannelAcceptedEvent(channel-1)

Wir können nun Befehle in den Client eingeben, aber beachten Sie bitte, dass Eingaben wie 'show name' (das erste, was ich getan habe) nur auf dem Server als Ausgabe erscheinen.
Der Befehl exit schließt die Verbindung wie erwartet.

Forking-Prozesse

Sbt kann Prozesse mit der Fork-API aufspalten. Forken wir die folgende Anwendung, die sich in src/main/scala/main/Main.scala befindet:

package main

object Main extends App {
  println("Hello World!: " + args.toList)
}

Schauen wir uns nun unseren Aufbau an:

val task1 = taskKey[Unit]("")
task1 := {
  def classpathOption(classpath: Seq[File]): Seq[String] = {
    "-classpath" :: Path.makeString(classpath) :: Nil
  }
  val cp: Seq[File] = (fullClasspath in Compile).value.map(_.data)
  val mainClass: String = "main.Main"
  val arguments: Seq[String] = Seq("Hello World!")
  val outputStrategy = StdoutOutput
//  val outputStrategy = LoggedOutput(streams.value.log)
  val config: ForkOptions = ForkOptions(
    javaHome.value,
    Some(outputStrategy), // StdOutput, LoggedOutput
    Vector(),
    Some(baseDirectory.value),
    javaOptions.value.toVector,
    connectInput.value,
    envVars.value
  )
  val fullArguments: Seq[String] = classpathOption(cp) ++ Seq(mainClass) ++ arguments

  // Process is being executed
  val exitValue = Fork.java(config, fullArguments)
}

Fork ruft einfach den Befehl 'java' auf, genau wie Sie es mit der CLI tun würden. Wie Sie wissen, können Sie alle möglichen Argumente angeben, damit java funktioniert, und ein vollständiger Befehl wäre etwa so:

java -classpath <all jars here> main.Main <args here>

Sbt hat solche Aufrufe für uns abstrahiert und stellt sie als Fork-API zur Verfügung. Wir müssen einige Einstellungen vornehmen, damit es funktioniert:

  • Bestimmen Sie, was die Hauptklasse ist (main.Main)
  • Bestimmen Sie die Argumente für die Hauptklasse (arguments)
  • Bestimmen Sie den Klassenpfad (der Teil nach -classpath), der vor 'main.Main' stehen muss,
  • Bestimmen Sie, wie die Ausgabe unseres Forked-Prozesses protokolliert werden soll (an StdOutput oder den Sbt Logger)
  • Bestimmen Sie javaHome, Boot-Klassen (Vector()), das zu verwendende baseDir, die javaOptions und Umgebungsvariablen

Kurz gesagt, wir richten eine vollständig konfigurierte JVM-Instanz ein und alles muss stimmen, um unsere Klasse 'main.Main' zu starten.
Schließlich können wir den Aufruf an Fork.java(config, fullArguments) tätigen und es ist ein blockierender Aufruf an diese JVM. Das Ergebnis ist ein ExitValue vom Typ Int.
Die Abhängigkeit dieser Aufgabe ist die folgende und wie erwartet sehen wir alle Aufgaben und Einstellungen, die wir aufrufen, bevor wir unseren Prozess aufgabeln können:

sbt:study-sbt> inspect task1
[info] Task: Unit
[info] content:
[info]
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task1
[info] Defined at:
[info]  /Users/dennis/projects/study-sbt/build.sbt:2
[info] Dependencies:
[info]  *:connectInput
[info]  *:envVars
[info]  *:javaHome
[info]  *:javaOptions
[info]  compile:fullClasspath
[info]  *:baseDirectory
[info] Delegates:
[info]  *:task1
[info]  {.}/*:task1
[info]  */*:task1

Einen ScalaRun 'Läufer' verwenden

Das vorherige Beispiel, in dem wir ein Programm geforkt haben, kann viel kürzer werden, wenn wir Keys.runner verwenden, das in Defaults.runnerTask implementiert ist und eine ScalaRun zurückgibt, 'eine vollständig konfigurierte Runner-Implementierung, die zum Ausführen einer Hauptklasse verwendet wird', wie zum Beispiel die Klasse main.Main. Die Aufgabe wird dann viel kürzer:

 val runnerToUse: ScalaRun = runner.value
  Run.run(
  "main.Main",
  (fullClasspath in Compile).value.map(_.data),
  Seq("hello", "world"),
  streams.value.log)(runnerToUse)

Beachten Sie, dass wir den Runner in den impliziten Geltungsbereich setzen können, wenn wir wollen, da es sich nur um einen generischen Runner handelt, der überall injiziert werden kann. Hier übergebe ich ihn explizit an Run.run(...)(runnerToUse), um auf den Anwendungsfall hinzuweisen.
Wenn wir uns die Abhängigkeiten ansehen, sehen wir Folgendes:

sbt:study-sbt> inspect task1
[info] Task: Unit
[info] content:
[info]
[info] Provided by:
[info]  {file:/Users/dennis/projects/study-sbt/}study-sbt/*:task1
[info] Defined at:
[info]  /Users/dennis/projects/study-sbt/build.sbt:2
[info] Dependencies:
[info]  *:task1::streams
[info]  compile:fullClasspath
[info]  *:runner
[info] Delegates:
[info]  *:task1
[info]  {.}/*:task1
[info]  */*:task1

Kompilieren einer Datei in einer Aufgabe

Das folgende Beispiel zeigt, wie Sie eine Aufgabe erstellen, die eine Datei erstellt, die Datei kompiliert und diese Datei ausführt. Bitte beachten Sie, dass dieses Beispiel etwas Code enthält. Der größte Teil des Codes besteht aus zwei Klassen, die für den Compiler erforderlich sind.
Das Beispiel zeigt, wie eine Aufgabe erstellt wird, die einige Ressourcen einrichtet, die wir benötigen. Es hat sich bewährt, alle sbt-Werte am Anfang Ihres Codes zu erhalten und dann, wenn alle Aufrufe an .value erledigt sind, mit Ihrer Logik zu beginnen. Auf diese Weise funktioniert der Ausführungsfluss wie erwartet.
Das Kompilieren von Dateien ist nicht trivial, selbst wenn Sie die Abstraktion und die APIs von Sbt verwenden. Dennoch, in nur 100 Zeilen erstellen wir eine Datei, kompilieren sie und führen sie aus, was nicht schlecht ist.

val task1 = taskKey[Unit]("")

task1 := {
    val compilers = Keys.compilers.value
    val classpath: Seq[File] = (fullClasspath in Compile).value.map(_.data)
    val outputDir: File = (classDirectory in Compile).value
    val options: Seq[String] = (scalacOptions in Compile).value
    val inputs: xsbti.compile.Inputs = (compileInputs in Compile in compile).value
    val cache: xsbti.compile.GlobalsCache = inputs.setup().cache()
    val log = streams.value.log
    implicit val runnerToUse: ScalaRun = runner.value

    val targetDir: File = target.value
    val fileToCompile: File = targetDir / "Engage.scala"
    val maxErrors: Int = 1000

    // create the Engage.scala file
    IO.createDirectory(targetDir)
    IO.write(fileToCompile, """object Engage extends App { println("Engage!") }""")
    // compile the file

    compilers.scalac() match {
      case compiler: sbt.internal.inc.AnalyzingCompiler =>
        compileSingleFile(
          compiler,
          fileToCompile,
          classpath,
          outputDir,
          options,
          maxErrors,
          cache,
          noChanges,
          noopCallback,
          log
        )
      case _ => sys.error("Expected a 'sbt.internal.inc.AnalyzingCompiler' compiler")
    }

  // lets run 'Engage'
  runSingleFile("Engage", classpath, Seq.empty, log)
}

def runSingleFile(fqcn: String,
                  classpath: Seq[File],
                  options: Seq[String],
                  log: Logger
                 )(implicit scalaRun: ScalaRun): scala.util.Try[Unit] = {
  log.info(s"Running: single file: $fqcn")
  Run.run(fqcn, classpath, options, log).map { _ =>
    log.info(s"Successfully executed: $fqcn")
  } recover { case t: Throwable =>
    log.error(s"Failure running: $fqcn, reason: ${t.getMessage}")
    throw t
  }
}

def compileSingleFile(
                       compiler: sbt.internal.inc.AnalyzingCompiler,
                       fileToCompile: File,
                       classpath: Seq[File],
                       outputDir: File,
                       options: Seq[String],
                       maxErrors: Int,
                       cache: xsbti.compile.GlobalsCache,
                       dependencyChanges: xsbti.compile.DependencyChanges,
                       analysisCallback: xsbti.AnalysisCallback,
                       log: sbt.internal.util.ManagedLogger): Unit = {

  log.info(s"Compiling a single file: $fileToCompile")

      compiler.apply(
        Array(fileToCompile),
        dependencyChanges,
        classpath.toArray,
        outputDir,
        options.toArray,
        analysisCallback,
        maxErrors,
        cache,
        log
      )
}

lazy val noChanges = new xsbti.compile.DependencyChanges {
  def isEmpty = true
  def modifiedBinaries = Array()
  def modifiedClasses = Array()
}

lazy val noopCallback = new xsbti.AnalysisCallback {
  override def startSource(source: File): Unit = {}
  override def mainClass(sourceFile: File, className: String): Unit = {}
  override def apiPhaseCompleted(): Unit = {}
  override def enabled(): Boolean = false
  override def binaryDependency(onBinaryEntry: File, onBinaryClassName: String, fromClassName: String, fromSourceFile: File, context: xsbti.api.DependencyContext): Unit = {}
  override def generatedNonLocalClass(source: File, classFile: File, binaryClassName: String, srcClassName: String): Unit = {}
  override def problem(what: String, pos: xsbti.Position, msg: String, severity: xsbti.Severity, reported: Boolean): Unit = {}
  override def dependencyPhaseCompleted(): Unit = {}
  override def classDependency(onClassName: String, sourceClassName: String, context: xsbti.api.DependencyContext): Unit = {}
  override def generatedLocalClass(source: File, classFile: File): Unit = {}
  override def api(sourceFile: File, classApi: xsbti.api.ClassLike): Unit = {}
  override def usedName(className: String, name: String, useScopes: java.util.EnumSet[xsbti.UseScope]): Unit = {}
}

Abrufen von Typen aus dem Scala-Typsystem per String

Es ist möglich, aber nicht empfehlenswert, Typen aus dem scala Type System über String abzurufen. Normalerweise würden wir etwas wie das Folgende tun, um eine Referenz auf List[Int] zu erhalten:

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> typeOf[List[Int]]
res0: reflect.runtime.universe.Type = scala.List[Int]

Wir könnten auch den Compiler bitten, eine TypeTag für uns zu erstellen und die Methode tpe verwenden, um an den Typ zu gelangen:

scala> implicitly[TypeTag[List[Int]]]
res1: reflect.runtime.universe.TypeTag[List[Int]] = TypeTag[scala.List[Int]]

scala> res1.tpe
res2: reflect.runtime.universe.Type = scala.List[Int]

Der Nachteil dieses Ansatzes ist, dass wir den Typ als Literal wie List[Int] angeben müssen, und das ist bei der Arbeit mit SBT nicht immer möglich.
Wenn wir das Problem etwas herunterbrechen, wäre es dann möglich, ein TypeTag zu konstruieren und ihm zwei Informationen zu geben? Ich möchte zum Beispiel ein TypeTag für ein scala.collection.immutable.List erhalten, das mit einem scala.Int konstruiert werden muss. Das sollte doch nicht so schwer sein, oder?
Lassen Sie uns das folgende Objekt in project/CustomLoader.scala erstellen. Wir werden einige Pakete aus scala.reflect importieren, die sich mit den build.sbt Standardimporten überschneiden:

object CustomLoader {
  import scala.reflect.api
  import scala.reflect.api.{TypeCreator, Universe}
  import scala.reflect.runtime.universe._

  def createTypeTagForST(simpleTypeName: String, classLoader: Option[ClassLoader] = None): TypeTag[_] = {
    val currentMirror = classLoader
      .map(cl => scala.reflect.runtime.universe.runtimeMirror(cl))
      .getOrElse(scala.reflect.runtime.currentMirror)
    val typSym = currentMirror.staticClass(simpleTypeName)
    val tpe = internal.typeRef(NoPrefix, typSym, List.empty)
    val ttag = TypeTag(currentMirror, new TypeCreator {
      override def apply[U <: Universe with Singleton](m: api.Mirror[U]): U#Type = {
        assert(m == currentMirror, s"TypeTag[$tpe] defined in $currentMirror cannot be migrated to $m.")
        tpe.asInstanceOf[U#Type]
      }
    })
    ttag
  }

  def createTypeTagForHKT(higherKindedTypeName: String = "scala.collection.immutable.List",
                                    parameterSymbol: String = "scala.Int",
                                    classLoader: Option[ClassLoader] = None): TypeTag[_] = {
    val currentMirror = classLoader
       .map(cl => scala.reflect.runtime.universe.runtimeMirror(cl))
      .getOrElse(scala.reflect.runtime.currentMirror)
    val typSym = currentMirror.staticClass(higherKindedTypeName)
    val paramSym = currentMirror.staticClass(parameterSymbol)
    val tpe = internal.typeRef(NoPrefix, typSym, List(paramSym.selfType))
    val ttag = TypeTag(currentMirror, new TypeCreator {
      override def apply[U <: Universe with Singleton](m: api.Mirror[U]): U#Type = {
        assert(m == currentMirror, s"TypeTag[$tpe] defined in $currentMirror cannot be migrated to $m.")
        tpe.asInstanceOf[U#Type]
      }
    })
    ttag
  }
}

Wir haben zwei Methoden in unserem 'CustomLoader', eine Methode, die ein TypeTag für einen SimpleType wie 'scala.Int' erstellt, und eine andere, die TypeTags für höherwertige Typen wie List[Int] erstellt. Der einzige Preis, den wir zahlen, ist, dass wir Typisierungsinformationen verlieren, so dass der Rückgabetyp ein TypeTag von irgendetwas ist. TypeTag[_]
Lassen Sie uns das verwenden. Zurück in unserem build.sbt, lassen Sie uns eine Aufgabe erstellen, die sowohl einen höherwertigen Typ als auch einen einfachen Typ erzeugt:

lazy val task1 = taskKey[Unit]("Load simple types and higher kinded types")
task1 := {
  import scala.reflect.runtime.universe._
  val listOfIntTypeTag: TypeTag[_] = CustomLoader.createTypeTagForHKT("scala.collection.immutable.List", "scala.Int")
  assert(listOfIntTypeTag.toString == "TypeTag[List[Int]]")

  val intTypeTag: TypeTag[_] = CustomLoader.createTypeTagForST("scala.Int")
  assert(intTypeTag.toString == "TypeTag[Int]")
}

Wir können optional einen Classloader bereitstellen, der unsere benutzerdefinierten Typen laden kann, z. B. main.Person. Wir müssen zunächst eine Aufgabe haben, die einen solchen Classloader bei Bedarf bereitstellen kann:

lazy val getFullClassLoader = taskKey[ClassLoader]("Returns a classloader that can load all project dependencies and compiled sources")
getFullClassLoader := {
  val scalaInstance: ScalaInstance = Keys.scalaInstance.value
  val fullClasspath: Seq[File] = (Keys.fullClasspath in Compile).value.map(_.data)
  val classDirectory: File = (Keys.classDirectory in Compile).value
  val classpath = Seq(classDirectory) ++ fullClasspath
  val cl: ClassLoader = sbt.internal.inc.classpath.ClasspathUtilities.makeLoader(classpath, scalaInstance)
  cl
}

Dieser Classloader kann in der folgenden Aufgabe verwendet werden, in der wir eine Case-Klasse main.Person laden, die sich in unserem nicht verwalteten Quellverzeichnis src/main/scala/main/Person.scala befindet:

package main

import scala.annotation.StaticAnnotation

case class Named(name: String = "", age: String = "") extends StaticAnnotation

@Named(name = "Dennis", age = "43")
@MyMarker()
case class Person(name: String, age: Int)

Die Klasse wurde sowohl mit einer Java- als auch mit einer Scala-Annotation versehen:

package main;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MyMarker {
    String name() default "N/A";
    String author() default "Dennis Was Here";
}

Lassen Sie uns eine TypeTag abrufen und nach Informationen fragen. Bitte beachten Sie, dass wir die Aufgabe getFullClassLoader verwenden und sie an CustomLoader weitergeben, damit sie alle Typen auflösen kann:

lazy val task2 = taskKey[Unit]("Load custom types and annotations")
task2 := {
  import scala.reflect.runtime.universe._
  val cl = getFullClassLoader.value
  val personTypeTag: TypeTag[_] = CustomLoader.createTypeTagForST("main.Person", Option(cl))
  assert(personTypeTag.toString == "TypeTag[Person]")
  assert(personTypeTag.tpe.typeSymbol.annotations.mkString(",") == """main.Named("Dennis", "43"),main.MyMarker""")

  val namedAnnotationTypeTag: TypeTag[_] = CustomLoader.createTypeTagForST("main.Named", Option(cl))
  assert(namedAnnotationTypeTag.toString == "TypeTag[Named]")

  val maybeNamedAnnotation: Option[Annotation] = personTypeTag.tpe.typeSymbol.annotations.find(_.tree.tpe =:= namedAnnotationTypeTag.tpe)

  assert(maybeNamedAnnotation.isDefined)
}

Einstellungen Initialisierung

Sie wissen inzwischen, dass ein Sbt-Build aus einem oder mehreren Projekten besteht und dass diese Projekte aus Einstellungen und Aufgaben bestehen. Der beste Inhalt darüber, wie dieser Prozess funktioniert, ist in der Sbt-Dokumentation - Settings Initialization beschrieben, die Sie in 10 Minuten lesen können.
Der Prozess der Initialisierung besteht aus zwei Schritten:

  1. Sammeln Sie alle Einstellungen von definierten Orten
  2. Alle Einstellungen in einer vorgegebenen Reihenfolge anwenden

1. Sammeln von Einstellungen

Die Einstellungen werden an vordefinierten Orten gesammelt:

  • Projekt auf Benutzerebene (~/.sbt/):
  • Laden Sie alle Plugins, die in ~/.sbt/<version>/plugins/*.sbt
  • Laden Sie alle Plugins, die in ~/.sbt/<version>/plugins/*.scala
  • Laden Sie alle Einstellungen, die in ~/.sbt/<version>/*.sbt
  • Laden Sie alle Einstellungen, die in ~/.sbt/<version>/*.scala)
  • SBT Projekt-Ebene:
  • Laden Sie alle Plugins, die in project/plugins.sbt
  • Laden Sie alle Plugins, die in project/project/*.scala
  • Laden Sie alle Einstellungen, die in project/*.sbt
  • Laden Sie alle Einstellungen, die in project/*.scala
  • Laden Sie das Projekt *.sbt Dateien build.sbt und Freunde.

Das Ergebnis all dieser Auflösungen ist eine Folge von Seq[Setting[_]], die geordnet werden muss und daher gibt es Einstellungen, die andere Einstellungen außer Kraft setzen.

2. Einstellungen in einer vordefinierten Reihenfolge anwenden

Die im vorherigen Schritt gesammelten Einstellungen ( Seq[Setting[_]] ) müssen geordnet werden. Es gibt eine vordefinierte Reihenfolge für diese Einstellungen, so dass die Einstellungen andere Einstellungen überschreiben. Die Reihenfolge ist:

  1. Alle AutoPlugin-Einstellungen
  2. Alle Einstellungen, die in project/Build.scala
  3. Alle im Benutzerverzeichnis definierten Einstellungen ~/.sbt/<version>/*.sbt
  4. Alle lokalen Konfigurationen build.sbt
    Das effektive Ergebnis ist ein Task-Graph, der zur Ausführung des Builds verwendet wird.

Befehle

Bei der Arbeit mit SBT gibt es mehrere Definitionen von 'einem Befehl'. Wenn Sie mit Sbt arbeiten, werden die Dinge, die Sie in die Konsole eingeben, als 'Befehle' bezeichnet. Diese Befehle, die Sie eingeben, lösen meist eine 'Aufgabe' oder eine 'Einstellung' aus, wie z.B. 'Name', die die Einstellung 'Name' auswertet. Neben einer 'Einstellung' oder einer 'Aufgabe' gibt es noch eine dritte Sache, die ausgeführt werden kann, und diese Sache wird 'Befehl' genannt.
In sbt gibt es eine technische Unterscheidung zwischen Aufgaben, die sich "innerhalb" der Build-Definition befinden, und Befehlen, die die Build-Definition selbst manipulieren. In den meisten Fällen ist es nicht notwendig, Befehle zu erstellen, da die meisten Aktivitäten durch die Verkettung mehrerer Tasks implementiert werden können.
Zusammenfassend lässt sich sagen, dass ein Befehl einem Task insofern ähnelt, als es sich bei beiden um eine benannte Operation handelt, die von der Konsole aus ausgeführt werden kann, und dass beide beliebigen Code ausführen können. Der Hauptunterschied besteht darin, dass ein Befehl als Parameter "den gesamten Zustand des Builds" erhält, der durch State repräsentiert wird, und er muss einen neuen State berechnen. Die Aufgabe eines Befehls ist es also, einen neuen Build-Status zu berechnen, während die Aufgabe eines Tasks darin besteht, Aufgaben auszuführen, z.B. Arbeit zu erledigen. Ein Befehl berechnet also den Status und eine Aufgabe führt die Arbeit aus, d.h. sie erzeugt die Nebeneffekte, die bei der Arbeit mit einem Build-Tool erforderlich sind.
Lassen Sie uns einen helloworld-Befehl erstellen, der die Methode Command.command verwendet, um einen Befehl ohne Argumente mit dem angegebenen Namen und Effekt zu erzeugen:

lazy val hello = Command.command("hello") { state =>
  println("Hello there!")
  state
}

commands += hello

Nachdem wir den Befehl hello in die Liste der Befehle des Builds aufgenommen haben, können wir ihn durch Eingabe von hello aufrufen:

sbt:study-sbt> hello
Hello there!

Schauen wir uns einige andere Möglichkeiten an, Befehle zu konstruieren, z.B. Command.args, das einen Befehl mit mehreren Argumenten mit dem angegebenen Namen, der Anzeige der Tabulatorvervollständigung und dem Effekt konstruiert:

val helloAll = Command.args("helloAll", "<name>") { (state: State, args: Seq[String]) =>
  println(s"Hi $args")
  state
}

commands += helloAll

Wir können es mit ausführen:

sbt:study-sbt> helloAll a b c d e
Hi List(a, b, c, d, e)

Der vorherige Befehl akzeptiert mehrere Argumente, daher die Argumentliste, aber wir können einen Befehl, der nur ein einziges Argument akzeptiert, mit der Methode Command.single ausführen, die einen Befehl mit einem einzigen Argument mit dem angegebenen Namen und der angegebenen Wirkung erzeugt:

def hello = Command.single("hello") { (state: State, input: String) =>
  println(s"Hello $input")
  state
}

commands += hello

Die Ausgabe lautet:

sbt:study-sbt> hello foo bar baz
Hello foo bar baz

Wir können auch Informationen aus dem aktuellen Status erhalten:

def printState = Command.command("printState") { state =>
  import state._
  println(definedCommands.size + " registered commands")
  println("commands to run: " + show(remainingCommands))
  println()
  println("original arguments: " + show(configuration.arguments))
  println("base directory: " + configuration.baseDirectory)
  println()
  println("sbt version: " + configuration.provider.id.version)
  println("Scala version (for sbt): " + configuration.provider.scalaProvider.version)
  println()

  val extracted = Project.extract(state)
  import extracted._
  println("Current build: " + currentRef.build)
  println("Current project: " + currentRef.project)
  println("Original setting count: " + session.original.size)
  println("Session setting count: " + session.append.size)
  state
}

def show[T](s: Seq[T]) = {
  s.map("'" + _ + "'").mkString("[", ", ", "]")
}
commands += printState

Wenn Sie den folgenden Befehl ;printState;clean;run ausführen, erhalten wir die folgende Ausgabe:

sbt:study-sbt> ;printState;clean;run
56 registered commands
commands to run: ['Exec(clean, None, Some(CommandSource(console0)))', 'Exec(run, None, Some(CommandSource(console0)))', 'Exec(shell, None, None)']

original arguments: []
base directory: /Users/dennis/projects/study-sbt

sbt version: 1.0.3
Scala version (for sbt): 2.12.4

Current build: file:/Users/dennis/projects/study-sbt/
Current project: study-sbt
Original setting count: 644
Session setting count: 0

Befehle, Einstellungen, Aufgaben und Status

Befehle und Tasks verwenden das Objekt State, um temporäre Werte zu speichern, die zwischen Befehlen oder zwischen einem Befehl und Tasks weitergegeben werden müssen, von Befehl zu Task, und zwar nur einseitig innerhalb einer einzigen Sitzung, ohne das Projekt neu zu laden. Zum Beispiel:

lazy val msg = settingKey[String]("")

lazy val ping = taskKey[Unit]("")
ping := {
  val buildState = state.value
  println(buildState.get(msg.key))
  buildState.put(msg.key, "ping")
}

lazy val pong = Command.command("pong") { state =>
  println(state.get(msg.key))
  state.put(msg.key, "pong")
}

commands += pong

Um einen Ping/Pong zu erstellen, muss der Befehl einen Wert in den Statusattributen speichern und die Aufgabe muss einen Wert in der Sitzung speichern, und zwar mit Hilfe der Methode keepAs:

lazy val msg = settingKey[String]("")

lazy val ping = taskKey[String]("")
ping in Global := {
  val buildState = state.value
  // command to task (read from state attributes)
  println(buildState.get(msg.key))
  "ping"
}
// store in session
ping in Global := (ping in Global).keepAs(ping).value

lazy val pong = Command.command("pong") { state =>
  val extracted = Project.extract(state)
  val session = state.get(sessionVars).get
  println(session.get(ping in Global))
  state.put(msg.key, "pong")
}

commands += pong

Status und können auch mit der Methode storeAs persistiert werden:

import sjsonnew.BasicJsonProtocol._
import scala.compat.Platform

lazy val msg = settingKey[String]("")
lazy val ping = taskKey[String]("")
ping in Global := {
  val buildState = state.value
  println("ping: " + buildState.get(msg.key))
  "ping" + Platform.currentTime
}
// store in session
ping in Global := (ping in Global).storeAs(ping).value

lazy val pong = Command.command("pong") { state =>
  // get persisted state, pass the state, get a new state back
  val (st, result) = SessionVar.loadAndSet((ping in Global).scopedKey, state)
  println("pong: " + result)
  // set attribute
  st.put(msg.key, "pong: " + Platform.currentTime)
}

commands += pong

Lassen Sie uns ein einfaches 'Schlangen'-Spiel erstellen. Zunächst wird die Schlange nicht gefunden. Sie haben drei Befehle, 'snake', der einfach nur eine Schlange druckt und dazu dient, Informationen in der Sitzung zu speichern, alterSnake, der die Sitzung nach dem Schlüssel in einem Bereich liest, also ScopedKey, so dass (snake in Global) ein ScopedKey wäre, 'a snake (key) in Global scope', und persistiert, um eine Sitzungsvariable mit diesem Aufgabenschlüssel zu speichern. Nur 'Task-Keys' können zum Persistieren von Sitzungswerten verwendet werden. createSnake 'erstellt' eine Schlange, indem es den Kopf der Schlange auf einen Sitzungswert persistiert. Dieser Sitzungswert kann von anderen Aufgaben oder Befehlen gelesen werden. Die SessionVars können also alternativ dazu verwendet werden, um in beide Richtungen zwischen Aufgaben und Befehlen unter Verwendung desselben Schlüssels zu kommunizieren; die Task Key und der zu verwendende Bereich ist Global.

import sjsonnew.BasicJsonProtocol._
lazy val snake = taskKey[String]("")
snake in Global := {
  val log = streams.value.log
  val str = SessionVar.load((snake in Global).scopedKey, state.value)
    .getOrElse("not found")
  log.info(str)
  str
}

lazy val alterSnake = Command.command("alterSnake") { state =>
  val maybeSnake = SessionVar.load((snake in Global).scopedKey, state)
  maybeSnake.foreach { body =>
    SessionVar.persist((snake in Global).scopedKey, state, body + "<")
  }
  state.log.info(maybeSnake.getOrElse("Snake is still hidden"))
  state
}

lazy val createSnake = taskKey[Unit]("")
createSnake := {
  SessionVar.persist((snake in Global).scopedKey, state.value, "-:")
}

commands += alterSnake

Ein komplexeres Beispiel finden Sie weiter unten:

import sjsonnew.BasicJsonProtocol._
// setting key can only be changed with a 'reload'
// and reloading is something we want to avoid
lazy val personName = settingKey[String]("The name of a person")
personName := "Dennis"

lazy val personAge = settingKey[Int]("The age of a person")
personAge := 42

lazy val savePersonName = taskKey[String]("Saves the person name")
savePersonName := {
  personName.?.value.getOrElse("Unknown")
}
savePersonName := savePersonName.storeAs(savePersonName).value

lazy val savePersonAge = taskKey[Int]("Saves the person age")
savePersonAge := personAge.?.value.getOrElse(0)
savePersonAge := savePersonAge.storeAs(savePersonAge).value

lazy val printPerson = taskKey[Unit]("Prints person")
printPerson := {
  val log = streams.value.log
  val buildState = state.value
  val maybeName = personName.?.value
  val maybeAge = personAge.?.value
  val name = maybeName.orElse(SessionVar.load(savePersonName.scopedKey, buildState)).getOrElse("Unknown")
  val age = maybeAge.orElse(SessionVar.load(savePersonAge.scopedKey, buildState)).getOrElse(-1)
  println(s"Person from state attributes: Person(${buildState.get(personName.key)}, ${buildState.get(personAge.key)})")
  println(s"Person from settings, then session else unknown: Person($name, $age)")
}

lazy val savePerson = taskKey[Unit]("Gets the value of Pi")
savePerson := {
  val buildState = state.value
  val log = streams.value.log
  val maybeName = personName.?.value
  val maybeAge = personAge.?.value
  val name = maybeName.orElse(SessionVar.load(savePersonName.scopedKey, buildState)).getOrElse("Unknown")
  val age = maybeAge.orElse(SessionVar.load(savePersonAge.scopedKey, buildState)).getOrElse(-1)
  log.info(s"Person from settings: Person($name, $age)")

  // is not used, just printed
  println("Name from saved session state:" + SessionVar.load(savePersonName.scopedKey, buildState))
  println("Age from saved session state:" + SessionVar.load(savePersonAge.scopedKey, buildState))

  // store the information in the session vars
  // note that sessionVars uses scopedKey of Task
  // session vars's context are Task values...
  val vars: SessionVar.Map = buildState.get(sessionVars).get // note the .get
  // store the name and age in the session
  vars.put(savePersonName, "Mr Bean")
  vars.put(savePersonAge, 50)
  log.info("Contents of the session vars: " + vars)

  println(s"Person from state attributes: Person(${buildState.get(personName.key)}, ${buildState.get(personAge.key)})")

  // store the name and age in the state attribute map
  // note that the attribute map of State uses the AttributeKey, which is the setting
  val newState = buildState.put(personName.key, "Jean Luc Picard")
    .put(personAge.key, Int.MinValue)

  println(s"Person from state attributes after change: Person(${newState.get(personName.key)}, ${newState.get(personAge.key)})")

  // the thing is, newState is gone now, therefore we have commands...
}

def loadPerson = Command.command("loadPerson") { state =>
  val extracted = Project.extract(state)
  // note that the extracted project settings are 'immutable', we don't want to reload
  println("Get the name from a setting: " + extracted.get(personName))
  println("Get the age from a setting: " + extracted.get(personAge))

  // session variables
  println("Contents of the session vars: " + state.get(sessionVars))
  // we can reuse the 'age' key, by putting a value in the mutable attribute map,
  // which doesn't need a 'reload' to be changed, just a call to 'put' would be enough
  println("Get name from a previous command from the (state) attribute map: " + state.get(savePersonName.key))
  println("Get age from a previous command from the (state) attribute map: " + state.get(savePersonName.key))

  // load session vars
  // note that session vars load values that are the result
  // of tasks, so the keys are of tasks
  val maybeName = SessionVar.load(savePersonName.scopedKey, state)
  val maybeAge = SessionVar.load(savePersonAge.scopedKey, state)
  println(s"Person from loaded session vars (task keys): $maybeName, $maybeAge")
  println(s"Person from state attributes (setting keys): Person(${state.get(personName.key)}, ${state.get(personAge.key)})")

  // note that the attribute state
  // are attribute keys and are settings keys
  state
    .put(personName.key, "from-command")
    .put(personAge.key, Int.MaxValue)
}

commands += loadPerson

Der vorherige Code zeigt eine Einstellung, einen Befehl und eine Aufgabe. Die Einstellung muss als Definition eines Wertes verwendet werden, wie Pi. Dies ist ein stabiler Wert für die Sitzung. Zugegeben, Pi kann durch Eingabe von set pi := 6.28 oder durch Ändern des Wertes in build.sbt geändert werden, aber dazu muss die Sitzung neu geladen werden, und Sie erhalten im Grunde eine neue Sitzung. Die Einstellungen können also als 'unveränderliche' Werte betrachtet werden, zumindest in einer einzigen Sitzung. Der veränderliche Teil einer Sitzung ist die AttributeMap des Status, auf die ein Befehl oder ein Task zugreifen kann.
Sowohl ein Befehl als auch ein Task können auf das Statusobjekt zugreifen. Ein Command erhält einen Verweis auf State und kann direkt darauf zugreifen. Die Task muss einen Verweis auf den State mit der Task state.value erhalten und kann dann darauf operieren. Auf State können Sie unter anderem die Methoden get(key): Option[T], put(key, value): State, remove(key): State, update(key)(f: Option[A] => B): State und has(key): Boolean um mit der Attribut-Map zu arbeiten. Dies ist eine Möglichkeit, wie ein Befehl das Verhalten von Aufgaben ändern kann, ohne dass das Projekt neu geladen werden muss.
Eine zweite Möglichkeit, den Status zu speichern, sind die Methoden keepAs und storeAs auf Task. Diese Werte werden als Sitzungsvariablen gespeichert und können persistiert werden.

Befehle Konstruktion

Ein Befehl benötigt mehrere Dinge, um konstruiert zu werden, natürlich abhängig von der Konstruktionsmethode. Die vollständige Liste lautet:

  • Die Syntax, die der Benutzer zum Aufrufen des Befehls verwendet,
  • Tabulatorvervollständigung für die Syntax,
  • Ein Parser, der die Eingabe in eine Datenstruktur umwandelt,
  • Die Aktion, die mit der geparsten Datenstruktur ausgeführt werden soll
  • Die Aktion wandelt das Build-State-Objekt um,
  • Hilfe, die dem Benutzer zur Verfügung gestellt wird
val action: (State, T) => State = ...
val parser: State => Parser[T] = ...
val command: Command = Command("name")(parser)(action)

Serialisierung der Typesafe-Konfiguration in JSON

Es ist möglich, die Typesafe-Konfiguration in JSON zu serialisieren. Dies kann bei der Integration mit anderen Tools nützlich sein, die JSON oder sogar YAML als Eingabe benötigen. Wir müssen eine Abhängigkeit von der Typesafe Configuration-Bibliothek hinzufügen. Erstellen Sie einfach eine neue Datei project/dependencies.sbt und fügen Sie das Folgende in die Datei ein:

libraryDependencies += "com.typesafe" % "config" % "1.3.1"
lazy val task1 = taskKey[String]("Serializing Typesafe Config to JSON")

task1 := {
  import com.typesafe.config._
  val conf: Config = ConfigFactory.parseString(
    """
      | akka {
      |    boolean_one = true
      |    boolean_two = on
      |    list_of_string = [1, 2, 3, 4, 5]
      |    number = 1
      |    timeout = 10ms
      |    name = "dennis"
      |    person = {
      |      name = "dennis"
      |      age = 42
      |    }
      | }
    """.stripMargin)

  val output: String = conf.root().render(ConfigRenderOptions.concise())
  val json = """{"akka":{"number":1,"boolean_two":"on","person":{"name":"dennis","age":42},"name":"dennis","boolean_one":true,"timeout":"10ms","list_of_string":[1,2,3,4,5]}}"""
  assert(output == json)
  output
}

Aus dieser konvertierten Typesafe Config können wir YAML mit Hilfe der circe-yaml-Bibliothek erzeugen, die den AST von SnakeYAML in den AST von circeübersetzt. Sie ermöglicht das Parsen von YAML 1.1-Dokumenten in den Json-AST von circe und die Verwendung von circe zum Parsen von JSON und Schreiben von YAML-Dokumenten.
Fügen Sie Folgendes zu project/dependencies.sbt hinzu:

libraryDependencies += "io.circe" %% "circe-yaml" % "0.6.1"

Wir können nun eine zweite Aufgabe erstellen, die die erste Aufgabe verwendet, um JSON aus der Typesafe-Konfiguration zu generieren und ein YAML zu erzeugen, das von Tools verwendet werden kann. YAML fängt einfach an, wird aber notorisch komplex, wenn Sie es wirklich verwenden wollen.

lazy val task2 = taskKey[String]("Converting JSON to YAML")
task2 := {
  import cats.syntax.either._
  import _root_.io.circe.yaml._
  import _root_.io.circe.yaml.syntax._
  val jsonString: String = task1.value
  val jsonAST = _root_.io.circe.parser.parse(jsonString).valueOr(throw _)

  // some options in generating YAML strings
  val yamlTwoSpaces: String = jsonAST.asYaml.spaces2
  val yamlFourSpaces: String = jsonAST.asYaml.spaces4
  val yamlPretty: String = _root_.io.circe.yaml.Printer(dropNullKeys = true,
      mappingStyle = Printer.FlowStyle.Block)
      .pretty(jsonAST)

  println(yamlPretty)

  yamlPretty
}

Parsen der Typesafe-Konfiguration

Typesafe Config kann zur einfachen Verwendung in Anwendungen in Case-Klassen geparst werden:

lazy val task1 = taskKey[Unit]("")

task1 := {
  import pureconfig._
  import com.typesafe.config._
  import scala.collection.mutable
  import scala.collection.JavaConverters._
  import pureconfig.error.ConfigReaderFailures

  case class HashKey(name: String, type: String)
  case class SortKey(name: String, type: String)
  case class DynamoDbTable(name: String, hashKey: HashKey, sortKey: Option[SortKey], stream: Option[String], rcu: Int, wcu: Int)

  val conf: Config = ConfigFactory.parseString(
    """
      |dynamodb {
      |  table1 {
      |    name = my-table-1
      |    hash-key = {
      |     name = myHashKey
      |     type = S
      |    }
      |    range-key = {
      |     name = myRangeKey
      |     type = N
      |    }
      |    stream = KEYS_ONLY
      |    rcu = 1
      |    wcu = 1
      |  }
      |  table2 {
      |    name = my-table-2
      |    hash-key = {
      |     name = myHashKey
      |     type = S
      |    }
      |    range-key = {
      |     name = myRangeKey
      |     type = N
      |    }
      |    stream = KEYS_ONLY
      |    rcu = 1
      |    wcu = 1
      |  }
      |  table3 {
      |    name = my-table-3
      |    hash-key = {
      |     name = myHashKey
      |     type = S
      |    }
      |    range-key = {
      |     name = myRangeKey
      |     type = N
      |    }
      |    stream = KEYS_ONLY
      |    rcu = 1
      |    wcu = 1
      |  }
      |}
    """.stripMargin
    )

  val dynamodb = conf.getConfig("dynamodb")
  val result: mutable.Set[Either[ConfigReaderFailures, DynamoDbTable]] = {
    dynamodb.root().keySet().asScala.map(dynamodb.getConfig).map { conf =>
      loadConfig[DynamoDbTable](conf)
    }
  }
  println(result)
}

Ein Fortschrittsbalken

Manchmal können Dinge einige Zeit in Anspruch nehmen, zum Beispiel die Bereitstellung einer Anwendung. Meistens würde Sbt einfach blockieren, bis es fertig ist, aber wäre es nicht schön, einen schönen Fortschrittsbalken zu sehen?
Basierend auf dem Code, den ich auf quaich gesehen habe, einem Scala "Serverless" Microframework für AWS Lambda, wie es von Brendan McAdams vorgestellt wurde, hat er den Code aus dem S3 Plugin übernommen, das wir wie folgt erstellen können:

/**
  * Progress bar code borrowed from https://github.com/sbt/sbt-s3/blob/master/src/main/scala/S3Plugin.scala
  */
def progressBar(percent:Int): String = {
  val b="=================================================="
  val s="                                                  "
  val p=percent/2
  val z_StringBuilder=new StringBuilder(80)
  z.append("r[")
  z.append(b.substring(0,p))
  if (p<50) {z.append("=>"); z.append(s.substring(p))}
  z.append("]   ")
  if (p<5) z.append(" ")
  if (p<50) z.append(" ")
  z.append(percent)
  z.append("%   ")
  z.mkString
}

lazy val task1 = taskKey[Unit]("")
task1 := {
  println(progressBar(0))
  println(progressBar(25))
  println(progressBar(33))
  println(progressBar(50))
  println(progressBar(66))
  println(progressBar(75))
  println(progressBar(100))
}

Es würde Folgendes ausgeben:

sbt:study-sbt> task1
[=>                                                  ]     0%
[=============>                                      ]    25%
[=================>                                  ]    33%
[==========================>                         ]    50%
[==================================>                 ]    66%
[======================================>             ]    75%
[==================================================]   100%
[success] Total time: 0 s, completed Nov 5, 2017 4:56:14 PM

Was wir brauchen, ist eine Quelle, die von 0 bis 100 zählt und die eine Funktion hat, die wir zurückrufen können:

lazy val task1 = taskKey[Unit]("Showing a progress bar")
task1 := {
  def goForIt(progress: Int => Unit): Unit = {
    def loop(x: Int): Int = {
      if (x <= 100) {
        Thread.sleep(100)
        progress(x)
        loop(x + 10)
      } else Math.min(100, x)
    }
    loop(0)
  }

  goForIt(progress => println(progressBar(progress)))
}

Normalerweise würden wir natürlich eine Art Fortschrittsanzeige registrieren, aber für den Moment reicht das.

Einstellung des Loglevels

Der logLevel von Sbt kann auf eingestellt werden:

  • sbt.util.Level.Debug: zeigt viel mehr Informationen an!
  • sbt.util.Level.Info: (Standard): ist die Standardeinstellung und zeigt eine normale Menge an Informationen an
  • sbt.util.Level.Warn: zeigt nur Warnungen an
  • sbt.util.Level.Error: zeigt nur Fehler an

Die Standardeinstellung ist sbt.util.Level.Info und die logLevel kann geändert werden:

sbt:study-sbt> set logLevel := sbt.util.Level.Debug
[info] Defining *:logLevel
[info] The new value will be used by *:evicted, *:update
[info] Reapplying settings...
[info] Set current project to study-sbt (in build file:/Users/dennis/projects/study-sbt/)

Seien Sie bereit für viele weitere Informationen!
Sie können den Loglevel wieder auf Info ändern, indem Sie das Projekt neu laden oder den Loglevel wieder auf Info setzen:

sbt:study-sbt> set logLevel := sbt.util.Level.Info
[info] Defining *:logLevel
[info] The new value will be used by *:evicted, *:update
[info] Reapplying settings...
[info] Set current project to study-sbt (in build file:/Users/dennis/projects/study-sbt/)

SBT-Typen zum Kennenlernen

Wenn Sie sich mit sbt beschäftigen, sollten Sie einen Blick auf einige Teile der Codebasis werfen:

  • (sbt-main) - sbt.Keys: Definiert alle verfügbaren Tasten. Dies ist ein guter Ausgangspunkt, da alle Tasten einen textuellen Inhalt haben, um zu lesen, was die Tasten tun, und auch eine schnelle Möglichkeit, nach Schlüsselwörtern zu suchen, wenn Sie nach einer bestimmten Funktionalität suchen.
  • (sbt-main) - sbt.Defaults: Alle Standardimplementierungen und -einstellungen für die verfügbaren Schlüssel. Dies ist ein guter Ort, um nachzusehen, wie eine Aufgabe verdrahtet ist.
  • (sbt-main) - sbt.Project: Diese Klasse definiert ein sbt-Projekt. Wenn Sie also in einer sbt-Sitzung project oder lazy val myproj = project in file(".") eingeben oder einfach nur einen Verweis auf das Projekt in einer build.sbt wünschen und irgendwo in Ihrer build.sbt project eingeben, erhalten Sie einen Verweis auf diesen Typ.
  • IO.write und IO.createDirectory sind ein guter Ausgangspunkt, um die Bibliothek zu durchsuchen.

Sbt's Design Übersicht

Das Folgende ist eine wichtige Lektüre, um das Design von Sbt zu lernen und ist Teil des Sbt-Wikis.
Die Aufgabe des reload -Befehls ist es, ein BuildStructure zu erzeugen und es in State zu speichern, damit es von zukünftigen Befehlen wie der Aufgabenausführung verwendet werden kann. BuildStructure ist der Datentyp, der alles über einen Build repräsentiert: Projekte und Beziehungen, ausgewertete Einstellungen und die Protokollierungskonfiguration. Sobald der Befehl reload über den Wert BuildStructure verfügt, speichert er ihn in State.attributes, verschlüsselt durch Keys.stateBuildStructure.
Um Informationen zwischen Befehlen weiterzugeben, ohne das Projekt neu laden zu müssen, enthält das Objekt State eine Attribut-Map. Da das State Objekt zwischen den Befehlen weitergegeben wird, ist es die ideale Methode, um Informationen zwischen Befehlen weiterzugeben. State.attributes ist eine typsichere Map. Die Schlüssel sind vom Typ AttributeKey[T] und Sie können nur Werte vom Typ T mit diesem Schlüssel verknüpfen. State verfügt über komfortable Methoden set/get, die über Erweiterungsmethoden an die zugrunde liegende Attribute-Map delegiert werden. Auf State können Sie unter anderem die Methoden get(key): Option[T], put(key, value): State, remove(key): State, update(key)(f: Option[A] => B): State und has(key): Boolean um mit der Attribut-Map zu arbeiten. Dies ist eine Möglichkeit, wie ein Befehl das Verhalten von Aufgaben ändern kann, ohne dass das Projekt neu geladen werden muss: Er setzt Attribute in State und die Aufgabe greift über die Aufgabe state auf State zu.

Projekt.Auszug

Der Aufruf Project.extract(state) ruft im Kern state.get(Keys.stateBuildStructure) auf, um die BuildStructure zurückzubekommen. Er erledigt auch noch einige andere Dinge:

  • löst eine schönere Ausnahme aus, wenn ein Projekt nicht geladen ist
  • lädt die Sitzung mit state.get(Keys.sessionSettings)
  • gibt die Sitzung und die Struktur in einem Extracted Wert zurück, was eine bessere Schnittstelle zu ihnen bietet

Session-Einstellungen

Der Datentyp SessionSettings verfolgt einige Informationen, die nicht persistent sind. Die beiden wichtigsten Informationen sind:
das aktuelle Projekt: geändert durch den Befehl project, z.B.
zusätzliche Einstellungen: hinzugefügt durch den Befehl set, z.B.
SessionSettings zeichnet nur diese Informationen auf; das Setzen dieser Werte auf ein SessionSettings-Objekt wendet die Änderungen nicht an. Insbesondere muss das Projekt neu geladen werden, damit die zusätzlichen Einstellungen wirksam werden. Beim Neuladen werden die Einstellungen auf Probleme wie Verweise auf nicht vorhandene Einstellungen geprüft und dann neu bewertet. Das Release-Plugin verfügt über eine reapply-Methode, die den richtigen Weg aufzeigt, um Einstellungen zum aktuellen Projekt hinzuzufügen.

Staatliches Management

Für die Statusverwaltung stehen Ihnen die folgenden Optionen zur Verfügung:

  • SessionVar: Die Schlüssel sind vom Typ ScopedKey[Task[T]], also TaskKeys,
  • storeAs: t1 := t1.storeAs(t1).value, auch die SessionVars verwenden,
  • keepAs: t1 := t1.keepAs(t1).value, verwenden auch die sessionVars,
  • State.attributes: Die Schlüssel sind vom Typ AttributeKey[T], so dass SettingKeys
  • sbt.IO kann java.util.Properties lesen und auf die Platte schreiben
  • java.io.ObjectOutputStream in Kombination mit sbt.IO zum Lesen oder Schreiben (nein),
  • Jawn mit [sjson-new] zum Schreiben von JSON in eine Datei in Kombination mit IO.write/read,
  • andere Lösungen, müssen diese Bibliotheken zum Klassenpfad von sbt hinzugefügt werden.

sjson-neu

sjson-new ist ein typklassenbasierter JSON-Codec. Die Bibliothek verwendet einen indirekten Ansatz, bei dem ein allgemeinerer JSON AST erstellt wird und eine 'Back-End'-JSON-Bibliothek zur Erstellung des JSON-Strings 'eingesteckt' werden kann. Die meisten 'Plug-in'-Bibliotheken bieten oft auch eine AST- und Parsing/Serialisierungs-Infrastruktur. Der offensichtlichste Nachteil bei der Verwendung eines 'proxy-basierten' Ansatzes wie bei diesen Bibliotheken ist die Aktualität der neuesten Versionen der angeschlossenen Bibliotheken.

sbt Schmuggelware

sbt-contraband, siehe die Dokumentation. Contraband ist eine Inhaltssprache für Ihre Datentypen und APIs und ermöglicht es, Datentypen im Laufe der Zeit weiterzuentwickeln. Es erzeugt entweder Java-Klassen oder Pseudo-Case-Klassen in Scala und generiert JSON-Bindungen für die Datentypen.

Play-Json

Diese Bibliothek ist für Entwickler sehr einfach zu benutzen. Fügen Sie einfach das Folgende zu Ihrem project/dependencies.sbt hinzu und schon kann es losgehen:

libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.7"

Sie können jetzt ganz einfach in/aus JSON serialisieren. Erstellen Sie die folgende Datei in project/Person.scala:

import play.api.libs.json.Json

object Person {
  implicit val format = Json.format[Person]
}
case class Person(name: String, age: Int)

und fügen Sie das Folgende in Ihre build.sbt ein:

import play.api.libs.json.Json

lazy val write = taskKey[Unit]("")
write := {
  val jsonStr: String = Json.toJson(Person("Dennis", 42)).toString
  IO.write(baseDirectory.value / "person.json", jsonStr)
  println(jsonStr)
}

lazy val read = taskKey[Unit]("")
read := {
  val person = Json.parse(IO.read(baseDirectory.value / "person.json")).as[Person]
  println(person)
}

Apache Avro und Avro4s

avro4s ist eine typklassenbasierte Bibliothek zur Serialisierung und Deserialisierung von Avro-Datensätzen mit Apache AVRO und unterstützt die Schemaentwicklung. Fügen Sie einfach das Folgende zu Ihrem project/dependencies.sbt hinzu und schon kann es losgehen:

libraryDependencies += "com.sksamuel.avro4s" %% "avro4s-core" % "1.8.0"

Sie können jetzt ganz einfach in/aus avro-Datensätzen serialisieren. Erstellen Sie die folgende Datei in project/Person.scala:

case class Person(name: String, age: Int)

und fügen Sie das Folgende in Ihre build.sbt ein:

import com.sksamuel.avro4s.{AvroInputStream, AvroOutputStream}

lazy val write = taskKey[Unit]("")
write := {
  val people = (1 to 10).map { i =>
    Person("Dennis" + i, 42 + i)
  }
  val os = AvroOutputStream.data[Person](baseDirectory.value / "person.avro")
  people.foreach(os.write)
  os.flush()
  os.close()
}

lazy val read = taskKey[Unit]("")
read := {
  val is = AvroInputStream.data[Person](baseDirectory.value / "person.avro")
  val people: List[Person] = is.iterator.toList
  is.close()
  people.foreach(println)
}

Ändern Sie nun die Fallklasse Person, indem Sie ein neues Feld 'luckyNumbers' hinzufügen:

case class Person(name: String, age: Int, luckyNumbers: Boolean = false)

Und nun read die Datei person.avro erneut. Sie haben die Schema-Evolution eingebaut!

Laden des Einstellungsskeletts

Folgendes kann zum Laden/Speichern von Einstellungen in Ihrem Plugin verwendet werden:

import com.github.dnvriend.ops.AllOps
import sbt._
import sbt.complete.DefaultParsers._
import sbt.internal.util.complete.Parser
import sjsonnew.BasicJsonProtocol._

object SettingsPluginKeys {
  lazy val users = taskKey[Seq[String]]("Get list of users")
  lazy val userName = settingKey[String]("The user name")
  lazy val printUserName = taskKey[Unit]("Shows the selected user name")
}

object SettingsPlugin extends AutoPlugin with AllOps {
  override def trigger = allRequirements

  val autoImport = SettingsPluginKeys

  import autoImport._

  def selectUserParser(state: State): Parser[String] = {
    val maybeUsers = SessionVar.load(users in Global, state)
    val strings = maybeUsers.getOrElse(Nil)
    Space ~> StringBasic.examples(strings: _*)
  }
  val selectUserNameCmd = Command("selectUserName")(selectUserParser) { (state, user) =>
    println("Selected: " + user)
    Settings.saveSettings()
    state.put(userName.key, user)
  }

  val loadSettingsCmd = Command.command("loadSettings") { state =>
    Settings.loadSettings()
    state.put(userName.key, "selected")
  }

  object Settings {
    def saveSettings(): Unit = println("Saving settings...")
    def loadSettings(): Unit = println("Loading settings...")
  }

  override def projectSettings: Seq[Def.Setting[_]] = Seq(
    users := Seq("foo", "bar", "baz", "quz"),
    users := users.storeAs(users in Global).value,
    printUserName := {
      val buildState = Keys.state.value
      val log = Keys.streams.value.log
      val name = userName.?.value.getOrElse(buildState.get(userName.key))
      log.info("You selected: " + name)
    },
    Keys.commands += loadSettingsCmd,
    Keys.commands += selectUserNameCmd,
  )
}

Fazit

Das Scala Build-Tool ist ein einfaches Build-Tool. Die Grundlagen sind Einstellungen und Aufgaben und das Erstellen von Abhängigkeiten zwischen Einstellungen und Aufgaben. Sbt ist mit Hilfe von Plugins stark erweiterbar. Das Scala Build Tool kann sowohl Java- als auch Scala-Quellcode kompilieren und bauen und ist das de-facto Build Tool für Scala-Projekte.

Verfasst von

Dennis Vriend

Contact

Let’s discuss how we can support your journey.