Installazione
brew install sbt o simili installa sbt che tecnicamente parlando consiste
Quando esegui sbtdal terminale, esegue effettivamente lo script bash del lanciatore sbt. Personalmente, non mi sono mai dovuto preoccupare di questa trinità, e ho usato semplicemente sbt come se fosse una cosa sola.
Configurazione
Per configurare sbt per un particolare progetto, salvare il .sbtoptsfile nella radice del progetto. Per configurare sbt modificare a livello di sistema /usr/local/etc/sbtopts. L'esecuzione sbt -helpdovrebbe dirti la posizione esatta. Ad esempio, per dare a sbt più memoria durante l'esecuzione una tantum sbt -mem 4096, o salvare -mem 4096in .sbtoptso sbtoptsaffinché l'aumento di memoria abbia effetto in modo permanente.
Struttura del progetto
sbt new scala/scala-seed.g8 crea una struttura minima del progetto Hello World sbt
.
├── README.md // most important part of any software project
├── build.sbt // build definition of the project
├── project // build definition of the build (sbt is recursive - explained below)
├── src // test and main source code
└── target // compiled classes, deployment package
Comandi frequenti
test // run all test
testOnly // run only failed tests
testOnly -- -z "The Hello object should say hello" // run one specific test
run // run default main
runMain example.Hello // run specific main
clean // delete target/
package // package skinny jar
assembly // package fat jar
publishLocal // library to local cache
release // library to remote repository
reload // after each change to build definition
Una miriade di conchiglie
scala // Scala REPL that executes Scala language (nothing to do with sbt)
sbt // sbt REPL that executes special sbt shell language (not Scala REPL)
sbt console // Scala REPL with dependencies loaded as per build.sbt
sbt consoleProject // Scala REPL with project definition and sbt loaded for exploration with plain Scala langauage
La definizione della build è un vero e proprio progetto Scala
Questo è uno dei concetti chiave di sbt idiomatici. Proverò a spiegare con una domanda. Supponiamo che tu voglia definire un'attività sbt che eseguirà una richiesta HTTP con scalaj-http. Intuitivamente potremmo provare quanto segue all'internobuild.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
val fooTask = taskKey[Unit]("Fetch meaning of life")
fooTask := {
import scalaj.http._ // error: cannot resolve symbol
val response = Http("http://example.com").asString
...
}
Tuttavia questo errore sarà detto mancante import scalaj.http._. Come è possibile quando, proprio sopra, aggiunto scalaj-httpa libraryDependencies? Inoltre, perché funziona quando, invece, aggiungiamo la dipendenza a project/build.sbt?
// project/build.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
La risposta è che in fooTaskrealtà fa parte di un progetto Scala separato dal tuo progetto principale. Questo diverso progetto Scala può essere trovato nella project/directory che ha la sua target/directory in cui risiedono le classi compilate. In effetti, sotto project/target/config-classesdovrebbe esserci una classe che si decompila in qualcosa di simile
object $9c2192aea3f1db3c251d extends scala.AnyRef {
lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
lazy val root : sbt.Project = { /* compiled code */ }
}
Vediamo che fooTaskè semplicemente un membro di un normale oggetto Scala denominato $9c2192aea3f1db3c251d. Chiaramente scalaj-httpdovrebbe essere una dipendenza della definizione del progetto $9c2192aea3f1db3c251de non la dipendenza del progetto corretto. Quindi deve essere dichiarato in project/build.sbtinvece di build.sbt, perché projectè dove risiede la definizione di build del progetto Scala.
Per guidare il punto che la definizione di build è solo un altro progetto Scala, esegui sbt consoleProject. Questo caricherà Scala REPL con il progetto di definizione build sul classpath. Dovresti vedere un'importazione lungo le linee di
import $9c2192aea3f1db3c251d
Quindi ora possiamo interagire direttamente con il progetto di definizione build chiamandolo con Scala vera e propria invece di build.sbtDSL. Ad esempio, viene eseguito quanto seguefooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbtunder root project è un DSL speciale che aiuta a definire la definizione di build del progetto Scala in project/.
E la definizione di build del progetto Scala, può avere la propria definizione di build del progetto Scala in project/project/e così via. Diciamo che sbt è ricorsivo .
sbt è parallelo per impostazione predefinita
sbt compila il DAG dalle attività. Ciò gli consente di analizzare le dipendenze tra le attività e di eseguirle in parallelo e persino di eseguire la deduplicazione. build.sbtDSL è progettato tenendo presente questo aspetto, il che potrebbe portare a una semantica inizialmente sorprendente. Quale pensi che sia l'ordine di esecuzione nel seguente frammento?
def a = Def.task { println("a") }
def b = Def.task { println("b") }
lazy val c = taskKey[Unit]("sbt is parallel by-default")
c := {
println("hello")
a.value
b.value
}
Intuitivamente si potrebbe pensare che il flusso qui sia prima di stampare, helloquindi eseguire ae quindi eseguire l' battività. Tuttavia questo in realtà significa eseguire ae bin parallelo , e prima println("hello") ancora
a
b
hello
o perché l'ordine di ae bnon è garantito
b
a
hello
Forse paradossalmente, in sbt è più facile fare parallelo che seriale. Se hai bisogno di un ordine seriale, dovrai usare cose speciali come Def.sequentialo Def.taskDynper emulare per la comprensione .
def a = Def.task { println("a") }
def b = Def.task { println("b") }
lazy val c = taskKey[Unit]("")
c := Def.sequential(
Def.task(println("hello")),
a,
b
).value
è simile a
for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()
dove vediamo che non ci sono dipendenze tra i componenti, mentre
def a = Def.task { println("a"); 1 }
def b(v: Int) = Def.task { println("b"); v + 40 }
def sum(x: Int, y: Int) = Def.task[Int] { println("sum"); x + y }
lazy val c = taskKey[Int]("")
c := (Def.taskDyn {
val x = a.value
val y = Def.task(b(x).value)
Def.taskDyn(sum(x, y.value))
}).value
è simile a
def a = Future { println("a"); 1 }
def b(v: Int) = Future { println("b"); v + 40 }
def sum(x: Int, y: Int) = Future { x + y }
for {
x <- a
y <- b(x)
c <- sum(x, y)
} yield { c }
dove vediamo sumdipende e deve aspettare ae b.
In altre parole
- per la semantica applicativa , utilizzare
.value
- per la semantica monadica utilizzare
sequentialotaskDyn
Considera un altro frammento semanticamente confuso come risultato della natura di costruzione delle dipendenze di value, dove invece di
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^
dobbiamo scrivere
val x = settingKey[String]("")
x := version.value
Nota che la sintassi .valueriguarda le relazioni nel DAG e non significa
"dammi il valore adesso"
invece significa qualcosa di simile
"il mio chiamante dipende innanzitutto da me e, una volta che so come si integra l'intero DAG, sarò in grado di fornire al mio chiamante il valore richiesto"
Quindi ora potrebbe essere un po 'più chiaro il motivo per cui xnon è ancora possibile assegnare un valore; non è ancora disponibile alcun valore nella fase di costruzione della relazione.
Possiamo chiaramente vedere una differenza nella semantica tra Scala vera e propria e il linguaggio DSL in formato build.sbt. Ecco alcune regole pratiche che funzionano per me
- DAG è composto da espressioni di tipo
Setting[T]
- Nella maggior parte dei casi usiamo semplicemente la
.valuesintassi e sbt si occuperà di stabilire una relazione traSetting[T]
- Occasionalmente dobbiamo modificare manualmente una parte di DAG e per questo utilizziamo
Def.sequentialoDef.taskDyn
- Una volta risolte queste stranezze sintatiche di ordinamento / relazione, possiamo fare affidamento sulla solita semantica Scala per costruire il resto della logica di business dei compiti.
Comandi vs compiti
I comandi sono una via d'uscita pigra dal DAG. Utilizzando i comandi è facile modificare lo stato di compilazione e serializzare le attività come desideri. Il costo è che perdiamo la parallelizzazione e la deduplicazione delle attività fornite da DAG, in questo modo le attività dovrebbero essere la scelta preferita. Puoi pensare ai comandi come a una sorta di registrazione permanente di una sessione che si potrebbe fare all'interno sbt shell. Ad esempio, dato
vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value
considera l'output della sessione successiva
sbt:root> x
[info] 13
sbt:root> show f
[info] 14
sbt:root> set x := 41
[info] Defining x
[info] The new value will be used by f
[info] Reapplying settings...
sbt:root> show f
[info] 42
In particolare, non il modo in cui mutiamo lo stato di compilazione con set x := 41. I comandi ci consentono, ad esempio, di effettuare una registrazione permanente della sessione precedente
commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}
Possiamo anche rendere il comando indipendente dai tipi usando Project.extracterunTask
commands += Command.command("cmd") { state =>
val log = state.log
import Project._
log.info(x.value.toString)
val (_, resultBefore) = extract(state).runTask(f, state)
log.info(resultBefore.toString)
val mutatedState = extract(state).appendWithSession(Seq(x := 41), state)
val (_, resultAfter) = extract(mutatedState).runTask(f, mutatedState)
log.info(resultAfter.toString)
mutatedState
}
Ambiti
Gli ambiti entrano in gioco quando proviamo a rispondere ai seguenti tipi di domande
- Come definire l'attività una volta e renderla disponibile a tutti i sottoprogetti nella build multi-progetto?
- Come evitare di avere dipendenze di test sul classpath principale?
sbt ha uno spazio di scoping multiasse che può essere esplorato utilizzando la sintassi della barra , ad esempio,
show root / Compile / compile / scalacOptions
| | | |
project configuration task key
Personalmente, raramente mi trovo a dovermi preoccupare dell'ambito. A volte voglio compilare solo sorgenti di test
Test/compile
o forse eseguire una particolare attività da un particolare sottoprogetto senza dover prima passare a quel progetto con project subprojB
subprojB/Test/compile
Penso che le seguenti regole pratiche aiutano a evitare complicazioni di scoping
- non hanno più
build.sbtfile ma solo un unico master sotto il progetto principale che controlla tutti gli altri sottoprogetti
- condividere attività tramite plug-in automatici
- scomporre le impostazioni comuni in Scala semplice
vale aggiungerla esplicitamente a ogni sottoprogetto
Build multiprogetto
Invece di più file build.sbt per ogni sottoprogetto
.
├── README.md
├── build.sbt // OK
├── multi1
│ ├── build.sbt // NOK
│ ├── src
│ └── target
├── multi2
│ ├── build.sbt // NOK
│ ├── src
│ └── target
├── project // this is the meta-project
│ ├── FooPlugin.scala // custom auto plugin
│ ├── build.properties // version of sbt and hence Scala for meta-project
│ ├── build.sbt // OK - this is actually for meta-project
│ ├── plugins.sbt // OK
│ ├── project
│ └── target
└── target
Avere un unico maestro build.sbtper governarli tutti
.
├── README.md
├── build.sbt // single build.sbt to rule theme all
├── common
│ ├── src
│ └── target
├── multi1
│ ├── src
│ └── target
├── multi2
│ ├── src
│ └── target
├── project
│ ├── FooPlugin.scala
│ ├── build.properties
│ ├── build.sbt
│ ├── plugins.sbt
│ ├── project
│ └── target
└── target
Esiste una pratica comune di fattorizzare le impostazioni comuni nelle build multi-progetto
definire una sequenza di impostazioni comuni in una val e aggiungerle a ciascun progetto. Meno concetti da imparare in questo modo.
per esempio
lazy val commonSettings = Seq(
scalacOptions := Seq(
"-Xfatal-warnings",
...
),
publishArtifact := true,
...
)
lazy val root = project
.in(file("."))
.settings(settings)
.aggregate(
multi1,
multi2
)
lazy val multi1 = (project in file("multi1")).settings(commonSettings)
lazy val multi2 = (project in file("multi2")).settings(commonSettings)
Navigazione progetti
projects // list all projects
project multi1 // change to particular project
Plugin
Ricorda che la definizione di build è un vero e proprio progetto Scala che risiede sotto project/. È qui che definiamo un plugin creando .scalafile
. // directory of the (main) proper project
├── project
│ ├── FooPlugin.scala // auto plugin
│ ├── build.properties // version of sbt library and indirectly Scala used for the plugin
│ ├── build.sbt // build definition of the plugin
│ ├── plugins.sbt // these are plugins for the main (proper) project, not the meta project
│ ├── project // the turtle supporting this turtle
│ └── target // compiled binaries of the plugin
Ecco un plug-in automatico minimo inproject/FooPlugin.scala
object FooPlugin extends AutoPlugin {
object autoImport {
val barTask = taskKey[Unit]("")
}
import autoImport._
override def requires = plugins.JvmPlugin // avoids having to call enablePlugin explicitly
override def trigger = allRequirements
override lazy val projectSettings = Seq(
scalacOptions ++= Seq("-Xfatal-warnings"),
barTask := { println("hello task") },
commands += Command.command("cmd") { state =>
"""eval println("hello command")""" :: state
}
)
}
L'override
override def requires = plugins.JvmPlugin
dovrebbe abilitare efficacemente il plugin per tutti i sottoprogetti senza dover chiamare esplicitamente enablePluginin build.sbt.
IntelliJ e sbt
Abilita la seguente impostazione (che dovrebbe essere abilitata per impostazione predefinita )
use sbt shell
sotto
Preferences | Build, Execution, Deployment | sbt | sbt projects
Riferimenti chiave