1. ファイル入出力の追跡

ファイル入出力の追跡 

多くの 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] です(グロブを参照)。fileInputsSeq[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 つのケースクラスによって実装されるシールされたトレイトです。

  1. Changes — 1 つ以上のソースファイルが変更されたことを示します。
  2. Unmodified — 前回の実行以降、ソースファイルが変更されていないことを示します。
  3. 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 は、次の結果型のいずれかを返すタスクの出力を自動的に追跡します。PathSeq[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]PathSeq[File]、または File のいずれかの場合、または foo / outputFiles が指定されている場合、Seq[Path] 型の allOutputFiles タスクが自動的に生成されます。fileOutputs が指定されており、戻り値の型がファイルまたはファイルのコレクションを表す場合、allOutputFiles の結果は、タスクによって返されるファイルと ouputFiles によって記述されたファイルの明確な結合になります。foo.outputFiles の呼び出しは、(foo / allOutputFiles).value の糖衣構文です。

フィルタ 

fileInputsfileOutputs は、Glob パターンで指定された内容を超えてフィルタリングできます。sbt は、sbt.nio.file.PathFilter 型の 4 つの設定を提供します。1. fileInputIncludeFilter — このフィルタにも一致するファイル入力のみを含めます。2. fileInputExcludeFilter— このフィルタにも一致するファイル入力を除外します。3. fileOutputIncludeFilter — このフィルタにも一致するファイル入力のみを含めます。4. fileOutputExcludeFilter — このフィルタにも一致するファイル出力を除外します。

デフォルトでは、sbt は `scala fileInputExcludeFilter := HiddenFileFilter.toNio || DirectoryFilter 両方の fileInputIncludeFilterfileInputOutputFilterAllPassFilter.toNio に設定します。fileOutputExcludeFilterNothingFilter.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タスクによって保存されることを意味します。これは、buildObjectslinkLibraryの呼び出しの間で何度も実行され、linkLibrarybuildObjectsの出力に対する累積的な変更を確認できることを意味します。

linkLibraryの完了に失敗した場合、sbtは、一般にどのファイルが正常に処理されたかを知ることが不可能であるため、linkLibraryに対応するbuildObjectsの出力の最終変更時刻の更新もスキップします。