许多 sbt 任务依赖于一组文件。例如,package
任务会生成一个包含资源和类文件的 jar 文件,这些文件由 compile
任务为项目生成。从 1.3.0 版本开始,sbt 提供了一个文件管理系统,可以跟踪任何任务的输入和输出。任务可以查询其文件依赖项中自上次任务完成以来哪些文件发生了更改,从而允许其增量地仅重新构建已修改的文件。此系统与 触发执行 集成,以便在持续构建中自动监控任务的文件依赖项。
为了最好地说明文件跟踪系统,我们构建了一个 build.sbt,它说明了所有基本功能。该示例将是一个能够使用 gcc 构建共享库的项目。这将通过两个任务完成:buildObjects
,它将 c 源文件编译为目标文件,以及 linkLibrary
,它将目标文件链接到共享库中。这些可以使用以下代码定义:
import java.nio.file.Path
val buildObjects = taskKey[Seq[Path]]("Compiles c files into object files.")
val linkLibrary = taskKey[Path]("Links objects into a shared library.")
buildObjects
任务将依赖于 *.c
源文件输入。linkLibrary
任务依赖于 buildObjects
生成的输出 *.o
目标文件。这创建了一个构建管道:如果 buildObjects
的任何输入源在调用 linkLibrary
之间没有修改,那么编译和链接都不应该发生。相反,当检测到输入源更改时,sbt 应该同时生成与修改的源文件相对应的新目标文件,并将共享库链接起来。
任务自然需要指定其依赖的输入。这些使用 fileInputs
键设置,该键的类型为:Seq[Glob]
(请参见 通配符)。fileInputs
指定为 Seq[Glob]
,以便可以提供多个搜索查询,如果源文件位于多个目录中或同一任务需要不同的文件类型,则可能需要这样做。
当在给定作用域中设置 fileInputs
键时,sbt 会自动为该作用域生成一个名为 allInputFiles
的任务,该任务返回一个包含与 fileInputs
查询匹配的所有文件的 Seq[Path]
。为了方便起见,为 Task[_]
定义了一个扩展方法,将 foo.inputFiles
转换为 (foo / allInputFiles).value
。我们可以使用这些来编写 buildObjects
的简单实现:
import scala.sys.process._
import java.nio.file.{ Files, Path }
import sbt.nio._
import sbt.nio.Keys._
val buildObjects = taskKey[Seq[Path]]("Compiles c files into object files.")
buildObjects / fileInputs += baseDirectory.value.toGlob / "src" / "*.c"
buildObjects := {
val outputDir = Files.createDirectories(streams.value.cacheDirectory.toPath)
def outputPath(path: Path): Path =
outputDir / path.getFileName.toString.replaceAll(".c$", ".o")
val logger = streams.value.log
buildObjects.inputFiles.map { path =>
val output = outputPath(path)
logger.info(s"Compiling $path to $output")
Seq("gcc", "-c", path.toString, "-o", output.toString).!!
output
}
}
此实现将收集所有以 *.c
扩展名结尾的文件,并使用 gcc 对它们进行编译,并将它们编译到输出目录中。
sbt 会自动监控 fileInputs
指定的通配符匹配的任何文件。在本例中,修改 src
目录中的任何以 *.c
扩展名结尾的文件都会在持续构建中触发构建。
每次从 sbt shell 调用 buildObjects
时,它都会重新编译所有源文件。随着源文件数量的增加,这会变得很昂贵。除了 fileInputs
之外,sbt 还提供了另一个 api,inputFileChanges
,它提供有关自上次任务成功完成以来哪些源文件发生了更改的信息。使用 inputFileChanges
,我们可以使上面的构建增量化:
import scala.sys.process._
import java.nio.file.{ Files, Path }
import sbt.nio._
import sbt.nio.Keys._
val buildObjects = taskKey[Seq[Path]]("Generate object files from c sources")
buildObjects / fileInputs += baseDirectory.value.toGlob / "src" / "*.c"
buildObjects := {
val outputDir = Files.createDirectories(streams.value.cacheDirectory.toPath)
val logger = streams.value.log
def outputPath(path: Path): Path =
outputDir / path.getFileName.toString.replaceAll(".c$", ".o")
def compile(path: Path): Path = {
val output = outputPath(path)
logger.info(s"Compiling $path to $output")
Seq("gcc", "-fPIC", "-std=gnu99", "-c", s"$path", "-o", s"$output").!!
output
}
val sourceMap = buildObjects.inputFiles.view.map(p => outputPath(p) -> p).toMap
val existingTargets = fileTreeView.value.list(outputDir.toGlob / **).flatMap { case (p, _) =>
if (!sourceMap.contains(p)) {
Files.deleteIfExists(p)
None
} else {
Some(p)
}
}.toSet
val changes = buildObjects.inputFileChanges
val updatedPaths = (changes.created ++ changes.modified).toSet
val needCompile = updatedPaths ++ sourceMap.filterKeys(!existingTargets(_)).values
needCompile.foreach(compile)
sourceMap.keys.toVector
}
FileChangeReport
使得能够编写增量任务,而无需手动跟踪输入文件。它是一个密封的特征,由三个案例类实现:
Changes
- 表示一个或多个源文件已被修改。Unmodified
- 自上次运行以来,没有一个源文件被修改。Fresh
- 上次源文件哈希的缓存条目不存在。有时在 inputFileChanges
的结果上进行模式匹配很方便:
foo.inputFileChanges match {
case FileChanges(created, deleted, modified, unmodified)
if created.nonEmpty || modified.nonEmpty =>
build(created ++ modified)
delete(deleted)
case _ => // no changes
}
输入文件报告没有说明输出。这就是为什么 buildObjects
实现需要检查目标目录以查看哪些输出存在。在该示例中,输入和输出之间存在一对一映射,但在一般情况下并非如此。buildObjects
的实现可能在 fileInputs
中包含头文件。这些文件本身不会被编译,但它们可能会触发一个或多个 *.c
源文件的重新编译。
请注意,调用 buildObjects.inputFileChanges
也会在持续构建中自动监控 buildObjects / fileInputs
。
文件的输出通常最好指定为任务的结果。在上面的示例中,buildObjects
是一个 Task
,它返回一个包含编译生成的目录文件的 Seq[Path]
。sbt 会自动跟踪返回以下结果类型之一的任何任务的输出:Path
、Seq[Path]
、File
或 Seq[File]
。我们可以使用它在 buildObjects
示例的基础上编写一个任务,将目标文件链接到共享库中:
val linkLibrary = taskKey[Path]("Links objects into a shared library.")
linkLibrary := {
val outputDir = Files.createDirectories(streams.value.cacheDirectory.toPath)
val logger = streams.value.log
val isMac = scala.util.Properties.isMac
val library = outputDir / s"mylib.${if (isMac) "dylib" else "so"}"
val linkOpts = if (isMac) Seq("-dynamiclib") else Seq("-shared", "-fPIC")
if (buildObjects.outputFileChanges.hasChanges || !Files.exists(library)) {
logger.info(s"Linking $library")
(Seq("gcc") ++ linkOpts ++ Seq("-o", s"$library") ++
buildObjects.outputFiles.map(_.toString)).!!
} else {
logger.debug(s"Skipping linking of $library")
}
library
}
这里的跟踪比较简单,因为链接共享库不是增量式的。因此,如果 buildObjects
的任何输出发生了更改,或者库不存在,我们都必须重新构建。
与 fileInputs
类似,还有一个 fileOutputs
键。当输出具有已知模式时,可以使用它作为在任务中返回输出文件的替代方法。例如,buildObjects
可以定义为:
val buildObjects = taskKey[Unit]("Compiles c files into object files.")
buildObjects / fileOutputs := target.value / "objects" / ** / "*.o"
当使用不透明的外部工具时,这很有用,因为输入到输出的映射是未知的。
与 allInputFiles
类似,如果 foo
的返回类型是 Seq[Path]
、Path
、Seq[File]
或 File
之一,则会自动为任务 foo
生成一个返回类型为 Seq[Path]
的 allOutputFiles
任务。如果指定了 foo / outputFiles
,也会生成它。当同时指定了 fileOutputs
并且返回类型表示文件或文件集合时,allOutputFiles
的结果是任务返回的文件和 ouputFiles
描述的文件的非重复并集。调用 foo.outputFiles
是 (foo / allOutputFiles).value
的语法糖。
fileInputs
和 fileOutputs
可以比其 Glob
模式指定的范围更窄地进行过滤。sbt 提供了四个类型为 sbt.nio.file.PathFilter 的设置:1. fileInputIncludeFilter
- 仅包含也匹配此过滤器的文件输入 2. fileInputExcludeFilter
- 排除也匹配此过滤器的任何文件输入 3. fileOutputIncludeFilter
- 仅包含也匹配此过滤器的文件输入 4. fileOutputExcludeFilter
- 排除任何也匹配此过滤器的文件输出
默认情况下,sbt 将 `scala fileInputExcludeFilter := HiddenFileFilter.toNio || DirectoryFilter
`
Both
fileInputIncludeFilter and
fileInputOutputFilter are set to
AllPassFilter.toNio. The
fileOutputExcludeFilter is set to
NothingFilter.toNio`.
要从 buildObjects
中排除名称中包含 test 的文件,请编写:
buildObjects / fileInputExcludeFilter := "*test*"
要保留以前对隐藏文件和目录的排除,请编写:
buildObjects / fileInputExcludeFilter :=
(buildObjects / fileInputExcludeFilter).value || "*test*"
或者
buildObjects / fileInputExcludeFilter ~= { ef => ef || "*test*" }
在大多数情况下,不需要设置 fileInputIncludeFilter
,因为路径名称过滤应该由 fileInputs
本身处理。同样,通常也不需要过滤输出。
sbt 会自动生成一个作用域为任务 foo
的 clean
实现,只要它也生成了 allOutputFiles
任务。调用 foo / clean
将删除之前由 foo
生成的所有文件。它不会重新评估 foo
。例如,调用 buildObjects / clean
将删除之前调用 buildObjects
生成的所有目标文件。生成的清理任务不是可传递的。调用 linkLibrary / clean
将删除共享库,但不会删除 buildObjects
生成的目标文件。
对于 sbt 跟踪的每个输入或输出文件,都有一个关联的 FileStamp
。这可以是文件的最后修改时间或哈希值。默认情况下,输入使用哈希值跟踪,输出使用最后修改时间跟踪。要更改此设置,请设置 inputFileStamper
或 outputFileStamper
val generateSources = taskKey[Seq[Path]]("Generates source files from json schema.")
generateSources / fileInputs := baseDirectory.value.toGlob / "schema" / ** / "*.json"
generateSources / outputFileStamper := FileStamper.Hash
在持续构建 ~bar
中,对于任意任务 bar
,给定某个任务 foo
,在 bar
中对 foo.inputFiles
和 foo.inputFileChanges
的任何调用都将导致 foo / fileInputs
指定的所有 glob 在持续构建中被监控。可传递文件输入依赖项会自动被监控。例如,~linkLibrary
持续构建命令将监控为 buildObjects
定义的 *.c
源文件。
只有当输入文件哈希值发生更改时,才会触发重建。可以使用以下方法覆盖此行为:
Global / watchForceTriggerOnAnyChange := true
对使用 foo.outputFiles
或 foo.outputFileChanges
收集的文件输出的更改不会触发重建。
每个文件的标记在每个任务的基础上进行跟踪。它们只有在增量任务本身成功时才会更新。在上面的示例中,这意味着当前文件最后修改时间 buildObjects
仅在 linkLibrary
任务成功时才由 linkLibrary
任务存储。这意味着 buildObjects
可以多次运行,并在调用 linkLibrary
和 linkLibrary
之间,linkLibrary
将看到对 buildObjects
输出的累积更改。
如果 linkLibrary
无法完成,sbt 还将跳过更新对应于 linkLibrary
的 buildObjects
输出的最后修改时间,因为通常无法知道哪些文件已成功处理。