許多 sbt 任務都依賴於檔案集合。例如,package
任務會產生一個包含資源和類別檔案的 jar 檔案,而這些檔案是由專案的 compile
任務所產生。從 1.3.0 版本開始,sbt 提供了一個檔案管理系統,可以追蹤任何任務的輸入和輸出。任務可以查詢自上次任務完成以來,其檔案依賴項中哪些已變更,從而使其可以增量重建僅修改過的檔案。此系統與觸發執行整合,因此會在持續建置中自動監控任務的檔案依賴項。
為了最好地說明檔案追蹤系統,我們建構了一個 build.sbt,其中說明了所有基本功能。該範例將是一個可以使用 gcc 在 c 中建置共用函式庫的專案。這將透過兩個任務完成: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
物件檔案。這會建立一個建置管線:如果在呼叫 linkLibrary
之間未修改 buildObjects
的任何輸入來源,則不應進行編譯和連結。相反地,當偵測到輸入來源變更時,sbt 應同時產生與修改後的來源檔案相對應的新物件檔案,並連結共用函式庫。
任務指定其依賴的輸入是很自然的。這些是使用 fileInputs
鍵設定的,其類型為:Seq[Glob]
(請參閱Globs)。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
指定的 glob 所符合的任何檔案。在此情況下,在 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
可以讓我們編寫增量任務,而無需手動追蹤輸入檔案。它是一個由三個 case 類別實作的密封 trait
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
實作需要檢查目標目錄以查看存在哪些輸出的原因。在該範例中,輸入和輸出之間存在 1:1 的對應關係,但一般情況下不一定是這樣。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
的語法糖。
除了 Glob
模式所指定的範圍外,fileInputs
和 fileOutputs
還可以進行額外的篩選。sbt 提供了四種 sbt.nio.file.PathFilter
類型的設定:1. fileInputIncludeFilter
— 只包含符合此篩選器的檔案輸入。2. fileInputExcludeFilter
— 排除符合此篩選器的所有檔案輸入。3. fileOutputIncludeFilter
— 只包含符合此篩選器的檔案輸出。4. fileOutputExcludeFilter
— 排除符合此篩選器的所有檔案輸出。
預設情況下,sbt 將 scala fileInputExcludeFilter := HiddenFileFilter.toNio || DirectoryFilter
設定為排除隱藏檔案和目錄。 fileInputIncludeFilter
和 fileInputOutputFilter
都設定為 AllPassFilter.toNio
。 fileOutputExcludeFilter
則設定為 NothingFilter.toNio
。
若要從 buildObjects
中排除名稱包含 test 的檔案,請寫入:
buildObjects / fileInputExcludeFilter := "*test*"
若要保留先前排除的隱藏檔案和目錄,請寫入:
buildObjects / fileInputExcludeFilter :=
(buildObjects / fileInputExcludeFilter).value || "*test*"
或
buildObjects / fileInputExcludeFilter ~= { ef => ef || "*test*" }
在大多數情況下,由於路徑名稱的篩選應該由 fileInputs
本身處理,因此不應該需要設定 fileInputIncludeFilter
。通常也不需要篩選輸出。
當 sbt 生成 allOutputFiles
任務時,也會自動生成範圍限定於 foo
任務的 clean
實作。呼叫 foo / clean
將會移除 先前 由 foo
生成的所有檔案。它不會重新評估 foo
。例如,呼叫 buildObjects / clean
將會移除先前呼叫 buildObjects
所生成的所有物件檔。產生的 clean 任務不是可傳遞的。呼叫 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
收集的檔案輸出變更不會觸發重新建置。
每個檔案的時間戳記都是根據每個任務追蹤的。只有在增量任務本身成功時才會更新。在上面的範例中,這表示只有在 linkLibrary
成功時,才會由 linkLibrary
任務儲存 buildObjects
的當前檔案最後修改時間。這表示在呼叫 linkLibrary
之間,可以多次執行 buildObjects
,並且 linkLibrary
將會看到 buildObjects
輸出的累計變更。
如果 linkLibrary
無法完成,sbt 也會跳過更新與 linkLibrary
對應的 buildObjects
輸出的最後修改時間,因為通常無法知道哪些檔案已成功處理。