1. 任务

任务 

任务和设置在入门指南中介绍,你可能想先阅读。此页面包含更多详细信息和背景信息,旨在作为参考。

简介 

设置和任务都生成值,但它们之间存在两个主要区别

  1. 设置在项目加载时进行评估。任务按需执行,通常响应用户的命令。
  2. 在项目加载开始时,设置及其依赖项是固定的。然而,任务可以在执行期间引入新任务。

功能 

任务系统有几个功能

  1. 通过与设置系统集成,任务可以像设置一样轻松灵活地添加、删除和修改。
  2. 输入任务使用解析器组合器定义其参数的语法。这允许灵活的语法和选项卡完成,与命令一样。
  3. 任务生成值。其他任务可以通过在任务定义中调用其 value 来访问任务的值。
  4. 可以动态更改任务图的结构。任务可以根据另一个任务的结果注入执行图。
  5. 有方法处理任务失败,类似于 try/catch/finally
  6. 每个任务都可以访问它自己的 Logger,默认情况下,它会以比最初打印到屏幕上更详细的级别持久化该任务的日志记录。

这些功能将在以下各节中详细讨论。

定义任务 

Hello World 示例(sbt) 

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 构建定义)。

实现任务 

实现任务后,定义任务主要分为三个部分

  1. 确定任务需要的设置和其他任务。它们是任务的输入。
  2. 定义使用这些输入实现任务的代码。
  3. 确定任务将进入的范围。

然后将这些部分组合起来,就像组合设置的部分一样。

定义基本任务 

使用 := 定义任务

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
任务范围 

与设置一样,任务可以在特定范围内定义。例如,compiletest 范围分别有单独的 compile 任务。任务的范围与设置的定义相同。在以下示例中,Test/sampleTask 使用 Compile/intTask 的结果。

Test / sampleTask := (Compile / intTask).value * 3
关于优先级 

提醒一下,中缀方法的优先级由方法的名称决定,后缀方法的优先级低于中缀方法。

  1. 赋值方法的优先级最低。这些方法的名称以 = 结尾,但 !=<=>= 和以 = 开头的名称除外。
  2. 以字母开头的名称的优先级次之。
  3. 以符号开头的名称且不包含在内的名称的优先级最高

    1. 的优先级最高。(此类别根据其开头的特定字符进一步细分。有关详细信息,请参阅 Scala 规范。)

因此,前面的示例等效于以下示例

(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 是通过 ScopeFilter.apply 方法构建的。此方法使用 Scope 的部分的过滤器创建 ScopeFilterProjectFilterConfigurationFilterTaskFilter。最简单的案例是显式指定部分的值

val filter: ScopeFilter =
   ScopeFilter(
      inProjects( core, util ),
      inConfigurations( Compile, Test )
   )
未指定的过滤器 

如果未指定任务过滤器,如上面的示例中,则默认为选择没有特定任务的范围(全局)。类似地,未指定的配置过滤器将选择全局配置中的范围。项目过滤器通常应该是显式的,但如果未指定,则将使用当前项目上下文。

更多关于过滤器构建的信息 

示例显示了基本的 inProjectsinConfigurations 方法。本节描述了构建 ProjectFilterConfigurationFilterTaskFilter 的所有方法。这些方法可以分为四组

  • 显式成员列表 (inProjects, inConfigurations, inTasks)
  • 全局值 (inGlobalProject, inGlobalConfiguration, inGlobalTask)
  • 默认过滤器 (inAnyProject, inAnyConfiguration, inAnyTask)
  • 项目关系 (inAggregates, inDependencies)

有关详细信息,请参阅 API 文档

组合 ScopeFilters 

ScopeFilters 可以使用 &&||--- 方法组合。

  • a && b 选择与 a 和 b 都匹配的范围。
  • a || b 选择与 a 或 b 匹配的范围。
  • a -- b 选择与 a 匹配但不与 b 匹配的范围。
  • -b 选择不与 b 匹配的范围。

例如,以下代码选择 core 项目的 CompileTest 配置以及 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!

使用 persistLogLevelpersistTraceLevel 设置控制日志记录的持久化详细程度。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,就会出现循环依赖关系。

使用 Def.sequential 

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

处理失败 

本节讨论 failureresultandFinally 方法,这些方法用于处理其他任务的失败。

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 

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”。