SBT sourceGenerators task - execute only if a file

2020-02-25 07:56发布

问题:

In my SBT project, I have an input file src/main/greeting/Greeting.txt with the following content:

Hello, world!

This is my build.sbt that generates Scala source from the Greeting.txt file:

sourceGenerators in Compile += Def.task{
  println("GENERATING FILES")
  val inputFile = file("src/main/greeting/Greeting.txt")
  val generatedFile =
    (sourceManaged in Compile).value / "scala" / "Main.scala"
  val greeting = IO.read(inputFile).trim
  IO.write(
    generatedFile,
    s"""object Main extends App { println("${greeting}") }"""
  )
  Seq(generatedFile)
}.taskValue

This build.sbt works fine, except that it runs my tasks to generate the Scala sources every time I compile/run my project. I would like it to only run these tasks when the Greeting.txt-file has changed. How can I achieve this?


MCVE

Bash-script that generates the project:

#!/bin/bash
mkdir sourceGeneratorsExample
cd sourceGeneratorsExample
mkdir -p src/main/scala
mkdir -p src/main/greeting
echo "Hello, world!" >> src/main/greeting/Greeting.txt
cat <<HEREDOC > build.sbt
sourceGenerators in Compile += Def.task{
  println("GENERATING FILES")
  val inputFile = file("src/main/greeting/Greeting.txt")
  val generatedFile =
    (sourceManaged in Compile).value / "scala" / "Main.scala"
  val greeting = IO.read(inputFile).trim
  IO.write(
    generatedFile,
    "object Main extends App { println(\"" + greeting + "\") }"
  )
  Seq(generatedFile)
}.taskValue
HEREDOC

Duplicates / Documentation

  • This is an answer from 2012, a lot has changed since then.
  • The current reference manual advises to use "sbt.Tracked.{ inputChanged, outputChanged } etc", but does not expand on that, and the Tracked object is not mentioned anywhere else in the manual.

回答1:

You can use FileFunction.cached, which is a:

Generic change-detection helper used to help build / artifact generation / etc. steps detect whether or not they need to run.

It uses a cache folder, where SBT automatically keeps a record of the file changes. With FileFunction.cached, your build.sbt might look like this:

sourceGenerators in Compile += Def.task{

  // * Create a cached function which generates the output files
  //   only if the input files have changed.
  // * The first parameter is a file instance of the path to
  //   the cache folder
  // * The second parameter is the function to process the input 
  //   files and return the output files
  val cachedFun = FileFunction.cached(
    streams.value.cacheDirectory / "greeting"
  ) { (in: Set[File]) =>

    println("GENERATING FILES")

    val generatedFile =
      (sourceManaged in Compile).value / "scala" / "Main.scala"
    val greeting = IO.read(in.head).trim
    IO.write(
      generatedFile,
      "object Main extends App { println(\"" + greeting + "\") }"
    )
    Set(generatedFile)
  }

  // get the input file
  val inputFile = file("src/main/greeting/Greeting.txt")

  // put the input file into a `Set` (as required by `cachedFun`),
  // pass it to the `cachedFun`,
  // convert the result to `Seq` (as required by `Def.task`)
  cachedFun(Set(inputFile)).toSeq

}.taskValue

The first parameter for FileFunction.cached is a directory that will be used to store cache information (e.g. hashes of the input files). Here, we passed streams.value.cacheDirectory / "greeting", which will create a cache subdirectory somewhere inside of the target-directory. The advantage is that his directory will be automatically cleaned when the task clean is run.

The first argument list of the cached method takes two additional optional inStyle and outStyle arguments, which determine how changes are detected (e.g. by modification date, or by comparing hashes). Note that in older versions of SBT, these two arguments are mandatory, so that your cachedFun would look somewhat like this:

val cachedFun = FileFunction.cached(
  cacheBaseDirectory = streams.value.cacheDirectory / "greeting",
  inStyle = FilesInfo.lastModified,
  outStyle = FilesInfo.exists
)(cachedFunBodyImpl)

The second argument list of the FileFunction.cached-method takes a function that maps a Set of input files to the Set of output files. It is invoked only if the input files have changed.

You can find more info for an older version of SBT here (SBT 0.13.5), which expands on cached and file tracking styles. Quoting:

There are two additional arguments for the first parameter list that allow the file tracking style to be explicitly specified. By default, the input tracking style is FilesInfo.lastModified, based on a file's last modified time, and the output tracking style is FilesInfo.exists, based only on whether the file exists. The other available style is FilesInfo.hash, which tracks a file based on a hash of its contents.


The first code snippet has been tested using SBT 1.2.8. The second code snippet should also work with earlier 0.13.x versions.



标签: scala sbt