Установка
brew install sbt
або подібне встановлює sbt, з якого технічно кажучи складається
Після запуску sbt
з терміналу він фактично запускає скрипт bash запуску sbt. Особисто мені ніколи не доводилося турбуватися про цю трійцю, а просто використовувати sbt так, ніби це одна річ.
Конфігурація
Для налаштування sbt для конкретного проекту збережіть .sbtopts
файл у корені проекту. Для налаштування sbt модифікуйте по всій системі /usr/local/etc/sbtopts
. Виконання sbt -help
повинно вказати точне місце розташування. Наприклад, щоб дати sbt більше пам’яті при одноразовому виконанні sbt -mem 4096
, або зберегти -mem 4096
в .sbtopts
або sbtopts
для збільшення пам’яті, щоб набути чинності назавжди.
Структура проекту
sbt new scala/scala-seed.g8
створює мінімальну структуру проекту 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
Часті команди
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
Безліч снарядів
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
Визначення збірки - належний проект Scala
Це одна з ключових ідіоматичних концепцій sbt. Спробую пояснити питанням. Скажіть, що ви хочете визначити завдання sbt, яке буде виконувати HTTP-запит із scalaj-http. Інтуїтивно ми можемо спробувати наступне всерединіbuild.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
...
}
Однак це буде помилка, сказавши про відсутність import scalaj.http._
. Як це можливо , коли ми, прямо вище, додають scalaj-http
до libraryDependencies
? Крім того, чому це працює, коли замість цього ми додаємо залежність project/build.sbt
?
// project/build.sbt
libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
Відповідь - fooTask
це фактично частина окремого проекту Scala від вашого основного проекту. Цей різний проект Scala можна знайти в project/
каталозі, який має власний target/
каталог, де розташовані складені класи. Насправді під ним project/target/config-classes
повинен бути клас, який декомпілюється на щось подібне
object $9c2192aea3f1db3c251d extends scala.AnyRef {
lazy val fooTask : sbt.TaskKey[scala.Unit] = { /* compiled code */ }
lazy val root : sbt.Project = { /* compiled code */ }
}
Ми бачимо, що fooTask
це просто член звичайного об’єкта Scala з назвою $9c2192aea3f1db3c251d
. Очевидно, scalaj-http
що залежність проекту повинна визначати, $9c2192aea3f1db3c251d
а не залежність належного проекту. Отже, це потрібно оголосити project/build.sbt
замість build.sbt
, тому що project
саме тут визначається проект Scala для визначення збірки.
Виконати точку, що визначає побудову, - це лише черговий проект Scala, виконайте його sbt consoleProject
. Це завантажить Scala REPL проектом визначення складання на classpath. Ви повинні бачити імпорт уздовж рядків
import $9c2192aea3f1db3c251d
Тож тепер ми можемо безпосередньо взаємодіяти з проектом визначення побудови, називаючи його власне Scala замість build.sbt
DSL. Наприклад, виконується наступнеfooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbt
під кореневим проектом - це спеціальний DSL, який допомагає визначити визначення побудови проекту Scala в project/
.
І побудувати визначення проекту Scala, може мати власне визначення побудови проекту Scala project/project/
і так далі. Ми кажемо, що sbt є рекурсивним .
sbt за замовчуванням паралельний
sbt будує DAG з завдань. Це дозволяє йому аналізувати залежності між завданнями та виконувати їх паралельно і навіть виконувати дедуплікацію. build.sbt
DSL розроблений з урахуванням цього, що може призвести до спочатку дивовижної семантики. Як ви вважаєте, порядок виконання в наступному фрагменті?
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
}
Інтуїтивно можна думати, що потік тут - це спочатку надрукувати, hello
потім виконати a
, а потім виконати b
завдання. Однак це на самому ділі означає , що виконати a
і b
в паралель , і перш , ніж println("hello")
так
a
b
hello
або тому, що порядок a
і b
не гарантується
b
a
hello
Можливо, парадоксально, в sbt легше зробити паралельний, ніж серійний. Якщо вам потрібно серійне замовлення, вам доведеться використовувати спеціальні речі, як-от Def.sequential
або Def.taskDyn
наслідувати для розуміння .
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
подібний до
for {
h <- Future(println("hello"))
a <- Future(println("a"))
b <- Future(println("b"))
} yield ()
там, де ми бачимо, немає залежності між компонентами, в той час як
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
подібний до
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 }
де ми бачимо, sum
залежить і має чекати a
і b
.
Іншими словами
- для прикладної семантики, використання
.value
- для використання монадійної семантики
sequential
абоtaskDyn
Розглянемо ще один семантично заплутаний фрагмент внаслідок характеру побудови залежності value
, де замість
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
val x = version.value
^
ми повинні писати
val x = settingKey[String]("")
x := version.value
Зверніть увагу, що синтаксис .value
стосується відносин у DAG і не означає
"дай мені значення зараз"
натомість це означає щось подібне
"мій абонент спочатку залежить від мене, і як тільки я дізнаюся, як цілий DAG поєднується разом, я зможу надати моєму абоненту потрібне значення"
Отже, тепер може бути трохи зрозуміліше, чому ще x
не можна призначити значення; ще немає значення на етапі побудови відносин.
Ми можемо чітко бачити різницю в семантиці між власне Scala та мовою DSL в build.sbt
. Ось кілька правил великих пальців, які працюють на мене
- DAG складається з виразів типу
Setting[T]
- У більшості випадків ми просто використовуємо
.value
синтаксис, а sbt подбає про встановлення зв’язку між нимиSetting[T]
- Інколи нам доводиться вручну підлаштовувати частину DAG і для цього ми використовуємо
Def.sequential
абоDef.taskDyn
- Після того, як ці синтатичні дива впорядкованості / взаємозв'язку будуть усунені, ми можемо спиратися на звичайну семантику Scala для побудови решти бізнес-логіки завдань.
Команди проти завдань
Команди - ледачий вихід із DAG. Використовуючи команди, можна легко вимкнути завдання збирання та задати послідовність за вашим бажанням. Вартість полягає в тому, що ми втрачаємо паралелізацію та дедуплікацію завдань, передбачених DAG, який спосіб завдання повинен бути кращим вибором. Ви можете думати команди як певний постійний запис сеансу, який можна зробити всередині sbt shell
. Наприклад, дано
vval x = settingKey[Int]("")
x := 13
lazy val f = taskKey[Int]("")
f := 1 + x.value
розглянемо результат наступного сеансу
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
Зокрема, не про те, як ми мутуємо стан збірки set x := 41
. Команди дозволяють нам зробити постійний запис вищевказаного сеансу, наприклад
commands += Command.command("cmd") { state =>
"x" :: "show f" :: "set x := 41" :: "show f" :: state
}
Ми також можемо зробити команду безпечною для типу, використовуючи Project.extract
таrunTask
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
}
Області застосування
Області застосування грають, коли ми намагаємось відповісти на такі питання
- Як визначити завдання один раз і зробити його доступним для всіх підпроектів у багатопроектній збірці?
- Як уникнути тестових залежностей від основного класу?
sbt має багатовісний простір масштабування, який можна переміщувати за допомогою синтаксису косою рисою , наприклад,
show root / Compile / compile / scalacOptions
| | | |
project configuration task key
Особисто мені рідко доводиться турбуватися про сферу застосування. Іноді хочеться скласти просто тестові джерела
Test/compile
або, можливо, виконати певне завдання з певного підпроекту без попереднього переходу до цього проекту project subprojB
subprojB/Test/compile
Я думаю, що наступні правила роботи допомагають уникнути ускладнень
- не мають декількох
build.sbt
файлів, а лише один головний під кореневим проектом, який контролює всі інші підпроекти
- ділитися завданнями через автоматичні плагіни
- виділити загальні параметри на звичайну Scala
val
та чітко додати її до кожного підпроекту
Багатопроектна збірка
Вмісто декількох файлів build.sbt для кожного підпроекту
.
├── 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
Майте єдиного господаря, build.sbt
щоб керувати ними всіма
.
├── 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
Існує загальна практика розбивати загальні параметри у складі багатопроектних проектів
визначте послідовність загальних параметрів у val і додайте їх до кожного проекту. Менше понять, щоб навчитися саме так.
наприклад
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)
Навігація проектів
projects // list all projects
project multi1 // change to particular project
Плагіни
Пам'ятайте, визначення визначення - це правильний проект Scala, який знаходиться в project/
. Тут ми визначаємо плагін, створюючи .scala
файли
. // 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
Ось мінімальний автоматичний плагін підproject/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
}
)
}
Переоцінка
override def requires = plugins.JvmPlugin
повинен ефективно включати плагін для всіх підпроектів, не вимагаючи явного дзвінка enablePlugin
в build.sbt
.
IntelliJ і sbt
Увімкніть наступне налаштування (яке дійсно повинно бути включене за замовчуванням )
use sbt shell
під
Preferences | Build, Execution, Deployment | sbt | sbt projects
Основні посилання