Установка
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.sbtDSL. Наприклад, виконується наступнеfooTask
$9c2192aea3f1db3c251d.fooTask.eval
build.sbtпід кореневим проектом - це спеціальний DSL, який допомагає визначити визначення побудови проекту Scala в project/.
І побудувати визначення проекту Scala, може мати власне визначення побудови проекту Scala project/project/і так далі. Ми кажемо, що sbt є рекурсивним .
sbt за замовчуванням паралельний
sbt будує DAG з завдань. Це дозволяє йому аналізувати залежності між завданнями та виконувати їх паралельно і навіть виконувати дедуплікацію. build.sbtDSL розроблений з урахуванням цього, що може призвести до спочатку дивовижної семантики. Як ви вважаєте, порядок виконання в наступному фрагменті?
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
Основні посилання