1. キャッシング

キャッシング 

タスクと設定は入門ガイドで紹介されており、タスクでより詳細に説明されています。最初にそれらを読むことをお勧めします。

カスタムタスクを定義する場合、不要な作業を避けるために値をキャッシュすることがあります。

Cache.cached 

sbt.util.Cacheは基本的なキャッシング機能を提供します。

package sbt.util

/**
 * A simple cache with keys of type `I` and values of type `O`
 */
trait Cache[I, O] {

  /**
   * Queries the cache backed with store `store` for key `key`.
   */
  def apply(store: CacheStore)(key: I): CacheResult[O]
}

IOの両方にsjsonnew.JsonFormatインスタンスがある場合、sbt.util.CacheImplicits._をインポートすることでCache[I, O]のインスタンスを導出できます(これによりBasicJsonProtocolもインポートされます)。

キャッシュを使用するには、CacheStore(またはファイル)と実際の作業を行う関数を指定して、Cache.cachedを呼び出すことで、キャッシュされた関数を生成できます。通常、キャッシュストアはstreams.value.cacheStoreFactory / "something"として作成されます。次のREPLの例では、一時ファイルからキャッシュストアを作成します。

scala> import sbt._, sbt.util.CacheImplicits._
import sbt._
import sbt.util.CacheImplicits._

scala> def doWork(i: Int): List[String] = {
         println("working...")
         Thread.sleep(1000)
         List.fill(i)("foo")
       }
doWork: (i: Int)List[String]

// use streams.value.cacheStoreFactory.make("something") for real tasks
scala> val store = sbt.util.CacheStore(file("/tmp/something"))
store: sbt.util.CacheStore = sbt.util.FileBasedStore@5a4a6716

scala> val cachedWork: Int => List[String] = Cache.cached(store)(doWork)
cachedWork: Int => List[String] = sbt.util.Cache$$$Lambda$5577/1548870528@3bb59fba

scala> cachedWork(1)
working...
res0: List[String] = List(foo)

scala> cachedWork(1)
res1: List[String] = List(foo)

scala> cachedWork(3)
working...
res2: List[String] = List(foo, foo, foo)

scala> cachedWork(1)
working...
res3: List[String] = List(foo)

ご覧のとおり、cachedWork(1)は連続して呼び出されたときにキャッシュされます。

以前の値 

TaskKeyにはpreviousというメソッドがあり、Option[A]を返します。これは軽量なトラッカーとして使用できます。最初に"hi"を返し、後続の呼び出しで"!"を追加するタスクを作成したいとします。hiというTaskKey[String]を定義し、その以前の値(Option[String]型)を取得できます。最初の呼び出しでは以前の値はNoneになり、後続の呼び出しではSome(x)になります。

lazy val hi = taskKey[String]("say hi again")
hi := {
  import sbt.util.CacheImplicits._
  val prev = hi.previous
  prev match {
    case None    => "hi"
    case Some(x) => x + "!"
  }
}

sbtシェルからshow hiを実行してテストできます。

sbt:hello> show hi
[info] hi
[success] Total time: 0 s, completed Aug 16, 2019 12:24:32 AM
sbt:hello> show hi
[info] hi!
[success] Total time: 0 s, completed Aug 16, 2019 12:24:33 AM
sbt:hello> show hi
[info] hi!!
[success] Total time: 0 s, completed Aug 16, 2019 12:24:34 AM
sbt:hello> show hi
[info] hi!!!
[success] Total time: 0 s, completed Aug 16, 2019 12:24:35 AM

各呼び出しで、hi.previousにはhiの評価結果の以前の結果が含まれています。

Tracked.lastOutput 

sbt.util.Trackedは、他のトラッカーと組み合わせることができる部分キャッシング機能を提供します。

タスクキーに関連付けられた以前の値と同様に、sbt.util.Tracked.lastOutputは最後に計算された値のトラッカーを作成します。Tracked.lastOutputは、値を格納する場所に関してより柔軟性があります。(これにより、複数のタスク間で値を共有できます)。

最初にIntを入力として受け取り、Stringに変換しますが、後続の呼び出しでは"!"を追加するとします。

scala> import sbt._, sbt.util.CacheImplicits._
import sbt._
import sbt.util.CacheImplicits._

// use streams.value.cacheStoreFactory.make("last") for real tasks
scala> val store = sbt.util.CacheStore(file("/tmp/last"))
store: sbt.util.CacheStore = sbt.util.FileBasedStore@5a4a6716

scala> val badCachedWork = Tracked.lastOutput[Int, String](store) {
         case (in, None)       => in.toString
         case (in, Some(read)) => read + "!"
       }
badCachedWork: Int => String = sbt.util.Tracked$$$Lambda$6326/638923124@68c6ff60

scala> badCachedWork(1)
res1: String = 1

scala> badCachedWork(1)
res2: String = 1!

scala> badCachedWork(2)
res3: String = 1!!

scala> badCachedWork(2)
res4: String = 1!!!

注記Tracked.lastOutputは、入力が変更された場合、キャッシュを無効化しません。

これを機能させるには、以下のTracked.inputChangedセクションを参照してください。

Tracked.inputChanged 

入力パラメーターの変更を追跡するには、Tracked.inputChangedを使用します。

scala> import sbt._, sbt.util.CacheImplicits._
import sbt._
import sbt.util.CacheImplicits._

// use streams.value.cacheStoreFactory.make("input") for real tasks
scala> val store = sbt.util.CacheStore(file("/tmp/input"))
store: sbt.util.CacheStore = sbt.util.FileBasedStore@5a4a6716

scala> val tracker = Tracked.inputChanged[Int, String](store) { case (changed, in) =>
         if (changed) {
           println("input changed")
         }
         in.toString
       }
tracker: Int => String = sbt.util.Tracked$$$Lambda$6357/1296627950@6e6837e4

scala> tracker(1)
input changed
res6: String = 1

scala> tracker(1)
res7: String = 1

scala> tracker(2)
input changed
res8: String = 2

scala> tracker(2)
res9: String = 2

scala> tracker(1)
input changed
res10: String = 1

これで、Tracked.inputChangedTracked.lastOutputをネストして、キャッシュの無効化を取り戻すことができます。

// use streams.value.cacheStoreFactory
scala> val cacheFactory = sbt.util.CacheStoreFactory(file("/tmp/cache"))
cacheFactory: sbt.util.CacheStoreFactory = sbt.util.DirectoryStoreFactory@3a3d3778

scala> def doWork(i: Int): String = {
         println("working...")
         Thread.sleep(1000)
         i.toString
       }
doWork: (i: Int)String

scala> val cachedWork2 = Tracked.inputChanged[Int, String](cacheFactory.make("input")) { case (changed: Boolean, in: Int) =>
         val tracker = Tracked.lastOutput[Int, String](cacheFactory.make("last")) {
           case (in, None)       => doWork(in)
           case (in, Some(read)) =>
             if (changed) doWork(in)
             else read
         }
         tracker(in)
       }
cachedWork2: Int => String = sbt.util.Tracked$$$Lambda$6548/972308467@1c9788cc

scala> cachedWork2(1)
working...
res0: String = 1

scala> cachedWork2(1)
res1: String = 1

トラッカーや以前の値を組み合わせる利点の1つは、無効化のタイミングを制御できることです。たとえば、2回だけ機能するキャッシュを作成できます。

lazy val hi = taskKey[String]("say hi")
lazy val hiCount = taskKey[(String, Int)]("track number of the times hi was called")

hi := hiCount.value._1
hiCount := {
  import sbt.util.CacheImplicits._
  val prev = hiCount.previous
  val s = streams.value
  def doWork(x: String): String = {
    s.log.info("working...")
    Thread.sleep(1000)
    x + "!"
  }
  val cachedWork = Tracked.inputChanged[String, (String, Int)](s.cacheStoreFactory.make("input")) { case (changed: Boolean, in: String) =>
    prev match {
      case None            => (doWork(in), 0)
      case Some((last, n)) =>
        if (changed || n > 1) (doWork(in), 0)
        else (last, n + 1)
    }
  }
  cachedWork("hi")
}

これは、hiCountタスクの以前の値を使用して呼び出された回数を追跡し、n > 1の場合にキャッシュを無効化します。

sbt:hello> hi
[info] working...
[success] Total time: 1 s, completed Aug 17, 2019 10:36:34 AM
sbt:hello> hi
[success] Total time: 0 s, completed Aug 17, 2019 10:36:35 AM
sbt:hello> hi
[success] Total time: 0 s, completed Aug 17, 2019 10:36:38 AM
sbt:hello> hi
[info] working...
[success] Total time: 1 s, completed Aug 17, 2019 10:36:40 AM

ファイル属性の追跡 

ファイルはキャッシングターゲットとして頻繁に登場しますが、java.io.Fileはファイル名だけを保持するため、キャッシングの目的ではそれ自体ではあまり役に立ちません。

ファイルキャッシングのために、sbtはsbt.util.FileFunction.cached(...)という機能を提供して、ファイルの入出力結果をキャッシュします。次の例では、*.mdの行数をカウントし、行数を内容としてクロス ターゲット ディレクトリに*.mdを出力するキャッシュされたタスクを実装しています。

lazy val countInput = taskKey[Seq[File]]("")
lazy val countFiles = taskKey[Seq[File]]("")

def doCount(in: Set[File], outDir: File): Set[File] =
  in map { source =>
    val out = outDir / source.getName
    val c = IO.readLines(source).size
    IO.write(out, c + "\n")
    out
  }

lazy val root = (project in file("."))
  .settings(
    countInput :=
      sbt.nio.file.FileTreeView.default
        .list(Glob(baseDirectory.value + "/*.md"))
        .map(_._1.toFile),
    countFiles := {
      val s = streams.value
      val in = countInput.value
      val t = crossTarget.value

      // wraps a function doCount in an up-to-date check
      val cachedFun = FileFunction.cached(s.cacheDirectory / "count") { (in: Set[File]) =>
        doCount(in, t): Set[File]
      }
      // Applies the cached function to the inputs files
      cachedFun(in.toSet).toSeq.sorted
    },
  )

最初の引数リストには、ファイル追跡スタイルを明示的に指定できる追加の引数が2つあります。デフォルトでは、入力追跡スタイルはファイルの最終更新時刻に基づくFilesInfo.lastModifiedであり、出力追跡スタイルはファイルの存在のみに基づくFilesInfo.existsです。

FileInfo 

  • FileInfo.existsはファイルが存在するかどうかを追跡します。
  • FileInfo.lastModifiedは最終更新時刻を追跡します。
  • FileInfo.hashはSHA-1コンテンツハッシュを追跡します。
  • FileInfo.fullは最終更新時刻とコンテンツハッシュの両方を追跡します。
scala> FileInfo.exists(file("/tmp/cache/last"))
res23: sbt.util.PlainFileInfo = PlainFile(/tmp/cache/last,true)

scala> FileInfo.lastModified(file("/tmp/cache/last"))
res24: sbt.util.ModifiedFileInfo = FileModified(/tmp/cache/last,1565855326328)

scala> FileInfo.hash(file("/tmp/cache/last"))
res25: sbt.util.HashFileInfo = FileHash(/tmp/cache/last,List(-89, -11, 75, 97, 65, -109, -74, -126, -124, 43, 37, -16, 9, -92, -70, -100, -82, 95, 93, -112))

scala> FileInfo.full(file("/tmp/cache/last"))
res26: sbt.util.HashModifiedFileInfo = FileHashModified(/tmp/cache/last,List(-89, -11, 75, 97, 65, -109, -74, -126, -124, 43, 37, -16, 9, -92, -70, -100, -82, 95, 93, -112),1565855326328)

sbt.util.FilesInfoもあり、FileSetを受け取ります(ただし、使用している複雑な抽象型のため、常に動作するとは限りません)。

scala> FilesInfo.exists(Set(file("/tmp/cache/last"), file("/tmp/cache/nonexistent")))
res31: sbt.util.FilesInfo[_1.F] forSome { val _1: sbt.util.FileInfo.Style } = FilesInfo(Set(PlainFile(/tmp/cache/last,true), PlainFile(/tmp/cache/nonexistent,false)))

Tracked.inputChanged 

次の例では、README.mdの行数をカウントするキャッシュされたタスクを実装しています。

lazy val count = taskKey[Int]("")

count := {
  import sbt.util.CacheImplicits._
  val prev = count.previous
  val s = streams.value
  val toCount = baseDirectory.value / "README.md"
  def doCount(source: File): Int = {
    s.log.info("working...")
    IO.readLines(source).size
  }
  val cachedCount = Tracked.inputChanged[ModifiedFileInfo, Int](s.cacheStoreFactory.make("input")) {
    (changed: Boolean, in: ModifiedFileInfo) =>
      prev match {
        case None       => doCount(in.file)
        case Some(last) =>
          if (changed) doCount(in.file)
          else last
      }
  }
  cachedCount(FileInfo.lastModified(toCount))
}

sbtシェルからshow countを実行して試すことができます。

sbt:hello> show count
[info] working...
[info] 2
[success] Total time: 0 s, completed Aug 16, 2019 9:58:38 PM
sbt:hello> show count
[info] 2
[success] Total time: 0 s, completed Aug 16, 2019 9:58:39 PM

// change something in README.md
sbt:hello> show count
[info] working...
[info] 3
[success] Total time: 0 s, completed Aug 16, 2019 9:58:44 PM

これは、sbt.util.FileInfoが自身を永続化するためにJsonFormatを実装しているため、すぐに動作します。

Tracked.outputChanged 

追跡は、ファイルにスタンプ(ファイル属性の収集)を付け、スタンプをキャッシュに格納し、後で比較することによって機能します。場合によっては、スタンプのタイミングに注意することが重要です。TypeScriptファイルをフォーマットし、SHA-1ハッシュを使用して変更を検出したいとします。フォーマッタを実行するにファイルにスタンプを付けると、タスクへの後続の呼び出しでキャッシュが無効になります。これは、フォーマッタ自体がTypeScriptファイルを修正する可能性があるためです。

これを防ぐには、Tracked.outputChangedを使用して作業が完了したにスタンプを付けます。

lazy val compileTypeScript = taskKey[Unit]("compiles *.ts files")
lazy val formatTypeScript = taskKey[Seq[File]]("format *.ts files")

compileTypeScript / sources := (baseDirectory.value / "src").globRecursive("*.ts").get
formatTypeScript := {
  import sbt.util.CacheImplicits._
  val s = streams.value
  val files = (compileTypeScript / sources).value

  def doFormat(source: File): File = {
    s.log.info(s"formatting $source")
    val lines = IO.readLines(source)
    IO.writeLines(source, lines ++ List("// something"))
    source
  }
  val tracker = Tracked.outputChanged(s.cacheStoreFactory.make("output")) {
     (outChanged: Boolean, outputs: Seq[HashFileInfo]) =>
       if (outChanged) outputs map { info => doFormat(info.file) }
       else outputs map { _.file }
  }
  tracker(() => files.map(FileInfo.hash(_)))
}

sbtシェルから`formatTypeScript`と入力して、その動作を確認してください。

sbt:hello> formatTypeScript
[info] formatting /Users/eed3si9n/work/hellotest/src/util.ts
[info] formatting /Users/eed3si9n/work/hellotest/src/hello.ts
[success] Total time: 0 s, completed Aug 17, 2019 10:07:30 AM
sbt:hello> formatTypeScript
[success] Total time: 0 s, completed Aug 17, 2019 10:07:32 AM

この実装の潜在的な欠点の1つは、ファイルの変更に関するtrue/false情報しか持っていないことです。これにより、1つのファイルが変更されるたびに、すべてのファイルの再フォーマットが行われる可能性があります。

// make change to one file
sbt:hello> formatTypeScript
[info] formatting /Users/eed3si9n/work/hellotest/src/util.ts
[info] formatting /Users/eed3si9n/work/hellotest/src/hello.ts
[success] Total time: 0 s, completed Aug 17, 2019 10:13:47 AM

このオールオアナッシングの動作を防ぐには、以下のTracked.diffOuputsを参照してください。

Tracked.outputChangedの別の潜在的な用途は、FileInfo.exists(_)と組み合わせて、出力ファイルがまだ存在するかどうかを追跡することです。キャッシュも格納されているtargetディレクトリに出力する場合は、通常は必要ありません。

Tracked.diffInputs 

Tracked.inputChangedトラッカーはBoolean値しか提供しないため、キャッシュが無効化されると、すべての作業をやり直す必要があります。違いを追跡するにはTracked.diffInputsを使用します。

Tracked.diffInputssbt.util.ChangeReportというデータ型を報告します。

/** The result of comparing some current set of objects against a previous set of objects.*/
trait ChangeReport[T] {

  /** The set of all of the objects in the current set.*/
  def checked: Set[T]

  /** All of the objects that are in the same state in the current and reference sets.*/
  def unmodified: Set[T]

  /**
   * All checked objects that are not in the same state as the reference.  This includes objects that are in both
   * sets but have changed and files that are only in one set.
   */
  def modified: Set[T] // all changes, including added

  /** All objects that are only in the current set.*/
  def added: Set[T]

  /** All objects only in the previous set*/
  def removed: Set[T]
  def +++(other: ChangeReport[T]): ChangeReport[T] = new CompoundChangeReport(this, other)

  ....
}

レポートの動作を印刷して確認してみましょう。

lazy val compileTypeScript = taskKey[Unit]("compiles *.ts files")

compileTypeScript / sources := (baseDirectory.value / "src").globRecursive("*.ts").get
compileTypeScript := {
  val s = streams.value
  val files = (compileTypeScript / sources).value
  Tracked.diffInputs(s.cacheStoreFactory.make("input_diff"), FileInfo.lastModified)(files.toSet) {
    (inDiff: ChangeReport[File]) =>
    s.log.info(inDiff.toString)
  }
}

たとえば、ファイルの名前を変更した場合の表示方法は次のとおりです。

sbt:hello> compileTypeScript
[info] Change report:
[info]  Checked: /Users/eed3si9n/work/hellotest/src/util.ts, /Users/eed3si9n/work/hellotest/src/hello.ts
[info]  Modified: /Users/eed3si9n/work/hellotest/src/util.ts, /Users/eed3si9n/work/hellotest/src/hello.ts
[info]  Unmodified:
[info]  Added: /Users/eed3si9n/work/hellotest/src/util.ts, /Users/eed3si9n/work/hellotest/src/hello.ts
[info]  Removed:
[success] Total time: 0 s, completed Aug 17, 2019 10:42:50 AM
sbt:hello> compileTypeScript
[info] Change report:
[info]  Checked: /Users/eed3si9n/work/hellotest/src/util.ts, /Users/eed3si9n/work/hellotest/src/bye.ts
[info]  Modified: /Users/eed3si9n/work/hellotest/src/hello.ts, /Users/eed3si9n/work/hellotest/src/bye.ts
[info]  Unmodified: /Users/eed3si9n/work/hellotest/src/util.ts
[info]  Added: /Users/eed3si9n/work/hellotest/src/bye.ts
[info]  Removed: /Users/eed3si9n/work/hellotest/src/hello.ts
[success] Total time: 0 s, completed Aug 17, 2019 10:43:37 AM

*.tsファイルと*.jsファイルのマッピングがあれば、コンパイルをよりインクリメンタルにすることができます。Scalaのインクリメンタルコンパイルでは、Zincは*.scala*.classファイルの関係と*.scala間の関係の両方を追跡します。TypeScriptで同様のものを作成できます。次の内容をproject/TypeScript.scalaとして保存します。

import sbt._
import sjsonnew.{ :*:, LList, LNil}
import sbt.util.CacheImplicits._

/**
 * products - products keep the mapping between source *.ts files and *.js files that are generated.
 * references - references keep the mapping between *.ts files referencing other *.ts files.
 */
case class TypeScriptAnalysis(products: List[(File, File)], references: List[(File, File)]) {
  def ++(that: TypeScriptAnalysis): TypeScriptAnalysis =
    TypeScriptAnalysis(products ++ that.products, references ++ that.references)
}
object TypeScriptAnalysis {
  implicit val analysisIso = LList.iso(
    { a: TypeScriptAnalysis => ("products", a.products) :*: ("references", a.references) :*: LNil },
    { in: List[(File, File)] :*: List[(File, File)] :*: LNil => TypeScriptAnalysis(in._1, in._2) })
}

build.sbtでは

lazy val compileTypeScript = taskKey[TypeScriptAnalysis]("compiles *.ts files")

compileTypeScript / sources := (baseDirectory.value / "src").globRecursive("*.ts").get
compileTypeScript / target := target.value / "js"
compileTypeScript := {
  import sbt.util.CacheImplicits._
  val prev0 = compileTypeScript.previous
  val prev = prev0.getOrElse(TypeScriptAnalysis(Nil, Nil))
  val s = streams.value
  val files = (compileTypeScript / sources).value

  def doCompile(source: File): TypeScriptAnalysis = {
    println("working...")
    val out = (compileTypeScript / target).value / source.getName.replaceAll("""\.ts$""", ".js")
    IO.touch(out)
    // add a fake reference from any file to util.ts
    val references: List[(File, File)] =
      if (source.getName != "util.ts") List(source -> (baseDirectory.value / "src" / "util.ts"))
      else Nil
    TypeScriptAnalysis(List(source -> out), references)
  }
  Tracked.diffInputs(s.cacheStoreFactory.make("input_diff"), FileInfo.lastModified)(files.toSet) {
    (inDiff: ChangeReport[File]) =>
    val products = scala.collection.mutable.ListBuffer(prev.products: _*)
    val references = scala.collection.mutable.ListBuffer(prev.references: _*)
    val initial = inDiff.modified & inDiff.checked
    val reverseRefs = initial.flatMap(x => Set(x) ++ references.collect({ case (k, `x`) => k }).toSet )
    products --= products.filter({ case (k, v) => reverseRefs(k) || inDiff.removed(k) })
    references --= references.filter({ case (k, v) => reverseRefs(k) || inDiff.removed(k) })
    reverseRefs foreach { x =>
      val temp = doCompile(x)
      products ++= temp.products
      references ++= temp.references
    }
    TypeScriptAnalysis(products.toList, references.toList)
  }
}

上記は、target/jsの下に.jsファイルを作成するだけの偽のコンパイルです。

sbt:hello> compileTypeScript
working...
working...
[success] Total time: 0 s, completed Aug 16, 2019 10:22:58 PM
sbt:hello> compileTypeScript
[success] Total time: 0 s, completed Aug 16, 2019 10:23:03 PM

hello.tsからutil.tsへの参照を追加したので、src/util.tsを変更した場合、src/util.tssrc/hello.tsの両方のコンパイルがトリガーされるはずです。

sbt:hello> show compileTypeScript
working...
working...
[info] TypeScriptAnalysis(List((/Users/eed3si9n/work/hellotest/src/util.ts,/Users/eed3si9n/work/hellotest/target/js/util.ts), (/Users/eed3si9n/work/hellotest/src/hello.ts,/Users/eed3si9n/work/hellotest/target/js/hello.ts)),List((/Users/eed3si9n/work/hellotest/src/hello.ts,/Users/eed3si9n/work/hellotest/src/util.ts)))

動作します。

Tracked.diffOutputs 

Tracked.diffOutputsは、Tracked.outputChangedよりも詳細なバージョンで、作業完了後にスタンプを打ち、変更されたファイルのセットも報告できます。

これを使用して、変更されたTypeScriptファイルのみをフォーマットできます。

lazy val formatTypeScript = taskKey[Seq[File]]("format *.ts files")

compileTypeScript / sources := (baseDirectory.value / "src").globRecursive("*.ts").get
formatTypeScript := {
  val s = streams.value
  val files = (compileTypeScript / sources).value
  def doFormat(source: File): File = {
    s.log.info(s"formatting $source")
    val lines = IO.readLines(source)
    IO.writeLines(source, lines ++ List("// something"))
    source
  }
  Tracked.diffOutputs(s.cacheStoreFactory.make("output_diff"), FileInfo.hash)(files.toSet) {
    (outDiff: ChangeReport[File]) =>
    val initial = outDiff.modified & outDiff.checked
    initial.toList map doFormat
  }
}

シェルでのformatTypeScriptの例を示します。

sbt:hello> formatTypeScript
[info] formatting /Users/eed3si9n/work/hellotest/src/util.ts
[info] formatting /Users/eed3si9n/work/hellotest/src/hello.ts
[success] Total time: 0 s, completed Aug 17, 2019 9:28:56 AM
sbt:hello> formatTypeScript
[success] Total time: 0 s, completed Aug 17, 2019 9:28:58 AM

事例研究: sbt-scalafmt 

sbt-scalafmtは、scalafmtscalafmtCheckタスクを実装し、互いに連携します。例えば、scalafmtが正常に実行され、ソースに変更がない場合は、scalafmtCheckのチェックをスキップします。

その実装例のスニペットを以下に示します。

private def cachedCheckSources(
  cacheStoreFactory: CacheStoreFactory,
  sources: Seq[File],
  config: Path,
  log: Logger,
  writer: PrintWriter
): ScalafmtAnalysis = {
  trackSourcesAndConfig(cacheStoreFactory, sources, config) {
    (outDiff, configChanged, prev) =>
      log.debug(outDiff.toString)
      val updatedOrAdded = outDiff.modified & outDiff.checked
      val filesToCheck =
        if (configChanged) sources
        else updatedOrAdded.toList
      val failed = prev.failed filter { _.exists }
      val files = (filesToCheck ++ failed.toSet).toSeq
      val result = checkSources(files, config, log, writer)
      // cachedCheckSources moved the outDiff cursor forward,
      // save filesToCheck so scalafmt can later run formatting
      prev.copy(
        failed = result.failed,
        pending = (prev.pending ++ filesToCheck).distinct
      )
  }
}

private def trackSourcesAndConfig(
  cacheStoreFactory: CacheStoreFactory,
  sources: Seq[File],
  config: Path
)(
    f: (ChangeReport[File], Boolean, ScalafmtAnalysis) => ScalafmtAnalysis
): ScalafmtAnalysis = {
  val prevTracker = Tracked.lastOutput[Unit, ScalafmtAnalysis](cacheStoreFactory.make("last")) {
    (_, prev0) =>
    val prev = prev0.getOrElse(ScalafmtAnalysis(Nil, Nil))
    val tracker = Tracked.inputChanged[HashFileInfo, ScalafmtAnalysis](cacheStoreFactory.make("config")) {
      case (configChanged, configHash) =>
        Tracked.diffOutputs(cacheStoreFactory.make("output-diff"), FileInfo.lastModified)(sources.toSet) {
          (outDiff: ChangeReport[File]) =>
          f(outDiff, configChanged, prev)
        }
    }
    tracker(FileInfo.hash(config.toFile))
  }
  prevTracker(())
}

上記では、trackSourcesAndConfigは、設定ファイル、ソースの最終更新スタンプ、2つのタスク間で共有される以前の値を追跡する三重にネストされたトラッカーです。2つの異なるタスク間で以前の値を共有するために、キーに関連付けられた.previousメソッドではなく、Tracked.lastOutputを使用しています。

概要 

必要な制御レベルに応じて、sbtは値とファイルをキャッシュおよび追跡するための柔軟なユーティリティセットを提供します。

  • .previousFileFunction.cached、およびCache.cachedは、開始するための基本的なキャッシュです。
  • 入力パラメーターの変更に基づいて結果を無効にするには、Tracked.inputChangedを使用します。
  • ファイル属性は、FileInfo.existsFileInfo.lastModified、およびFileInfo.hashを使用して値として追跡できます。
  • Trackedは、入力の無効化、出力の無効化、差分の追跡を行うために、多くの場合ネストされたトラッカーを提供します。