任务
描述
任务是 sbt 构建中的工作单元。任务可以 compile 代码、增量运行 test、package JAR 文件,或执行构建所需的任何其他操作。
任务的一个有趣之处在于,它们支持并行编程,类似于 Future[A] 和 IO[A]。Future 和 IO 通常通过 for 推导式组合,而 sbt 通过直接风格的 build.sbt DSL 提供任务的 结构化并发(structured concurrency)。
lazy val intTaskA = taskKey[Int]("")
lazy val intTaskB = taskKey[Int]("")
lazy val intTaskC = taskKey[Int]("")
intTaskA := 1
intTaskB := 2
// intTaskA 和 intTaskB 将并行运行
intTaskC := {
val a = intTaskA.value
val b = intTaskB.value
a + b
}
Def.Initialized[Task[A]]
在底层,包括任务键在内的所有任务都被类型化为 Def.Initialized[Task[A]]。
Def.Initialized[A]。这与设置的类型相同,用于在加载时构建设置/任务图。之所以如此命名,是因为它表示A的已初始化值。sbt.Task[A]。这是一个表示可重复异步计算的数据类型,是由 sbt 任务引擎执行的部分。
将这两者嵌套在一起,任务可被视为设置/任务图中的一个节点,用于产生 A 的异步计算。
设置与任务
设置和任务都会产生值,但两者之间有一个重大区别:
- 设置在构建加载时求值。任务按需执行,通常是响应用户从 shell 中调用。
功能
任务系统具有以下几个特点:
- 通过与设置系统集成,任务可以像设置一样方便地添加、删除和修改。
- 任务会产生值。其他任务可以在任务定义中对其调用
value来访问该任务的值。 - 任务的值会根据其输入自动缓存。详情参见缓存任务。
- 输入任务使用解析器组合子定义参数语法,从而与命令一样支持灵活的语法和 Tab 补全。
- 可以动态更改任务图的结构。任务可以根据另一个任务的结果注入执行图。
- 有多种方式处理任务失败,类似于
try/catch/finally。 - 每个任务都可以访问自己的 Logger,默认情况下,该 Logger 会以比最初打印到屏幕更详细的级别保留该任务的日志。
以下各节将详细讨论这些功能。
定义任务
Hello world 示例
从命令行运行 sbt hello 来调用该任务。运行 sbt tasks 可查看此任务列表。
定义键
要声明一个新任务,需定义一个 TaskKey 类型的 lazy val:
lazy val sampleTask = taskKey[Int]("A sample task.")
val 的名称用于在 build.sbt 和命令行中引用该任务。传递给 taskKey 方法的字符串是任务的描述。传递给 taskKey 的类型参数(此处为 Int)是任务产生的值的类型。
我们为示例再定义几个键:
lazy val intTask = taskKey[Int]("An int task")
lazy val stringTask = taskKey[String]("A string task")
这些示例本身就是 build.sbt 中的有效条目。
实现任务
定义键之后,实现任务主要分为三个部分:
- 确定任务所需的设置和其他任务,它们是任务的输入。
- 根据这些输入定义实现任务的代码。
- 确定任务所在的作用域。
这些部分的组合方式与设置的各部分组合方式相同。
定义基本任务
任务使用 := 定义
lazy val intTask = taskKey[Int]("An int task")
lazy val stringTask = taskKey[String]("A string task")
intTask := 1 + 2
stringTask := Def.uncached {
sys.props("user.name")
}
带输入的任务
以其他任务或设置为输入的任务同样使用 := 定义。输入的值通过 value 方法引用。该方法是一种特殊语法,只能在定义任务时调用,例如作为 := 的参数。以下示例定义了一个将 intTask 的结果加一并返回的任务。
sampleTask := intTask.value + 1
多个设置的处理方式类似:
stringTask := {
val x = sampleTask.value
val i = intTask.value
s"sample: $x, int: $i"
}
任务作用域
与设置一样,任务也可以在特定作用域中定义。例如,Compile 和 Test 作用域各有单独的 compile 任务。
在以下示例中,Test/sampleTask 使用了 Compile/intTask 的结果:
Test / sampleTask := (Compile / intTask).value * 3
提醒一下,中缀方法的优先级由方法名决定。
- 赋值方法的优先级最低。这些方法的名称以
=结尾, 但!=、<=、>=以及以=开头的名称除外。 - 以字母开头的方法优先级次之。
- 以符号开头且不属于第 1 类的方法优先级最高。 (该类别根据具体的首字符进一步细分,详见 Scala 规范。)
因此,前面的示例等价于以下写法:
(Test / sampleTask).:=((Compile / intTask).value * 3)
分离实现
任务的实现可以与绑定分离。例如,基本的分离定义如下:
// 定义一个新的独立任务实现
lazy val intTaskImpl: Initialize[Task[Int]] =
Def.cachedTask { sampleTask.value - 3 }
// 将实现绑定到特定键
intTask := intTaskImpl.value
请注意,无论何时使用 .value,都必须在任务定义内部,例如在上述 Def.task 中或作为 := 的参数。
修改现有任务
一般情况下,通过将前一个任务声明为输入来修改任务。
// 初始定义
intTask := 3
// 引用前一个定义的覆盖定义
intTask := intTask.value + 1
通过不将前一个任务声明为输入来完全覆盖任务。以下示例中的每个定义都完全覆盖前一个定义。即运行 intTask 时只会打印 #3。
intTask := {
println("#1")
3
}
intTask := {
println("#2")
5
}
intTask := {
println("#3")
sampleTask.value - 3
}
错误处理
任务中的错误
要在任务中表示错误状态,请抛出异常。例如:
sbt 的任务引擎会捕获并处理该错误,而不会导致 sbt 崩溃:
sbt:aaa> intTask
[error] stack trace is suppressed; run last intTask for the full output
[error] (intTask) boom
[error] elapsed time: 0 s, cache 0%, 1 error
可以使用 last intTask 查看堆栈跟踪。
result
result 方法创建一个新任务,返回原任务的完整 Result[A1] 值。Result 的结构与类型为 A1 的任务结果的 Either[Incomplete, A1] 相同,即有两个子类型:
Result.Inc:在失败时包装IncompleteResult.Value:在成功时包装任务的结果。
因此,由 result 创建的任务无论原始任务成功还是失败都会执行。
lazy val intTask = taskKey[Int]("An int task")
intTask := sys.error("boom")
intTask := Def.uncached {
intTask.result.value match
case Result.Inc(inc: Incomplete) =>
println("Ignoring failure: " + inc)
3
case Result.Value(v) =>
println("Using successful result: " + v)
v
}
这将重新连接原始 intTask 定义,使得原始任务失败时打印异常并返回常量 3,成功时打印并返回其值。
failure
failure 方法创建一个新任务,当原始任务未能正常完成时返回 Incomplete 值。如果原始任务成功,则新任务失败。
lazy val intTask = taskKey[Int]("An int task")
intTask := sys.error("boom")
intTask := Def.uncached {
println("Ignoring failure: " + intTask.failure.value)
3
}
这将重新连接 intTask,使其打印原始异常并返回常量 3。
任务的进阶功能
Streams:按任务记录日志
按任务记录日志是名为 Streams 的任务特定数据通用系统的一部分。
要使用 Streams,获取 streams 任务的值。可以通过 log 方法获取 Logger:
foo := {
val s = streams.value
s.log.debug("Saying hi...")
s.log.info("Hello!")
}
可以按特定任务的作用域来限定日志设置的范围:
foo / logLevel := Level.Debug
foo / traceLevel := 5
要获取任务的最后一次日志输出,使用 last 命令:
$ last foo
[debug] Saying hi...
[info] Hello!
日志的保存详细程度由 persistLogLevel 和 persistTraceLevel 设置控制。last 命令根据这些级别显示已记录的内容。这些级别不影响已记录的信息。
条件任务
当任务的顶层由 if 表达式构成时,会自动创建条件任务:
bar := {
if number.value < 0 then negAction.value
else if number.value == 0 then zeroAction.value
else posAction.value
}
与常规(应用函子式)任务组合不同,条件任务会像 if 表达式自然期望的那样延迟对 then 子句和 else 子句的求值。这在 Def.taskDyn { ... } 中已经可以实现,但与动态任务不同,条件任务可以与 inspect 命令配合使用。
使用 Def.taskDyn 的动态计算
利用任务的结果来决定下一步要求值的任务有时很有用,这可以通过 Def.taskDyn 实现。
taskDyn 的结果称为动态任务,因为它在运行时引入依赖关系。taskDyn 方法支持与 Def.task 和 := 相同的语法,区别在于返回的是任务而非普通值。
例如,
lazy val stringTask = taskKey[String]("A string task")
lazy val intTask = taskKey[Int]("An int task")
lazy val foo = taskKey[Int]("foo")
val dynamic = Def.taskDyn {
// 根据 `stringTask` 的值决定要求值的内容
if stringTask.value == "dev" then
// 创建 dev 模式任务:仅在 stringTask 的值为 "dev" 时求值
Def.task {
3
}
else
// 创建生产任务:仅在 stringTask 的值不为 "dev" 时求值
Def.task {
intTask.value + 5
}
}
stringTask := "test"
intTask := 1
foo := Def.uncached {
val num = dynamic.value
println(s"number selected was $num")
num
}
foo 唯一的静态依赖是 stringTask。对 intTask 的依赖仅在非 dev 模式下引入。
使用 Def.sequential
Def.sequential 是用于定义半顺序任务的辅助函数,与动态任务类似,但更易定义。为演示顺序任务,我们创建一个名为 compilecheck 的自定义任务,依次运行 Compile / compile 和 Compile / scalastyle。
lazy val compilecheck = taskKey[Unit]("compile and then scalastyle")
lazy val root = (project in file("."))
.settings(
Compile / compilecheck := Def.sequential(
Compile / compile,
(Compile / scalastyle).toTask("")
).value
)
若需完全顺序执行,请改用命令。
从多个作用域获取值
从多个作用域获取值的表达式通用形式如下:
<setting-or-task>.all(<scope-filter>).value
all 方法被隐式添加到任务和设置中,接受用于选择 Scopes 的 ScopeFilter。结果类型为 Seq[A],其中 A 是键的底层类型。
一个常见场景是获取所有子项目的源文件以便一次性处理,例如将其传递给 scaladoc。我们想获取值的任务是 sources,目标是所有非根项目的 Compile 配置中的值,如下所示:
lazy val core = project
lazy val util = project
val filter = ScopeFilter(inProjects(core, util), inConfigurations(Compile))
lazy val root = rootProject
.settings(
sources := {
// 每个 sources 定义的类型为 Seq[File],
// 最终得到 Seq[Seq[File]],再将其展平为 Seq[File]
val allSources: Seq[Seq[File]] = sources.all(filter).value
allSources.flatten
}
)