多くの sbt タスクは、ファイルのコレクションに依存します。たとえば、package
タスクは、プロジェクトの compile
タスクによって生成されるリソースとクラスファイルを含む jar ファイルを生成します。バージョン 1.3.0 以降、sbt は、タスクの入出力を追跡するファイル管理システムを提供します。タスクは、最後にタスクが完了してから、どのファイル依存関係が変更されたかを照会でき、変更されたファイルのみをインクリメンタルに再構築できます。このシステムは、トリガー実行と統合されているため、タスクのファイル依存関係は、継続的なビルドで自動的に監視されます。
ファイル追跡システムを最もよく説明するために、すべての重要な機能を示す build.sbt を構築します。例は、gcc を使用して c で共有ライブラリをビルドできるプロジェクトになります。これは、c ソースファイルをオブジェクトファイルにコンパイルする buildObjects
と、オブジェクトファイルを共有ライブラリにリンクする linkLibrary
の 2 つのタスクで実行されます。これらは次のように定義できます。
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 は、そのスコープに対して、fileInputs
クエリに一致するすべてのファイルを含む Seq[Path]
を返す allInputFiles
という名前のタスクを自動的に生成します。便宜上、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 シェルから buildObjects
が呼び出されるたびに、すべてのソースファイルが再コンパイルされます。ソースファイルの数が増えるにつれて、これはコストがかかるようになります。fileInputs
に加えて、sbt は別の API である inputFileChanges
も提供します。この API は、タスクが最後に正常に完了してからどのソースファイルが変更されたかに関する情報を提供します。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
を使用すると、入力ファイルを手動で追跡することなく、インクリメンタルタスクを作成できます。これは、3 つのケースクラスによって実装されるシールされたトレイトです。
Changes
— 1 つ以上のソースファイルが変更されたことを示します。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
にヘッダーファイルを含めることができます。これらはそれ自体ではコンパイルされませんが、1 つ以上の *.c
ソースファイルの再コンパイルをトリガーする可能性があります。
buildObjects.inputFileChanges
を呼び出すと、継続的なビルドで buildObjects / fileInputs
が自動的に監視されることにも注意してください。
ファイルの出力は、多くの場合、タスクの結果として指定するのが最適です。上記の例では、buildObjects
は、コンパイルによって生成されたオブジェクトファイルを含む Seq[Path]
を返す Task
です。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 / outputFiles
が指定されている場合、Seq[Path]
型の allOutputFiles
タスクが自動的に生成されます。fileOutputs
が指定されており、戻り値の型がファイルまたはファイルのコレクションを表す場合、allOutputFiles
の結果は、タスクによって返されるファイルと ouputFiles
によって記述されたファイルの明確な結合になります。foo.outputFiles
の呼び出しは、(foo / allOutputFiles).value
の糖衣構文です。
fileInputs
と fileOutputs
は、Glob
パターンで指定された内容を超えてフィルタリングできます。sbt は、sbt.nio.file.PathFilter 型の 4 つの設定を提供します。1. fileInputIncludeFilter
— このフィルタにも一致するファイル入力のみを含めます。2. fileInputExcludeFilter
— このフィルタにも一致するファイル入力を除外します。3. fileOutputIncludeFilter
— このフィルタにも一致するファイル入力のみを含めます。4. fileOutputExcludeFilter
— このフィルタにも一致するファイル出力を除外します。
デフォルトでは、sbt は `scala fileInputExcludeFilter := HiddenFileFilter.toNio || DirectoryFilter
両方の
fileInputIncludeFilter
と fileInputOutputFilter
を AllPassFilter.toNio
に設定します。fileOutputExcludeFilter
は NothingFilter.toNio
に設定されます。
名前の中に test を持つファイルを buildObjects
から除外するには、次のように記述します。
buildObjects / fileInputExcludeFilter := "*test*"
非表示のファイルとディレクトリの以前の除外を維持するには、次のように記述します。
buildObjects / fileInputExcludeFilter :=
(buildObjects / fileInputExcludeFilter).value || "*test*"
または
buildObjects / fileInputExcludeFilter ~= { ef => ef || "*test*" }
ほとんどの場合、fileInputIncludeFilter
を設定する必要はありません。パス名フィルタリングは fileInputs
自体で処理する必要があるためです。また、出力をフィルタリングする必要もあまりありません。
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
のいずれかで収集されたファイル出力への変更は、再ビルドをトリガーしません。
各ファイルのスタンプは、タスクごとに追跡されます。それらは、増分タスク自体が成功した場合にのみ更新されます。上記の例では、これはbuildObjects
の現在のファイル最終変更時刻が、成功した場合にのみlinkLibrary
タスクによって保存されることを意味します。これは、buildObjects
がlinkLibrary
の呼び出しの間で何度も実行され、linkLibrary
がbuildObjects
の出力に対する累積的な変更を確認できることを意味します。
linkLibrary
の完了に失敗した場合、sbtは、一般にどのファイルが正常に処理されたかを知ることが不可能であるため、linkLibrary
に対応するbuildObjects
の出力の最終変更時刻の更新もスキップします。