文章目录
SBT -> Simple Build Tool -> Maven
https://www.scala-sbt.org/1.x/docs/zh-cn/index.html
sbt相当于java世界的maven,它可以
- 构建scala项目
- 持续的编译与测试
- 可以重用Maven或者ivy的repository进行依赖管理
SBT基础篇
Hello,SBT
默认情况下,sbt会用和启动自身相同版本的Scala来构建项目。你可以通过执行sbt run 来运行项目或者通过sbt console进入Scala REPL。sbt console已经帮你设置好项目的classpath.
项目的基本构建设置都放在项目根目录build.sbt文件里。如果你准备将你的项目打包成一个jar,在build.sbt中至少要写上name和version,如下
lazy val root = (project in file("."))
.settings(
name := "hello",
version := "1.0",
scalaVersion := "2.12.8"
)
你可以在项目名/project/build.properties文件中强制制定一个版本的sbt
sbt.version=1.2.8
目录结构
源代码
sbt和maven的默认源文件目录结构是一样的(所有的路径都是相对于基础目录–项目名目录的)
src/
main/
resources/
<files to include in main jar here>
scala/
<main Scala sources>
java/
<main Java sources>
test/
resources
<files to include in test jar here>
scala/
<test Scala sources>
java/
<test Java sources>
sbt构建定义文件
基础目录中有build.sbt,其他的sbt文件在project子目录中。project目录可以包含.scala文件,这些文件最后会和.sbt文件合并共同构成完整的构建定义。project/中的.sbt文件不等同于项目基础目录中的.sbt文件
build.sbt
project/
Build.scala
构建产品
构建出来的文件(编译的classes,打包的jars,托管文件,caches和文档)默认写在target目录中。
配置版本管理
你的.gitignore文件(或其他版本控制系统等同的文件)应该包含:
target/
运行
交互模式
在你的项目目录下运行sbt不跟任何参数:
$sbt
执行sbt不跟任何命令行参数将会进入交互模式。输入compile编译,输入run启动程序,输入exit或者 Ctrl+D (Unix)或者 Ctrl+Z (Windows)可以退出交互模式。
批处理模式
以空格为分隔符制定参数,对于接受参数的sbt命令,将命令和参数用引号引起来一起传给sbt:
$ sbt clean compile "testOnly TestA TestB"
在这个例子中,testOnly 有两个参数 TestA 和 TestB。这个命令会按顺序执行(clean, compile, 然后 testOnly)。
持续构建和测试
在命令前面加上前缀 ~ 后,每当有一个或多个源文件发生变化时就会自动运行该命令。例如,在交互模式下尝试:
> ~ compile
常用的命令
clean 删除所有生成的文件 (在 target 目录下)。
compile 编译源文件(在 src/main/scala 和 src/main/java 目录下)。
test 编译和运行所有测试。
console 进入到一个包含所有编译的文件和所有依赖的 classpath 的 Scala 解析器。输入 :quit, Ctrl+D (Unix),或者 Ctrl+Z (Windows) 返回到 sbt。
run <参数>* 在和 sbt 所处的同一个虚拟机上执行项目的 main class。
package 将 src/main/resources 下的文件和 src/main/scala 以及 src/main/java 中编译出来的 class 文件打包成一个 jar 文件。
help <命令> 显示指定的命令的详细帮助信息。如果没有指定命令,会显示所有命令的简介。
reload 重新加载构建定义(build.sbt, project/.scala, project/.sbt 这些文件中定义的内容)。在修改了构建定义文件之后需要重新加载。
tab键自动补全子集
命令历史记录
! 显示历史记录命令帮助。
!! 重新执行前一条命令。
!: 显示所有之前的命令。
!:n 显示之前的最后 n 条命令。
!n 执行 !: 命令显示的结果中下标为 n 的命令。
!-n 执行从该命令往前数第 n 条命令。
!string 执行最近执行过的以 string 打头的命令。
!?string 执行最近执行过的包含 string 的命令。
.sbt构建定义
什么是构建定义
sbt在检查项目和处理构建定义文件之后,行程一个project定义
在build.sbt中你可以创建一个本目录的Project工程定义,like this:
lazy val root = (project in file(".")
每一个工程对应一个不可变的映射表(immutable map)(一些键值对的集合)来描述工程
例如,一个叫做name的key,映射到一个字符串的值,即项目的名称。
构建定义文件不会直接影响sbt的map.
取而代之的是构建定义会创建一个类型为Setting[T]的庞大的对象列表,T是映射表中值(value)的类型。一个Setting描述的是一次对映射表(map)的转化,像增加一个新的键值对或者追加到一个已经存在的value上。(在函数式编程关于使用不可变数据结构和值的思想中,一次转换返回一个新的map —— 它不会就地更新旧的 map。)
lazy val root = (project in file("."))
.settings(
name := "hello"
)
这个Setting[String]会通过增加(或者替换)name键的值为“hello”来对map做一次转换。转换后的map成为sbt新的map.
为了创建这个map,sbt会先对所有设置的列表进行排序,这样对同一个key的改变可以放在一起操作,而且如果value依赖于其他的key,会先处理其他被依赖的key.然后,sbt会对Settings排好序的列表进行遍历,按顺序把每一项都应用到map中。
总结:一个构建定义是一个Project,拥有一个类型为 Setting[T] 的列表,Setting[T] 是会影响到 sbt 保存键值对的 map 的一种转换,T 是每一个 value 的类型。
如何在build.sbt中定义设置
build.sbt定义了一个Project,它持有一个名为settings的scala表达式列表,如下:
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.8"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "hello"
)
每一项 Setting 都定义为一个 Scala 表达式。在 settings 中的表达式是相互独立的,而且它们仅仅是表达式,不是完整的 Scala 语句。
这些表达式可以用 val,lazy val,def 声明。 build.sbt 不允许使用顶层的 object 和 class。它们必须写到 project/ 目录下作为完整的 Scala 源文件。
在左边,name, version 和 scalaVersion 都是 键(keys)。一个键(key)就是一个 SettingKey[T],TaskKey[T] 或者 InputKey[T] 的实例,T 是期望的 value 的类型。 key 的类别将在下面讲解。
键(Keys)有一个返回 Setting[T] 的 := 方法。
键(key)name 上的 := 方法会返回一个 Setting,在这里特指 Setting[String]。String 也出现在 name 自身的类型 SettingKey[String] 中。 在这个例子中,返回的 Setting[String] 是一个在 sbt 的 map 中增加或者替换键为 name 的转换,赋值为 “hello”。
如果你使用了错误类型的 value,构建定义会编译不通过
键(Keys)
类型(Types)
有三种类型的 key:
SettingKey[T]:一个 key 对应一个只计算一次的 value(这个值在加载项目的时候计算,然后一直保存着)。
TaskKey[T]:一个 key 对应一个称之为 task 的 value,每次都会重新计算,可能存在潜在的副作用。
InputKey[T]:一个 key
对应一个可以接收命令行参数的 task。
内置的 Keys
内置的 keys 实际上是对象 Keys 的字段。build.sbt 会隐式包含 import sbt.Keys._,所以可以通过 name 取到 sbt.Keys.name。
自定义 Keys
lazy val hello = taskKey[Unit]("一个 task 示例")
这里我们用事实说明了 .sbt 文件除了可以包含设置(settings)外,还可以包含 vals 和 defs。所有这些定义都会在设置(settings)之前被计算而跟它们在文件里定义的位置无关。 vals 和 defs 必须以空行和设置(settings)分隔。
注意: 通常,使用 lazy val 而不是 val 可以避免初始化顺序的问题。
Task vs Setting keys
TaskKey[T] 是用来定义 task 的。Tasks 就是像 compile 或者 package 这样的操作。它们可能返回 Unit(Unit 在 Scala 中表示 void),或者可能返回 task 相关的返回值, 例如 package 就是一个类型为 TaskKey[File] 的 task, 它的返回值是其生成的 jar 文件。
每当你执行一个 task,例如在 sbt 命令行中输入 compile,sbt 将会对涉及到的每个 task 恰好执行一次。
sbt 描述项目的 map 会将设置(setting)保存为固定的字符串,比如像 name;但是它不得不保存 task 的可执行代码,比如 compile — 即使这段可执行的代码最终返回一个字符串,它也需要每次都重新执行。
一个给定的 key 总是指向一个 task 或者 一个普通的设置(setting)。 也就是说,“taskiness” (是否每次都重新执行)是 key 的一个属性(property),而不是一个值(value)。
build.sbt中的引入
你可以将 import 语句放在 build.sbt 的顶部;它们可以不用空行分隔。
默认引入
import sbt._
import Keys._
bare .sbt 构建定义
bare .sbt 构建定义由一个 Setting[_] 表达式的列表组成,而不是定义 Project。
name := "hello"
version := "1.0"
scalaVersion := "2.12.8"
添加依赖库
有两种方式添加第三方的依赖。一种是将 jar 文件 放入 lib/(非托管的依赖)中,另一种是在 build.sbt 中添加托管的依赖,像这样:
val derby = "org.apache.derby" % "derby" % "10.4.1.3"
ThisBuild / organization := "com.example"
ThisBuild / scalaVersion := "2.12.8"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val root = (project in file("."))
.settings(
name := "hello",
libraryDependencies += derby
)
就是像这样添加版本为 10.4.1.3 的 Apache Derby 库作为依赖。
key libraryDependencies 包含两个方面的复杂性:+= 方法而不是 :=,第二个就是 % 方法。+= 方法是将新的值追加该 key 的旧值后面而不是替换它,这将在 更多设置 中介绍。% 方法是用来从字符串构造 Ivy 模块 ID 的,将在 库依赖 中介绍。
Scope
每一个key可以在多个上下文中关联一个值,每个上下文诚挚为“scope”
sbt 生成一个 map 来处理描述项目的 settings 列表,这个 map 中的 key 就是 scope 下的 key。构建定义中定义的每一个 setting(例如在 build.sbt 中)都有一个 scope 下的 key。
Scope轴
Scope 轴 是一种类型,该类型的每个实例都能定义自己的 scope(也就是说,每个实例的 key 可以有自己唯一的值)。
有三种类型的 scope 轴:
Projects
Configurations
Tasks
通过Project轴划分Scope轴
如果你将 多个项目 放在同一个构建中,每个项目需要有属于自己的 settings。也就是说,keys 可以根据项目被局限在不同的上下文中。
Project 轴可以设置成构建全局的,因此一个 setting 可以应用到全局构建而不是单个项目。当一个项目没有定义项目层级的 setting 的时候,构建层级的 setting 通常作为默认的设置
通过Configuration轴划分Scope
通过 Task 轴划分 Scope
全局 Scope
代理
如果在一个 scope 中某一个 key 没有关联的值,那么该 key 就是未定义的。
对于每一个 scope,sbt 有由其他 scope 构成的替代选项的搜索路径。通常,如果一个 key 在特定的 scope 下没有关联的值,sbt 会尝试从更加一般的 scope(例如 Global scope 或者构建全局 scope)中获取值。
这个特性允许你一旦在更加一般的 scope 中设置了某一项设置的值之后,使得多个特定的 scope 能够继承该值。
你可以像下面这样用 inspect 命令查看一个 key 的替代选项的查找路径或者“代理”。往下看。
在构建定义中涉及 scope
如果你创建的 build.sbt 中有一个bare key,它的作用于将是当前的 project 下,configuration 和 task 均为 Global:
lazy val root = (project in file("."))
.settings(
name := "hello"
)
启动 sbt 并且执行 inspect name 可以看到它由 {file:/home/hp/checkout/hello/}default-aea33a/*:name 提供,也就是说,project 是 {file:/home/hp/checkout/hello/}default-aea33a, configuration 是 *(表示全局),task 没有显示出来(实际上也是全局)。
Keys 会调用一个重载的 in 方法设置 scope。传给 in 方法的参数可以是任何 scope 轴的实例。比如说,你可以将 name 局限在 Compile configuration 中,尽管没有真实的理由要这样做:
name in Compile := "hello"
或者你可以把 name 局限在 packageBin task 中(没有什么意义!仅仅是个例子):
name in packageBin := "hello"
或者你可以把 name 局限在多个 scope 轴中,例如在 Compile configuration 的 packageBin task 中:
name in (Compile, packageBin) := "hello"
或者你可以用 Global 表示所有的轴:
name in Global := "hello"
(name in Global 隐式的把 scope 轴的值 Global 转换为 scope 所有轴的值均为 Global;task 和 configuration 默认是 Global,因此这里的效果是将 project 设置成 Global, 也就是说,定义了 /:name 而不是 {file:/home/hp/checkout/hello/}default-aea33a/*:name)
如果你之前不熟悉 Scala,提醒一下:in 和 := 仅仅是方法,不是魔法,理解这点很重要。Scala 让你用一种更好的方式编写它们,但是你也可以用 Java 的风格:
name.in(Compile).:=("hello")
毫无理由使用这种丑陋的语法,但是它阐明这实际上是方法。
更多的设置
- := 通过 := 创建的 Setting 会往转换之后新的 map 中放入一个固定的常量。例如,如果你通过 name := “hello” 对 map 做一次转换,新的 map 中 key name 就保存着一个字符串 “hello”。
- += 会追加单个元素到列表中。如果 SettingKey[T] 中的 T 是一个列表,你就可以往列表中追加而不是替换
- ++= 会连接两个列表。
依赖于其他 key 的值计算值
引用另一个 task 或者 setting 的值只需要调用它们各自的 value 方法。该 value 方法比较特殊而且只能在 :=,+= 或者 ++= 方法的参数上调用。
追加依赖:+= 和 ++=
当追加到一个已经存在的 setting 或者 task 时可以使用另一些 key,就像它们可以通过 := 赋值一样。例如,比方说你有一个以项目名称命名的覆盖率报告,而且你想在每次清除文件的时候都清除它:
cleanFiles += file("coverage-report-" + name.value + ".txt")
库依赖
可以通过下面这两种方式添加库依赖:
非托管依赖 为放在 lib 目录下的 jar 文件
托管依赖 配置在构建定义中,并且会自动从仓库(repository)中下载
非托管依赖
托管依赖
libraryDependencies Key
可以像这样定义一个依赖,其中 groupId, artifactId 和 revision 都是字符串:
libraryDependencies += groupID % artifactID % revision
当然,你也可以通过 ++= 一次将所有依赖作为一个列表添加:
libraryDependencies ++= Seq(
groupID % artifactID % revision,
groupID % otherID % otherRevision
)
通过 %% 方法获取正确的 Scala 版本,sbt 会在 工件名称中加上项目的 Scala 版本号
libraryDependencies += "org.scala-tools" % "scala-stm_2.11" % "0.3"
等价于
libraryDependencies += "org.scala-tools" %% "scala-stm" % "0.3"
Ivy 修正
groupID % artifactID % revision 中的 revision 不需要是一个固定的版本号。Ivy 能够根据你指定的约束选择一个模块的最新版本。你指定 “latest.integration”,“2.9.+” 或者 “[1.0,)”,而不是 一个固定的版本号
解析器
不是所有的依赖包都放在同一台服务器上,sbt 默认使用标准的 Maven2 仓库。如果你的依赖不在默认的仓库中,你需要添加 resolver 来帮助 Ivy 找到它。
通过以下形式添加额外的仓库:
resolvers += name at location
在两个字符串中间有一个特殊的 at。
例如:
resolvers += “Sonatype OSS Snapshots” at “https://oss.sonatype.org/content/repositories/snapshots”
resolvers key 在 Keys 中像这样定义:
val resolvers = settingKeySeq[Resolver]
at 方法通过两个字符串创建了一个 Resolver 对象。
sbt 会搜索你的本地 Maven 仓库如果你将它添加为一个仓库:
resolvers += “Local Maven Repository” at “file://”+Path.userHome.absolutePath+"/.m2/repository"
或者,为了方便起见:
resolvers += Resolver.mavenLocal
参见 解析器 获取更多关于定义其他类型的仓库的内容。
覆写默认的解析器
resolvers 不包含默认的解析器,仅仅通过构建定义添加额外的解析器。
sbt 将 resolvers 和一些默认的仓库组合起来构成 externalResolvers。
然而,为了改变或者移除默认的解析器,你需要覆写externalResolvers 而不是 resolvers。
Per-configuration dependencies
通常会有依赖只被测试代码使用(在 src/test/scala 中,通过 Test configuration 编译)而没有在主应用中使用。
如果你想要一个依赖只在 Test configuration 的 classpath 中出现而不是 Compile configuration,像这样添加 % “test”:
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" % "test"
也可能也会像这样使用类型安全的 Test configuration:
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" % Test
现在,如果你在 sbt 的命令提示行里输入 show compile:dependencyClasspath,你不应该看到 derby jar。但是如果你输入 show test:dependencyClasspath, 你应该在列表中看到 derby jar。
通常,测试相关的依赖,如 ScalaCheck, Specs2 和 ScalaTest 将会被定义为 % “test”。
多项目构建
多项目
将多个相关的项目定义在一个构建中是很有用的,尤其是如果它们依赖另一个,而且你倾向于一起修改它们。
每个子项目在构建中都有它们自己的源文件夹,当打包时生成各自的 jar 文件,而且通常和其他的项目一样运转。
通过声明一个类型为 Project 的 lazy val 定义一个项目,例如:
lazy val util = project
lazy val core = project
val 的名称被用作项目的 ID 和基目录名。该 ID 用于在命令行中引用该项目。基目录可能通过 in 方法改变。例如,下面是一个更加显示的方式来实现前一个例子:
lazy val util = project.in(file("util"))
lazy val core = project in file("core")
公共设定
要跨多个项目提取公共设置,请创建一个名为commonSettings的序列,并在每个项目上调用settings方法。
lazy val commonSettings = Seq(
organization := "com.example",
version := "0.1.0",
scalaVersion := "2.12.8"
)
lazy val core = (project in file("core"))
.settings(
commonSettings,
// other settings
)
lazy val util = (project in file("util"))
.settings(
commonSettings,
// other settings
)
依赖
Aggregation
lazy val root = (project in file("."))
.aggregate(util, core)
.settings(
aggregate in update := false
)
[...]
Classpath 依赖
一个项目可能依赖另一个项目的代码。这是通过添加 dependsOn 方法来实现的。例如,如果 core 在 classpath 中需要 util,你将这样定义 core:
lazy val core = project.dependsOn(util)
现在 core 中的代码可以使用 util 的类。在编译时也会在两个项目之间创建顺序;在编译 core 之前,util 必须被更新和编译过。
为了依赖多个项目,像这样 dependsOn(bar, baz) 给 dependsOn 多个参数。
configuration 粒度的 classpath 依赖
foo dependsOn(bar) 表示 foo 中的 compile configuration 依赖于 bar 中的 compile configuration。你可以显示的写成这样:dependsOn(bar % “compile->compile”)。
“compile->compile” 中的 -> 表示 “depends on“,所以 “test->compile” 表示 foo 中的 test configuration 将依赖于 bar 中的 compile configuration。
省略 ->config 部分暗示 ->compile,所以 dependsOn(bar % “test”) 表示 foo 中的 test configuration 依赖于 bar 中的 Compile configuration。
一个实用的声明 “test->test” 表示 test 依赖于 test。例如,这样允许你将测试工具代码放在 bar/src/test/scala 中,然后在 foo/src/test/scala 中使用这些代码,
对于一个依赖你可以有多个 configuration,以分号分隔,例如:dependsOn(bar % “test->test;compile->compile”)。
默认的 root 项目
交互式引导项目
[IJ]sbt:finance-report> projects
[info] In file:/E:/workspace/finance-report/
[info] api
[info] * finance-report
[info] service
通用代码
在一个 .sbt 文件中的定义对于其他的 .sbt 文件不可见。为了在不同的 .sbt 文件中共享代码,在构建根目录下的 project/ 目录下定义一个或多个 Scala 文件。
插件
声明一个插件
如果你的项目在 hello 目录下,而且你正在往构建定义中添加一个 sbt-site 插件,创建 hello/project/site.sbt 并且通过传递插件的 Ivy 模块 ID 声明插件依赖 给 addSbtPlugin:
addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.7.0")
如果你添加 sbt-assembly,像下面这样创建 hello/project/assembly.sbt :
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")
不是所有的插件都在同一个默认的仓库中,而且一个插件的文档会指导你添加能够找到它的仓库:
resolvers += Resolver.sonatypeRepo("public")
插件通常提供设置将它添加到项目并且开启插件功能。这些将在下一小节描述。
启用和禁用自动插件
一个插件能够声明它自己的设置被自动添加到构建定义中去,在这种情况下你不需要为添加它做任何事情。
作为 0.13.5 版本的 sbt,有一个新的特性叫自动插件,它能够在自动的、安全的、确保所有依赖都在项目里的前提下开启插件。很多自动插件应该能够自动开启,然而有些却需要显式开启。
如果你正在使用一个需要显示开启的自动插件,那么你需要添加这样的代码到你的 build.sbt 文件:
lazy val util = (project in file(“util”))
.enablePlugins(FooPlugin, BarPlugin)
.settings(
name := “hello-util”
)
enablePlugins 方法允许项目显式定义它们需要使用的自动插件。
项目也可以使用 disablePlugins 方法排除掉一些插件。例如,如果我们希望能够从 util 中移除 IvyPlugin 插件的设置,我们将 build.sbt 修改如下:
lazy val util = (project in file("util"))
.enablePlugins(FooPlugin, BarPlugin)
.disablePlugins(plugins.IvyPlugin)
.settings(
name := "hello-util"
)
自动插件会在文档中说明是否需要显示的开启。如果你对一个项目中开启了哪些插件好奇,只需要在 sbt 命令行中执行 plugins 命令。
例如:
> plugins
In file:/home/jsuereth/projects/sbt/test-ivy-issues/
sbt.plugins.IvyPlugin: enabled in scala-sbt-org
sbt.plugins.JvmPlugin: enabled in scala-sbt-org
sbt.plugins.CorePlugin: enabled in scala-sbt-org
sbt.plugins.JUnitXmlReportPlugin: enabled in scala-sbt-org
这里, plugins 的输出显示 sbt 默认的插件都被开启了。sbt 默认的设置通过3个插件提供:
CorePlugin: 提供对 task 的核心并行控制。
IvyPlugin: 提供发布、解析模块的机制。
JvmPlugin: 提供编译、测试、执行、打包 Java/Scala 项目的机制。
另外,JUnitXmlReportPlugin 提供对生成 junit-xml 的试验性支持。
老的非自动的插件通常需要显示的添加设置,以致于多项目构建可以有不同的项目类型。插件的文档会指出如何配置它,但是特别是对于老的插件,这包含添加对插件必要的基本设置和自定义。
例如,对于 sbt-site 插件,为了在项目中开启它,需要创建包含如下内容的 site.sbt 文件来。
site.settings
如果构建定义了多个项目,往项目中直接添加如下内容替而代之:
// 在 `util` 项目中不使用 site 插件
lazy val util = (project in file("util"))
// 在`core` 项目中开启 site 插件
lazy val core = (project in file("core"))
.settings(site.settings)
自定义设置和任务
定义一个键
val scalaVersion = settingKey[String]("scala的版本")
val clean = taskKey[Unit]("删除构建产生的文件,包括生成的 source 文件,编译的类和任务缓存。")
键的构造函数有两个字符串参数:键的名称( “scalaVersion” )和文档字符串( “用于构建工程的scala的版本。 ” )。
还记得 .sbt 构建定义中,类型 T 在 SettingKey[T] 中表示的设置的值的类型。类型 T 在 TaskKey [T] 中指示任务的结果的类型。 在 .sbt 构建定义中,一个设置有一个固定的值,直到项目重新加载。任务会在每一个“任务执行”(用户在交互输入中或在batch模式下输入一个命令)被重新计算。
键可以在定义在.sbt 构建定义,.scala 文件或一个自动插件中。任何在启用的自动插件的autoImport对象的 val 将被自动导入 到你的 .sbt 文件。
执行任务
val sampleStringTask = taskKey[String]("A sample string task.")
val sampleIntTask = taskKey[Int]("A sample int task.")
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.8"
lazy val library = (project in file("library"))
.settings(
sampleStringTask := System.getProperty("user.home"),
sampleIntTask := {
val sum = 1 + 2
println("sum: " + sum)
sum
}
)
任务的执行语义
以sampeIntTask为例,任务体中的每一行应严格地一个接一个被取值。这就是顺序语义:
sampleIntTask := {
val sum = 1 + 2 // first
println("sum: " + sum) // second
sum // third
}
在现实中,JVM可能内联sum为3,但任务可观察到的行为仍将与严格地一个接一个被执行完全相同。
现在假设我们定义了另外两个的自定义任务startServer和stopServer,并修改sampeIntTask,如下所示:
val startServer = taskKey[Unit]("start server")
val stopServer = taskKey[Unit]("stop server")
val sampleIntTask = taskKey[Int]("A sample int task.")
val sampleStringTask = taskKey[String]("A sample string task.")
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.8"
lazy val library = (project in file("library"))
.settings(
startServer := {
println("starting...")
Thread.sleep(500)
},
stopServer := {
println("stopping...")
Thread.sleep(500)
},
sampleIntTask := {
startServer.value
val sum = 1 + 2
println("sum: " + sum)
stopServer.value // THIS WON'T WORK
sum
},
sampleStringTask := {
startServer.value
val s = sampleIntTask.value.toString
println("s: " + s)
s
}
)
从sbt交互式提示符中运行sampleIntTask将得到如下结果:
> sampleIntTask
stopping...
starting...
sum: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:00:00 PM
不同于普通的Scala方法调用,调用任务的value方法将不被严格取值。相反,他们只是充当占位符来表示sampleIntTask依赖于startServer和stopServer任务。当你调用sampleIntTask时,sbt的任务引擎将:
在对sampleIntTask取值前对依赖任务取值(偏序)
如果依赖任物是相互独立的,尝试并行取值(并行)
每次命令执行,每个任务依赖项将被评估且仅被评估一次(去重)
任务依赖项去重
为证明这最后一点,我们可以从 sbt 交互式提示符运行 sampleStringTask。
> sampleStringTask
stopping...
starting...
sum: 3
s: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:30:00 PM
因为sampleStringTask依赖于startServer和sampleIntTask两个任务,而sampleIntTask也依赖于startServer任务,它作为任务依赖出现了两次。如果这是一个普通的 Scala 方法调用,它会被计算两次,但由于任务的依赖项被标记为value类型,它将只被计算一次。
如果我们不做重复任务相关项的去重,则当我们执行test时最终会编译测试源代码很多次,因为compile in Test作为test in Test的依赖项出现了很多次。
清理任务
直接使用Scala
总结
sbt: 核心概念
Scala 基础。不可否认,熟悉 Scala 语法非常有帮助。Programming in Scala,Scala 的作者写的非常好的介绍。
.sbt 构建定义
你的构建定义是一个大的 Setting 对象列表,sbt 使用 Setting 转换之后的键值对执行 task。
为了创建 Setting,在一个 key 上调用其中的一个方法::=,+= 或者 ++=。
没有可变的状态,至于转换;例如,一个 Setting 将 sbt 的键值对集合转换成一个新的集合。不会就地改变任何代码。
每一个设置都有一个特定类型的值,由 key 决定。
tasks 是特殊的设置,通过 key 产生 value 的计算在每次出发 task 的时候都会重新执行。Non-task 计算只会在构建定义的第一次加载时执行。
Scopes
每一个 key 都可能有多个 value,按照 scope 划分。
scope 会用三个轴:configuration,project,task。
scope 允许你按项目、按 task、按 configuration 有不同的行为。
一个 configuration 是一种类型的构建,例如 Compile 或者 Test。
project 轴也支持 “构建全局” scope。
scopes 回滚或 代理 到更通用的 scope。
将大部分配置放在 build.sbt 中,但是用 .scala 构建定义文件定义类和更大的 task 实现。
构建定义是一个 sbt 项目,来自于项目目录。
插件是对构建定义的扩展
通过在 addSbtPlugin方法在project/plugins.sbt中添加插件。(不是在项目基目录下的 build.sbt 中)。
sbt源代码 https://github.com/sbt/sbt