Instalacja
brew install sbt
lub podobne instalacje, z których technicznie rzecz biorąc składa się
Kiedy wykonujesz sbt
z terminala, faktycznie uruchamia skrypt bash programu uruchamiającego SBT. Osobiście nigdy nie musiałem martwić się o tę trójcę i po prostu używać sbt tak, jakby to była jedna rzecz.
Konfiguracja
Aby skonfigurować sbt dla określonego .sbtopts
pliku zapisu projektu w katalogu głównym projektu. Aby skonfigurować sbt w całym systemie, modyfikuj /usr/local/etc/sbtopts
. Wykonanie sbt -help
powinno podać dokładną lokalizację. Na przykład, aby dać sbt więcej pamięci jako jednorazowe wykonanie sbt -mem 4096
lub zapisać -mem 4096
w .sbtopts
lub w sbtopts
celu zwiększenia pamięci, aby zadziałało na stałe.
Struktura projektu
sbt new scala/scala-seed.g8
tworzy minimalną strukturę projektu 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
Częste polecenia
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
Niezliczone muszle
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
Definicja kompilacji to właściwy projekt Scala
To jedna z kluczowych idiomatycznych koncepcji sbt. Spróbuję wyjaśnić pytaniem. Powiedzmy, że chcesz zdefiniować zadanie SBT, które wykona żądanie HTTP za pomocą scalaj-http. Intuicyjnie możemy wypróbować następujące rozwiązaniabuild.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
...
}
Jednak będzie to błąd z informacją o braku import scalaj.http._
. Jak to możliwe, kiedy, tuż powyżej, dodaje scalaj-http
się libraryDependencies
? Co więcej, dlaczego to działa, skoro zamiast tego dodajemy zależność do project/build.sbt
?
// project/build.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
Odpowiedź jest taka, że w fooTask
rzeczywistości jest to część oddzielnego projektu Scala od twojego głównego projektu. Ten inny projekt Scala można znaleźć w project/
katalogu, który ma własny target/
katalog, w którym znajdują się jego skompilowane klasy. W rzeczywistości pod project/target/config-classes
powinna znajdować się klasa, która dekompiluje się do czegoś podobnego
object $9c2192aea3f1db3c251d extends scala.AnyRef {
lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
lazy val root : sbt.Project = { /* compiled code */ }
}
Widzimy, że fooTask
jest to po prostu członek zwykłego obiektu Scala o nazwie $9c2192aea3f1db3c251d
. Oczywiście scalaj-http
powinna to być zależność projektu definiującego, $9c2192aea3f1db3c251d
a nie zależność odpowiedniego projektu. Dlatego należy go zadeklarować w project/build.sbt
zamiast build.sbt
, ponieważ project
jest to miejsce, w którym znajduje się definicja kompilacji projekt Scala.
Aby wskazać, że definicja kompilacji to tylko kolejny projekt Scali, wykonaj sbt consoleProject
. Spowoduje to załadowanie Scala REPL z projektem definicji kompilacji w ścieżce klas. Powinieneś zobaczyć import wzdłuż linii
import $9c2192aea3f1db3c251d
Więc teraz możemy bezpośrednio współdziałać z projektem definicji kompilacji, wywołując go za pomocą właściwej Scali zamiast build.sbt
DSL. Na przykład wykonuje następujące czynnościfooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
w ramach projektu głównego jest specjalnym DSL, który pomaga zdefiniować definicję kompilacji projektu Scala w ramach project/
.
Definicja kompilacji Projekt Scala może mieć własną definicję kompilacji Projekt Scala project/project/
i tak dalej. Mówimy, że sbt jest rekurencyjny .
sbt jest domyślnie równoległy
sbt buduje DAG z zadań. Pozwala to analizować zależności między zadaniami i wykonywać je równolegle, a nawet przeprowadzać deduplikację. build.sbt
DSL został zaprojektowany z myślą o tym, co może prowadzić do początkowo zaskakującej semantyki. Jak myślisz, jaka jest kolejność wykonywania w poniższym fragmencie?
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
}
Intuicyjnie można by pomyśleć płynąć tutaj jest do pierwszego wydruku hello
potem wykonanie a
, a następnie b
zadania. Jednak w rzeczywistości oznacza to wykonać a
i b
w równolegle , a przed println("hello")
tak
a
b
hello
lub ponieważ kolejność a
i b
nie jest gwarantowana
b
a
hello
Być może paradoksalnie, w sbt łatwiej jest robić równolegle niż szeregowo. Jeśli potrzebujesz zamówień seryjnych, będziesz musiał użyć specjalnych rzeczy, takich jak Def.sequential
lub Def.taskDyn
do naśladowania zrozumienia .
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
jest podobne do
for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()
gdzie widzimy, że nie ma zależności między komponentami, podczas gdy
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
jest podobne do
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 }
to, gdzie widzimy, sum
zależy od i musi czekać na a
i b
.
Innymi słowy
- dla semantyki aplikacyjnej użyj
.value
- dla semantyki monadycznej użyj
sequential
lubtaskDyn
Rozważ inny semantycznie mylący fragment kodu w wyniku natury budowania zależności value
, gdzie zamiast
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^
musimy pisać
val x = settingKey[String]("")
x := version.value
Zwróć uwagę, że składnia .value
dotyczy relacji w DAG i nie oznacza
„podaj mi teraz wartość”
zamiast tego oznacza coś takiego
„mój rozmówca zależy najpierw ode mnie, a kiedy już wiem, jak pasuje do siebie cały DAG, będę w stanie podać mojemu rozmówcy żądaną wartość”
Więc teraz może być trochę jaśniejsze, dlaczego x
nie można jeszcze przypisać wartości; nie ma jeszcze wartości na etapie budowania relacji.
Wyraźnie widać różnicę w semantyce między właściwą Scalą a językiem DSL w build.sbt
. Oto kilka praktycznych zasad, które sprawdzają się w moim przypadku
- DAG składa się z wyrażeń typu
Setting[T]
- W większości przypadków po prostu używamy
.value
składni, a sbt zajmie się ustanowieniem relacji międzySetting[T]
- Czasami musimy ręcznie zmodyfikować część DAG i do tego używamy
Def.sequential
lubDef.taskDyn
- Gdy już uporamy się z tymi osobliwościami w składni porządkowania / relacji, możemy polegać na zwykłej semantyce Scali do budowania pozostałej logiki biznesowej zadań.
Polecenia a zadania
Polecenia są leniwym wyjściem z DAG. Za pomocą poleceń można łatwo modyfikować stan kompilacji i serializować zadania zgodnie z potrzebami. Kosztem jest to, że tracimy zrównoleglanie i deduplikację zadań dostarczanych przez DAG, którą drogą zadania powinny być preferowane. Możesz myśleć o poleceniach jako o rodzaju trwałego nagrania sesji, którą można wykonać w środku sbt shell
. Na przykład podane
vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value
rozważ wyniki następnej sesji
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
W szczególności nie dotyczy to sposobu zmiany stanu kompilacji za pomocą set x := 41
. Polecenia pozwalają np. Na trwałe nagranie powyższej sesji
commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}
Możemy również uczynić polecenie bezpiecznym, używając Project.extract
irunTask
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
}
Zakresy
Lunety wchodzą w grę, gdy próbujemy odpowiedzieć na następujące rodzaje pytań
- Jak raz zdefiniować zadanie i udostępnić je wszystkim podprojektom w kompilacji wieloprojektowej?
- Jak uniknąć zależności testowych w głównej ścieżce klas?
sbt ma wieloosiową przestrzeń zakresu, po której można nawigować za pomocą składni ukośnika , na przykład
show root / Compile / compile / scalacOptions
| | | |
project configuration task key
Osobiście rzadko muszę się martwić o zakres. Czasami chcę skompilować tylko źródła testowe
Test/compile
a może wykonać określone zadanie z określonego podprojektu bez konieczności wcześniejszego przechodzenia do tego projektu za pomocą project subprojB
subprojB/Test/compile
Myślę, że poniższe praktyczne zasady pomogą uniknąć komplikacji związanych z określaniem zakresu
- nie mają wielu
build.sbt
plików, ale tylko jeden główny w projekcie głównym, który kontroluje wszystkie inne podprojekty
- udostępniać zadania za pomocą wtyczek automatycznych
- wyodrębnij typowe ustawienia do zwykłej Scali
val
i dodaj je do każdego podprojektu
Kompilacja wieloprojektowa
Zamiast wielu plików build.sbt dla każdego podprojektu
.
├── 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
Miej jednego pana, build.sbt
który będzie rządził wszystkimi
.
├── 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
Istnieje powszechna praktyka uwzględniania wspólnych ustawień w kompilacjach obejmujących wiele projektów
zdefiniuj sekwencję wspólnych ustawień w wartości i dodaj je do każdego projektu. Mniej pojęć do nauczenia się w ten sposób.
na przykład
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)
Nawigacja projektów
projects // list all projects
project multi1 // change to particular project
Wtyczki
Pamiętaj, że definicja kompilacji to właściwy projekt Scala, który znajduje się w project/
. W tym miejscu definiujemy wtyczkę, tworząc .scala
pliki
. // 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
Oto minimalne auto plugin podproject/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
}
)
}
Zastąpienie
override def requires = plugins.JvmPlugin
powinny skutecznie włączyć wtyczkę dla wszystkich podprojektów bez konieczności wzywania wyraźnie enablePlugin
w build.sbt
.
IntelliJ i sbt
Włącz następujące ustawienie (które powinno być domyślnie włączone )
use sbt shell
pod
Preferences | Build, Execution, Deployment | sbt | sbt projects
Kluczowe odniesienia