安装
brew install sbt
或从技术上讲由以下内容构成的类似安装sbt
sbt
从终端执行时,它实际上运行sbt启动器bash脚本。就我个人而言,我不必担心这种三位一体,而只需将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任务,该任务将使用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项目的一部分。可以在project/
目录下找到该不同的Scala项目,该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,它有助于在project/
。下定义构建定义Scala项目。
而构建定义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
模拟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属性和DSL语言中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
关键参考