这一小节讲解如何创建自定义设置和任务。
在理解本节之前,请先阅读 sbt 入门前面的章节,尤其是 .sbt 构建定义和更多关于设置。
键有三种类型。SettingKey
和 TaskKey
在 .sbt 构建定义讲解。关于 InputKey
的内容在输入任务页面。
列举一些来自 Keys 的例子:
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.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 具有一些实用工具库和方便的函数,特别是可以经常使用 API 中的 IO 来操作文件和目录。
当从依赖于其他任务的自定义任务中使用value
时,一个要注意的重要细节是是任务的执行语义。对执行语义,我们的意思是到底何时这些任务被取值。
以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.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
图形表示:
不同于普通的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
类型,它将只被计算一次。以下是sampeStringTask
如何取值的图形表示:
如果我们不做重复任务相关项的去重,则当我们执行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
确保一些事发生在其它一些事物之后的另一种方式是使用Scala。例如,在project/ServerUtil.scala
中实现一个简单的函数,你可以编写:
sampleIntTask := {
ServerUtil.startServer
try {
val sum = 1 + 2
println("sum: " + sum)
} finally {
ServerUtil.stopServer
}
sum
}
因为普通的方法调用遵循顺序语义,所有事情按顺序发生。这里没有去重,所以你必须要小心。
如果你发现自己有很多自定义代码,可以考虑将其移动到插件,从而可以在多个构建中重复利用。
本小节是个快速的向导;更多关于自定义任务可以在任务中找到。