任务和设置在入门指南中介绍,你可能想先阅读。此页面包含更多详细信息和背景信息,旨在作为参考。
设置和任务都生成值,但它们之间存在两个主要区别
任务系统有几个功能
value
来访问任务的值。try/catch/finally
。这些功能将在以下各节中详细讨论。
build.sbt
:
lazy val hello = taskKey[Unit]("Prints 'Hello World'")
hello := println("hello world!")
从命令行运行“sbt hello”以调用任务。运行“sbt tasks”以查看此任务列表。
要声明一个新任务,请定义一个类型为 TaskKey
的延迟值
lazy val sampleTask = taskKey[Int]("A sample task.")
val
的名称在 Scala 代码和命令行中引用任务时使用。传递给 taskKey
方法的字符串是任务的描述。传递给 taskKey
的类型参数(这里为 Int
)是任务生成的值的类型。
我们将为示例定义另外几个键
lazy val intTask = taskKey[Int]("An int task")
lazy val stringTask = taskKey[String]("A string task")
这些示例本身是 build.sbt
中的有效条目,或者可以作为序列的一部分提供给 Project.settings
(请参阅.scala 构建定义)。
实现任务后,定义任务主要分为三个部分
然后将这些部分组合起来,就像组合设置的部分一样。
使用 :=
定义任务
intTask := 1 + 2
stringTask := System.getProperty("user.name")
sampleTask := {
val sum = 1 + 2
println("sum: " + sum)
sum
}
如引言所述,任务按需进行评估。例如,每次调用 sampleTask
时,它都会打印总和。如果用户名在两次运行之间发生了变化,stringTask
将在这些单独的运行中取不同的值。(在一次运行中,每个任务最多评估一次。)相反,设置在项目加载时评估一次,并在下一次重新加载之前固定。
将其他任务或设置作为输入的任务也使用 :=
定义。通过 value
方法引用输入的值。此方法是特殊语法,只能在定义任务时调用,例如在 :=
的参数中。以下定义一个将 intTask
生成的值加 1 并返回结果的任务。
sampleTask := intTask.value + 1
多个设置的处理方式类似
stringTask := "Sample: " + sampleTask.value + ", int: " + intTask.value
与设置一样,任务可以在特定范围内定义。例如,compile
和 test
范围分别有单独的 compile
任务。任务的范围与设置的定义相同。在以下示例中,Test/sampleTask
使用 Compile/intTask
的结果。
Test / sampleTask := (Compile / intTask).value * 3
提醒一下,中缀方法的优先级由方法的名称决定,后缀方法的优先级低于中缀方法。
=
结尾,但 !=
、<=
、>=
和以 =
开头的名称除外。以符号开头的名称且不包含在内的名称的优先级最高
因此,前面的示例等效于以下示例
(Test / sampleTask).:=( (Compile / intTask).value * 3 )
此外,以下的括号是必需的
helloTask := { "echo Hello" ! }
如果没有它们,Scala 会将该行解释为 ( helloTask.:=("echo Hello") ).!
而不是我们想要的 helloTask.:=( "echo Hello".! )
。
任务的实现可以与绑定分离。例如,一个基本的单独定义看起来像
// Define a new, standalone task implemention
lazy val intTaskImpl: Initialize[Task[Int]] =
Def.task { sampleTask.value - 3 }
// Bind the implementation to a specific key
intTask := intTaskImpl.value
请注意,无论何时使用 .value
,它都必须位于任务定义中,例如在上面的 Def.task
中或作为 :=
的参数。
在一般情况下,通过将先前任务声明为输入来修改任务。
// initial definition
intTask := 3
// overriding definition that references the previous definition
intTask := intTask.value + 1
通过不将先前任务声明为输入来完全覆盖任务。以下示例中的每个定义都完全覆盖了上一个定义。也就是说,当运行 intTask
时,它只会打印 #3
。
intTask := {
println("#1")
3
}
intTask := {
println("#2")
5
}
intTask := {
println("#3")
sampleTask.value - 3
}
从多个范围获取值的表达式的通用形式是
<setting-or-task>.all(<scope-filter>).value
注意! 确保将 ScopeFilter
赋值为 val
!这是 .all
宏的实现细节要求。
all
方法隐式添加到任务和设置中。它接受一个 ScopeFilter
,它将选择 Scopes
。结果的类型为 Seq[T]
,其中 T
是键的底层类型。
一个常见的场景是获取所有子项目的源,以便一次性处理所有源,例如将它们传递给 scaladoc。我们要获取其值的 task 是 sources
,我们希望在所有非根项目和 Compile
配置中获取这些值。这看起来像
lazy val core = project
lazy val util = project
val filter = ScopeFilter( inProjects(core, util), inConfigurations(Compile) )
lazy val root = project.settings(
sources := {
// each sources definition is of type Seq[File],
// giving us a Seq[Seq[File]] that we then flatten to Seq[File]
val allSources: Seq[Seq[File]] = sources.all(filter).value
allSources.flatten
}
)
下一节描述了构建 ScopeFilter
的各种方法。
一个基本的 ScopeFilter
是通过 ScopeFilter.apply
方法构建的。此方法使用 Scope
的部分的过滤器创建 ScopeFilter
:ProjectFilter
、ConfigurationFilter
和 TaskFilter
。最简单的案例是显式指定部分的值
val filter: ScopeFilter =
ScopeFilter(
inProjects( core, util ),
inConfigurations( Compile, Test )
)
如果未指定任务过滤器,如上面的示例中,则默认为选择没有特定任务的范围(全局)。类似地,未指定的配置过滤器将选择全局配置中的范围。项目过滤器通常应该是显式的,但如果未指定,则将使用当前项目上下文。
示例显示了基本的 inProjects
和 inConfigurations
方法。本节描述了构建 ProjectFilter
、ConfigurationFilter
或 TaskFilter
的所有方法。这些方法可以分为四组
inProjects
, inConfigurations
, inTasks
)inGlobalProject
, inGlobalConfiguration
, inGlobalTask
)inAnyProject
, inAnyConfiguration
, inAnyTask
)inAggregates
, inDependencies
)有关详细信息,请参阅 API 文档。
ScopeFilters
可以使用 &&
、||
、--
和 -
方法组合。
a && b
选择与 a 和 b 都匹配的范围。a || b
选择与 a 或 b 匹配的范围。a -- b
选择与 a 匹配但不与 b 匹配的范围。-b
选择不与 b 匹配的范围。例如,以下代码选择 core
项目的 Compile
和 Test
配置以及 util
项目的全局配置的范围。
val filter: ScopeFilter =
ScopeFilter( inProjects(core), inConfigurations(Compile, Test)) ||
ScopeFilter( inProjects(util), inGlobalConfiguration )
all
方法适用于设置(类型为 Initialize[T]
的值)和任务(类型为 Initialize[Task[T]]
的值)。它返回一个设置或任务,提供一个 Seq[T]
,如表所示。
目标 | 结果 |
---|---|
Initialize[T] | Initialize[Seq[T]] |
Initialize[Task[T]] | Initialize[Task[Seq[T]]] |
这意味着 all
方法可以与构建任务和设置的方法组合。
某些范围可能未定义设置或任务。在这种情况下,?
和 ??
方法可以提供帮助。它们都定义在设置和任务上,并指示在键未定义时该怎么办。
? | 对于底层类型为 T 的设置或任务,它不接受任何参数,并返回类型为 Option[T] 的设置或任务(分别)。如果设置/任务未定义,则结果为 None;如果已定义,则结果为 Some[T],其中包含该值。 |
?? | 对于底层类型为 T 的设置或任务,它接受类型为 T 的参数,如果设置/任务未定义,则使用此参数。 |
以下人为的示例将最大错误设置为当前项目的所有聚合的最大值。
// select the transitive aggregates for this project, but not the project itself
val filter: ScopeFilter =
ScopeFilter( inAggregates(ThisProject, includeRoot=false) )
maxErrors := {
// get the configured maximum errors in each selected scope,
// using 0 if not defined in a scope
val allVersions: Seq[Int] =
(maxErrors ?? 0).all(filter).value
allVersions.max
}
all
的目标是任何任务或设置,包括匿名任务或设置。这意味着可以在不为每个范围定义新的任务或设置的情况下,一次获取多个值。一个常见的用例是将获取的每个值与其来自的项目、配置或完整范围配对。
resolvedScoped
:提供完整的封闭 ScopedKey(它是一个范围 + AttributeKey[_]
)。thisProject
:提供与该范围关联的 Project(在全局和构建级别未定义)。thisProjectRef
:提供上下文的 ProjectRef(在全局和构建级别未定义)。configuration
:提供上下文的 Configuration(全局配置未定义)。例如,以下代码定义了一个任务,用于打印定义了 sbt 插件的非 Compile 配置。这可能用于识别配置不正确的构建(或者不识别,因为这是一个相当人为的示例)。
// Select all configurations in the current project except for Compile
lazy val filter: ScopeFilter = ScopeFilter(
inProjects(ThisProject),
inAnyConfiguration -- inConfigurations(Compile)
)
// Define a task that provides the name of the current configuration
// and the set of sbt plugins defined in the configuration
lazy val pluginsWithConfig: Initialize[Task[ (String, Set[String]) ]] =
Def.task {
( configuration.value.name, definedSbtPlugins.value )
}
checkPluginsTask := {
val oddPlugins: Seq[(String, Set[String])] =
pluginsWithConfig.all(filter).value
// Print each configuration that defines sbt plugins
for( (config, plugins) <- oddPlugins if plugins.nonEmpty )
println(s"$config defines sbt plugins: ${plugins.mkString(", ")}")
}
本节中的示例使用上一节中定义的任务键。
按任务日志记录器是用于特定于任务的数据(称为流)的更通用系统的一部分。这允许控制堆栈跟踪的详细程度,并分别为每个任务记录日志,以及调用任务的最后一次日志记录。任务还可以访问他们自己的持久化二进制或文本数据。
要使用流,请获取 streams
任务的值。这是一个特殊的任务,它为定义的任务提供一个 TaskStreams 实例。此类型提供了对命名二进制和文本流、命名日志记录器和默认日志记录器的访问。默认的 Logger(这是最常用的方面)通过 log
方法获取。
myTask := {
val s: TaskStreams = streams.value
s.log.debug("Saying hi...")
s.log.info("Hello!")
}
您可以通过特定任务的范围对日志记录设置进行范围限定。
myTask / logLevel := Level.Debug
myTask / traceLevel := 5
要获取任务的最后一次日志记录输出,请使用 last
命令。
$ last myTask
[debug] Saying hi...
[info] Hello!
使用 persistLogLevel
和 persistTraceLevel
设置控制日志记录的持久化详细程度。last
命令根据这些级别显示已记录的内容。这些级别不影响已记录的信息。
(需要 sbt 1.4.0+)
当 Def.task { ... }
在顶层包含一个 if
表达式时,会自动创建一个条件任务(或选择性任务)。
bar := {
if (number.value < 0) negAction.value
else if (number.value == 0) zeroAction.value
else posAction.value
}
与常规(应用)任务组合不同,条件任务会延迟对 then 子句和 else 子句的计算,正如 if
表达式自然预期的。这已经在 Def.taskDyn { ... }
中可以实现,但与动态任务不同,条件任务可以使用 inspect
命令。
Def.taskDyn
进行动态计算 使用任务的结果来确定要计算的下一个任务可能很有用。这使用 Def.taskDyn
完成。taskDyn
的结果称为动态任务,因为它在运行时引入了依赖关系。taskDyn
方法支持与 Def.task
和 :=
相同的语法,除了您返回一个任务而不是一个简单值。
例如,
val dynamic = Def.taskDyn {
// decide what to evaluate based on the value of `stringTask`
if(stringTask.value == "dev")
// create the dev-mode task: this is only evaluated if the
// value of stringTask is "dev"
Def.task {
3
}
else
// create the production task: only evaluated if the value
// of the stringTask is not "dev"
Def.task {
intTask.value + 5
}
}
myTask := {
val num = dynamic.value
println(s"Number selected was $num")
}
myTask
唯一的静态依赖关系是 stringTask
。对 intTask
的依赖关系仅在非开发模式下引入。
注意:动态任务不能引用自身,否则会导致循环依赖。在上面的示例中,如果传递给 taskDyn 的代码引用了 myTask,就会出现循环依赖关系。
sbt 0.13.8 添加了 Def.sequential
函数,用于在半顺序语义下运行任务。这与动态任务类似,但更易于定义。为了演示顺序任务,让我们创建一个名为 compilecheck
的自定义任务,它运行 Compile / compile
,然后运行 scalastyle-sbt-plugin 添加的 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
)
要从 shell 中调用 compilecheck
中的这种任务类型。如果编译失败,compilecheck
将停止执行。
root> compilecheck
[info] Compiling 1 Scala source to /Users/x/proj/target/scala-2.10/classes...
[error] /Users/x/proj/src/main/scala/Foo.scala:3: Unmatched closing brace '}' ignored here
[error] }
[error] ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed
本节讨论 failure
、result
和 andFinally
方法,这些方法用于处理其他任务的失败。
failure
failure
方法创建一个新任务,当原始任务无法正常完成时,该任务返回 Incomplete
值。如果原始任务成功,则新任务失败。 Incomplete 是一个异常,其中包含有关导致失败的任何任务以及任务执行期间抛出的任何底层异常的信息。
例如,
intTask := sys.error("Failed.")
intTask := {
println("Ignoring failure: " + intTask.failure.value)
3
}
这会覆盖 intTask
,以便打印原始异常并返回常量 3
。
failure
不会阻止依赖于目标的其他任务失败。考虑以下示例。
intTask := if(shouldSucceed) 5 else sys.error("Failed.")
// Return 3 if intTask fails. If intTask succeeds, this task will fail.
aTask := intTask.failure.value - 2
// A new task that increments the result of intTask.
bTask := intTask.value + 1
cTask := aTask.value + bTask.value
下表列出了每个任务的结果,具体取决于最初调用的任务。
调用任务 | intTask 结果 | aTask 结果 | bTask 结果 | cTask 结果 | 总体结果 |
---|---|---|---|---|---|
intTask | 失败 | 未运行 | 未运行 | 未运行 | 失败 |
aTask | 失败 | 成功 | 未运行 | 未运行 | 成功 |
bTask | 失败 | 未运行 | 失败 | 未运行 | 失败 |
cTask | 失败 | 成功 | 失败 | 失败 | 失败 |
intTask | 成功 | 未运行 | 未运行 | 未运行 | 成功 |
aTask | 成功 | 失败 | 未运行 | 未运行 | 失败 |
bTask | 成功 | 未运行 | 成功 | 未运行 | 成功 |
cTask | 成功 | 失败 | 成功 | 失败 | 失败 |
总体结果始终与根任务(直接调用的任务)相同。failure
将成功转换为失败,并将失败转换为 Incomplete
。当其任何输入失败时,普通任务定义会失败,并在其他情况下计算其值。
result
result
方法创建一个新任务,它返回原始任务的完整 Result[T]
值。 Result 的结构与类型为 T
的任务结果的 Either[Incomplete, T]
相同。也就是说,它有两个子类型。
Inc
,在失败的情况下包装 Incomplete
。Value
,在成功的情况下包装任务的结果。因此,由 result
创建的任务无论原始任务成功还是失败都会执行。
例如,
intTask := sys.error("Failed.")
intTask := {
intTask.result.value match {
case Inc(inc: Incomplete) =>
println("Ignoring failure: " + inc)
3
case Value(v) =>
println("Using successful result: " + v)
v
}
}
这会覆盖原始的 intTask
定义,因此如果原始任务失败,则会打印异常并返回常量 3
。如果成功,则会打印并返回该值。
andFinally
方法定义了一个新任务,该任务运行原始任务并计算一个副作用,无论原始任务成功与否。该任务的结果是原始任务的结果。例如,
intTask := sys.error("I didn't succeed.")
lazy val intTaskImpl = intTask andFinally { println("andFinally") }
intTask := intTaskImpl.value
这会修改原始的 intTask
,使其始终打印“andFinally”,即使任务失败。
请注意,andFinally
会构建一个新任务。这意味着必须调用新任务才能运行额外的代码块。这在对另一个任务调用 andFinally 而不是像前面的示例那样覆盖任务时很重要。例如,考虑以下代码。
intTask := sys.error("I didn't succeed.")
lazy val intTaskImpl = intTask andFinally { println("andFinally") }
otherIntTask := intTaskImpl.value
如果直接运行 intTask
,则 otherIntTask
永远不会参与执行。这种情况类似于以下简单的 Scala 代码。
def intTask(): Int =
sys.error("I didn't succeed.")
def otherIntTask(): Int =
try { intTask() }
finally { println("finally") }
intTask()
这里很明显,调用 intTask() 永远不会导致打印“finally”。