1. 自定义设置和任务

自定义设置和任务 

本页将引导您开始创建自己的设置和任务。

要理解本页内容,请确保您已阅读入门指南中的前面几页,尤其是 build.sbt任务图

定义键 

Keys 充满了说明如何定义键的示例。大多数键都在 Defaults 中实现。

键具有以下三种类型之一。SettingKeyTaskKey.sbt 构建定义 中有描述。在 输入任务 页面上阅读有关 InputKey 的内容。

来自 Keys 的一些示例

val scalaVersion = settingKey[String]("The version of Scala used for building.")
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.")

键构造函数有两个字符串参数:键的名称("scalaVersion")和文档字符串("The version of scala used for building.")。

请记住,从 .sbt 构建定义 中,SettingKey[T] 中的类型参数 T 表示设置的值的类型。TaskKey[T] 中的 T 表示任务结果的类型。还请记住,从 .sbt 构建定义 中,设置在项目重新加载之前具有固定值,而任务会在每次“任务执行”(每次有人在 sbt 交互式提示符或批处理模式下键入命令)时重新计算。

键可以在 .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.18"

lazy val library = (project in file("library"))
  .settings(
    sampleStringTask := System.getProperty("user.home"),
    sampleIntTask := {
      val sum = 1 + 2
      println("sum: " + sum)
      sum
    }
  )

如果任务有依赖项,您将使用 value 来引用它们的值,如 任务图 中所述。

实现任务最困难的部分通常不是 sbt 特定的;任务只是 Scala 代码。困难的部分可能是编写任务的“主体”,它执行您想要执行的任何操作。例如,您可能正在尝试格式化 HTML,在这种情况下,您可能想要使用 HTML 库(您将 将库依赖项添加到您的构建定义中 并编写基于 HTML 库的代码,也许)。

sbt 有一些实用程序库和便捷函数,特别是在 IO 中,您可以经常使用便捷的 API 来操作文件和目录。

任务的执行语义 

当使用 value 从自定义任务依赖其他任务时,需要注意一个重要的细节是任务的执行语义。通过执行语义,我们的意思是这些任务到底 *何时* 评估。

例如,如果我们以 sampleIntTask 为例,任务主体中的每一行都应严格地按顺序评估。即顺序语义

sampleIntTask := {
  val sum = 1 + 2        // first
  println("sum: " + sum) // second
  sum                    // third
}

实际上,JVM 可能会将 sum 内联到 3,但任务的可观察 *效果* 将保持与按顺序执行每一行相同。

现在假设我们定义了另外两个自定义任务 startServerstopServer,并修改 sampleIntTask 如下

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.18"

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

为了回顾发生了什么,让我们看一下 sampleIntTask 的图形表示

task-dependency

与普通的 Scala 方法调用不同,在任务上调用 value 方法不会被严格评估。相反,它们只是充当占位符,表示 sampleIntTask 依赖于 startServerstopServer 任务。当您调用 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 依赖于 startServersampleIntTask 任务,而 sampleIntTask 也依赖于 startServer 任务,所以它出现了两次作为任务依赖项。如果这是一个普通的 Scala 方法调用,它将被评估两次,但由于 value 只是表示任务依赖项,因此它将被评估一次。以下是 sampleStringTask 评估的图形表示

task-dependency

如果我们没有对任务依赖项进行重复数据删除,那么当调用 test 任务时,我们将最终多次编译测试源代码,因为 Test / compile 多次出现在 Test / test 的任务依赖项中。

清理任务 

应该如何实现 stopServer 任务?清理任务的概念不适合任务的执行模型,因为任务是关于跟踪依赖项的。最后一次操作应该成为依赖于其他中间任务的任务。例如,stopServer 应该依赖于 sampleStringTask,此时 stopServer 应该成为 sampleStringTask

lazy val library = (project in file("library"))
  .settings(
    startServer := {
      println("starting...")
      Thread.sleep(500)
    },
    sampleIntTask := {
      startServer.value
      val sum = 1 + 2
      println("sum: " + sum)
      sum
    },
    sampleStringTask := {
      startServer.value
      val s = sampleIntTask.value.toString
      println("s: " + s)
      s
    },
    sampleStringTask := {
      val old = sampleStringTask.value
      println("stopping...")
      Thread.sleep(500)
      old
    }
  )

为了证明它有效,请从交互式提示符运行 sampleStringTask

> sampleStringTask
starting...
sum: 3
s: 3
stopping...
[success] Total time: 1 s, completed Dec 22, 2014 6:00:00 PM

task-dependency

使用普通的 Scala 

另一种确保某件事在另一件事之后发生的方法是使用 Scala。例如,在 project/ServerUtil.scala 中实现一个简单的函数,您可以编写

sampleIntTask := {
  ServerUtil.startServer
  try {
    val sum = 1 + 2
    println("sum: " + sum)
  } finally {
    ServerUtil.stopServer
  }
  sum
}

由于普通的函数调用遵循顺序语义,因此所有操作按顺序发生。没有重复数据删除,因此您必须注意这一点。

将它们变成插件 

如果您发现有许多自定义代码,请考虑将其移至插件,以便在多个构建中重复使用。

创建插件非常容易,正如 之前所提 以及 此处详细讨论

本页只是一个小小的尝试;在 任务 页面上还有更多关于自定义任务的内容。