缓存
sbt 2.0 引入了混合本地/远程缓存系统,可将任务结果缓存到本地磁盘和与 Bazel 兼容的远程缓存。sbt 历史上已实现多种缓存,如 update 缓存、增量编译等,但 sbt 2.x 的缓存在以下方面具有显著变革:
- 自动。sbt 2.x 缓存通过将自身嵌入任务宏实现自动化,而 sbt 1.x 中插件作者需在任务实现中手动调用缓存函数。
- 机器级。sbt 2.x 的磁盘缓存在机器上的所有构建之间共享。
- 支持远程。在 sbt 2.x 中,缓存存储单独配置,使所有可缓存任务自动支持远程缓存。
缓存的总体目标是随着代码规模的增长,使构建和测试时间的增长趋于平缓,而非像现状那样线性增长。因此,加速比取决于代码规模等因素,但对于当前测试需要 10 分钟以上的构建,实现 5 倍到 20 倍加速是可达到的。
缓存基础
基本思路是将构建过程视为纯函数,接受输入 (A1, A2, A3, ...) 并返回输出 (R1, List(O1, O2, O3, ...))。例如,我们可以接受源文件列表、Scala 版本,最终产生 *.jar 文件。若此假设成立,则对于相同输入,我们可以为所有人记忆输出 JAR。我们关注此技术,是因为使用记忆的输出 JAR 比执行 Scala 编译等实际任务更快。
密闭构建
作为构建即纯函数的心智模型,构建工程师有时使用密闭构建(hermetic build)一词,指在沙漠中无钟表、无网络的集装箱内进行的构建。若能从该状态产生 JAR 文件,则该 JAR 应可安全地被任何机器共享。为何提到钟表?因为JAR 可能包含时间戳,因此每次产生略有不同的 JAR。为避免此问题,密闭构建工具会将时间戳覆盖为固定日期 2010-01-01,无论构建何时进行。
最终捕获临时输入的构建被称为破坏密闭性或非密闭。另一种常见的破坏密闭性的方式是将绝对路径作为输入或输出捕获。有时路径通过宏嵌入 JAR,您可能直到检查字节码才知道。
自动缓存
以下是自动缓存的演示:
val someKey = taskKey[String]("something")
someKey := name.value + version.value + "!"
在 sbt 2.x 中,任务结果将根据 name 和 version 两个设置的值进行缓存。首次运行任务时将在现场执行,但自第二次起将使用磁盘缓存:
sbt:demo> show someKey
[info] demo0.1.0-SNAPSHOT!
[success] elapsed time: 0 s, cache 0%, 1 onsite task
sbt:demo> show someKey
[info] demo0.1.0-SNAPSHOT!
[success] elapsed time: 0 s, cache 100%, 1 disk cache hit
缓存与序列化同样困难
要参与自动缓存,输入键(如 name 和 version)必须为 sjsonnew.HashWriter typeclass 提供 given,返回类型必须为 sjsonnew.JsonFormat 提供 given。Contraband 可用于生成 sjson-new 编解码器。
缓存文件
缓存文件(如 java.io.File)需要单独考虑,不是因为技术难度,而主要是因为涉及文件时的歧义和假设。当我们说「文件」时,它可能实际指:
- 从已知位置的相对路径
- 物化的实际文件
- 文件的唯一证明或内容哈希
从技术上讲,File 仅表示文件路径,因此我们只能反序列化文件名如 target/a/b.jar。若下游任务假设 target/a/b.jar 存在于文件系统中,这将失败。为清晰起见,也为避免捕获绝对路径,sbt 2.x 为这三种情况提供了三种独立的类型。
xsbti.VirtualFileRef用于表示仅相对路径,相当于传递字符串xsbti.VirtualFile表示带内容的物化文件,可以是虚拟文件或您磁盘上的文件
然而,对于密闭构建而言,两者都不适合表示文件列表。仅文件名无法保证文件相同,而在 JSON 等中携带完整文件内容效率太低。
神秘的第三个选项——文件的唯一证明——在此发挥作用。除相对路径外,HashedVirtualFileRef 还跟踪 SHA-256 内容哈希和文件大小。这可轻松序列化为 JSON,同时我们仍能引用确切文件。
文件的效果
有许多生成文件的任务不以 VirtualFile 为返回类型。例如,compile 返回 Analysis,而 *.class 文件的生成在 sbt 1.x 中作为 side effect(副作用)发生。
要参与缓存,我们需要将这些效果声明为我们关心的事物。
someKey := {
val conv = fileConverter.value
val out: java.nio.file.Path = createFile(...)
val vf: xsbti.VirtualFile = conv.toVirtualFile(out)
Def.declareOutput(vf)
vf: xsbti.HashedVirtualFileRef
}
远程缓存
您可以选择性地扩展构建,除本地磁盘缓存外还使用远程缓存。远程缓存允许多台机器共享构建构件和输出,从而提升构建性能。
假设您的项目或公司有十几个人。每天早晨,您会 git pull 这些人所做的更改,并需要构建他们的代码。若项目成功,代码规模会随时间增长,您花在构建他人代码上的时间占比会增加。这成为团队规模和代码规模的限制因素。远程缓存通过 CI 系统填充缓存、您可下载构件和任务输出来扭转这一趋势。
sbt 2.x 实现了与 Bazel 兼容的 gRPC 接口,可与多种开源和商业后端配合使用。详见 Remote cache setup。
参考
另请参阅 Cached task 参考指南。