Монтаж
brew install sbt
или аналогичные установки sbt, которые технически состоят из
Когда вы выполняете sbt
с терминала, он фактически запускает bash-скрипт sbt launcher. Лично мне никогда не приходилось беспокоиться об этой троице, я просто использовал 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 с проектом определения сборки в путь к классам. Вы должны увидеть импорт по строкам
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
Ключевые ссылки