Installazione
brew install sbt
o simili installa sbt che tecnicamente parlando consiste
Quando esegui sbt
dal 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 .sbtopts
file nella radice del progetto. Per configurare sbt modificare a livello di sistema /usr/local/etc/sbtopts
. L'esecuzione sbt -help
dovrebbe dirti la posizione esatta. Ad esempio, per dare a sbt più memoria durante l'esecuzione una tantum sbt -mem 4096
, o salvare -mem 4096
in .sbtopts
o sbtopts
affinché 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-http
a 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 fooTask
realtà 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-classes
dovrebbe 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-http
dovrebbe essere una dipendenza della definizione del progetto $9c2192aea3f1db3c251d
e non la dipendenza del progetto corretto. Quindi deve essere dichiarato in project/build.sbt
invece 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.sbt
DSL. Ad esempio, viene eseguito quanto seguefooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
under 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.sbt
DSL è 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, hello
quindi eseguire a
e quindi eseguire l' b
attività. Tuttavia questo in realtà significa eseguire a
e b
in parallelo , e prima println("hello")
ancora
a
b
hello
o perché l'ordine di a
e b
non è 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.sequential
o Def.taskDyn
per 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 sum
dipende e deve aspettare a
e b
.
In altre parole
- per la semantica applicativa , utilizzare
.value
- per la semantica monadica utilizzare
sequential
otaskDyn
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 .value
riguarda 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 x
non è 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
.value
sintassi e sbt si occuperà di stabilire una relazione traSetting[T]
- Occasionalmente dobbiamo modificare manualmente una parte di DAG e per questo utilizziamo
Def.sequential
oDef.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.extract
erunTask
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.sbt
file 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
val
e 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.sbt
per 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 .scala
file
. // 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 enablePlugin
in 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