安装
brew install sbt 或类似的安装 sbt,从技术上讲包括
当您从终端执行sbt 时,它实际上会运行 sbt 启动器 bash 脚本。就我个人而言,我从来不用担心这三位一体,只是把 sbt 当作一个单一的东西来使用。
配置
要为特定项目配置 sbt,请将 .sbtopts 文件保存在项目的根目录中。要配置 sbt 系统范围修改 /usr/local/etc/sbtopts。执行sbt -help 应该会告诉您确切的位置。例如,一次性执行 sbt -mem 4096 给 sbt 更多内存,或者将 -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 任务,该任务将使用 scalaj-http 执行 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 只是一个名为$9c2192aea3f1db3c251d 的常规Scala 对象的成员。显然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,它帮助定义project/ 下的构建定义Scala 项目。
以及构建定义Scala项目,project/project/下可以有自己的构建定义Scala项目等等。我们说sbt is recursive。
sbt 默认是并行的
sbt 在任务之外构建DAG。这允许它分析任务之间的依赖关系并并行执行它们,甚至执行重复数据删除。 build.sbt DSL 的设计考虑到了这一点,这可能会导致最初令人惊讶的语义。你觉得下面sn-p的执行顺序是什么?
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 任务。然而这实际上意味着在 parallel 和 before println("hello") so
中执行
a 和
b
a
b
hello
或者因为a和b的顺序无法保证
b
a
hello
也许自相矛盾的是,在 sbt 中并行比串行更容易。如果您需要串行订购,您将不得不使用特殊的东西,例如 Def.sequential 或 Def.taskDyn 来模拟 for-comprehension。
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
- 对于 monadic 语义使用
sequential 或taskDyn
考虑 another 在语义上混淆 sn-p,因为 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 还不能被赋值;在关系建立阶段还没有可用的价值。
我们可以在build.sbt 中清楚地看到 Scala 本身和 DSL 语言之间的语义差异。以下是一些对我有用的经验法则
- 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 有一个多轴作用域空间,可以使用slash syntax 进行导航,例如,
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
在多项目构建中有factoring out common settings 的常见做法
在 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下的最小auto plugin
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
应该有效地为所有子项目启用插件,而不必在build.sbt中显式调用enablePlugin。
IntelliJ 和 sbt
请启用以下设置(应该由default启用)
use sbt shell
下
Preferences | Build, Execution, Deployment | sbt | sbt projects
主要参考文献