安装
brew install sbt
或类似的安装 sbt 从技术上讲包括
当您sbt
从终端执行时,它实际上运行 sbt 启动器 bash 脚本。就个人而言,我从来不用担心这三位一体,只需将 sbt 当作一个单一的东西来使用。
配置
.sbtopts
为项目根目录下的特定项目保存文件配置 sbt 。配置 sbt 系统范围的 modify /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 任务,该任务将使用 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
...
}
但是,这会出错,说 missing 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
root 项目下是一个特殊的 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
andb
的顺序不能保证
b
a
hello
也许自相矛盾的是,在 sbt 中并行比串行更容易。如果您需要串行订购,则必须使用特殊的东西,例如Def.sequential
orDef.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
- 对于一元语义使用
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 本身和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
主要参考资料