Add fileInputs and watchTriggers task

This adds two new tasks: fileInputs and watchTriggers, that will be used by sbt
both to fetch files within a task as well as to create watch sources for
continuous builds. In a subsequent commit, I will add a task for a
command that will traverse the task dependency graph to find all of the
input task dependency scopes. The idea is to make it possible to easily
and accurately specify the watch sources for a task. For example, we'd
be able to write something like:

val foo = taskKey[Unit]("print text file contents")
foo / fileInputs := baseDirectory.value  **  "*.txt"
foo := {
  (foo / fileInputs).value.all.foreach(f =>
    println(s"$f:\n${new String(java.nio.Files.readAllBytes(f.toPath))}"))
}

If the user then runs `~foo`, then the task should trigger if the user
modifies any file with the "txt" extension in the project directory.
Today, the user would have to do something like:

val fooInputs = settingKey[Seq[Source]]("the input files for foo")
fooInputs := baseDirectory.value ** "*.txt"

val foo = taskKey[Unit]("print text file contents")
foo := {
  fooInputs.value.all.foreach(f =>
    println(s"$f:\n${new String(java.nio.Files.readAllBytes(f.toPath))}"))
}
watchSources ++= fooInputs.value

or even worse:

val foo = taskKey[Unit]("print text file contents")
foo := {
  (baseDirectory.value ** "*.txt").all.foreach(f =>
    println(s"$f:\n${new String(java.nio.Files.readAllBytes(f.toPath))}"))
}
watchSources ++= baseDirectory.value ** "*.txt"

which makes it possible for the watchSources and the task sources to get
out of sync.

For consistency, I also renamed the `outputs` key `fileOutputs`.
This commit is contained in:
Ethan Atkins 2018-12-14 14:19:36 -08:00
parent 6da876cbe7
commit 1df62b6933
12 changed files with 134 additions and 11 deletions

View File

@ -143,6 +143,7 @@ object Defaults extends BuildCommon {
defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq(
excludeFilter :== HiddenFileFilter,
classLoaderCache := ClassLoaderCache(4),
fileInputs :== Nil,
) ++ TaskRepository
.proxy(GlobalScope / classLoaderCache, ClassLoaderCache(4)) ++ globalIvyCore ++ globalJvmCore
) ++ globalSbtCore
@ -381,12 +382,14 @@ object Defaults extends BuildCommon {
crossPaths.value
)
},
unmanagedSources := {
unmanagedSources / fileInputs := {
val filter =
(includeFilter in unmanagedSources).value -- (excludeFilter in unmanagedSources).value
val baseSources = if (sourcesInBase.value) baseDirectory.value * filter :: Nil else Nil
(unmanagedSourceDirectories.value.map(_ ** filter) ++ baseSources).all.map(Stamped.file)
unmanagedSourceDirectories.value.map(_ ** filter) ++ baseSources
},
unmanagedSources / fileInputs += baseDirectory.value * "foo.txt",
unmanagedSources := (unmanagedSources / fileInputs).value.all.map(Stamped.file),
watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ {
val baseDir = baseDirectory.value
val bases = unmanagedSourceDirectories.value
@ -407,7 +410,7 @@ object Defaults extends BuildCommon {
managedSourceDirectories := Seq(sourceManaged.value),
managedSources := generate(sourceGenerators).value,
sourceGenerators :== Nil,
sourceGenerators / outputs := Seq(managedDirectory.value ** AllPassFilter),
sourceGenerators / fileOutputs := Seq(managedDirectory.value ** AllPassFilter),
sourceDirectories := Classpaths
.concatSettings(unmanagedSourceDirectories, managedSourceDirectories)
.value,
@ -421,11 +424,12 @@ object Defaults extends BuildCommon {
resourceDirectories := Classpaths
.concatSettings(unmanagedResourceDirectories, managedResourceDirectories)
.value,
unmanagedResources := {
unmanagedResources / fileInputs := {
val filter =
(includeFilter in unmanagedResources).value -- (excludeFilter in unmanagedResources).value
unmanagedResourceDirectories.value.map(_ ** filter).all.map(Stamped.file)
unmanagedResourceDirectories.value.map(_ ** filter)
},
unmanagedResources := (unmanagedResources / fileInputs).value.all.map(Stamped.file),
watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ {
val bases = unmanagedResourceDirectories.value
val include = (includeFilter in unmanagedResources).value
@ -573,10 +577,10 @@ object Defaults extends BuildCommon {
lazy val configTasks: Seq[Setting[_]] = docTaskSettings(doc) ++ inTask(compile)(
compileInputsSettings :+ (clean := Clean.taskIn(ThisScope).value)
) ++ configGlobal ++ defaultCompileSettings ++ compileAnalysisSettings ++ Seq(
outputs := Seq(
fileOutputs := Seq(
compileAnalysisFileTask.value.toGlob,
classDirectory.value ** "*.class"
) ++ (sourceGenerators / outputs).value,
) ++ (sourceGenerators / fileOutputs).value,
compile := compileTask.value,
clean := Clean.taskIn(ThisScope).value,
manipulateBytecode := compileIncremental.value,
@ -663,7 +667,7 @@ object Defaults extends BuildCommon {
},
watchStartMessage := Watched.projectOnWatchMessage(thisProjectRef.value.project),
watch := watchSetting.value,
outputs += target.value ** AllPassFilter,
fileOutputs += target.value ** AllPassFilter,
)
def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] =
@ -2039,7 +2043,7 @@ object Classpaths {
transitiveClassifiers :== Seq(SourceClassifier, DocClassifier),
sourceArtifactTypes :== Artifact.DefaultSourceTypes.toVector,
docArtifactTypes :== Artifact.DefaultDocTypes.toVector,
outputs :== Nil,
fileOutputs :== Nil,
sbtDependency := {
val app = appConfiguration.value
val id = app.provider.id

View File

@ -133,6 +133,8 @@ object Keys {
val managedSources = taskKey[Seq[File]]("Sources generated by the build.").withRank(BTask)
val sources = taskKey[Seq[File]]("All sources, both managed and unmanaged.").withRank(BTask)
val sourcesInBase = settingKey[Boolean]("If true, sources from the project's base directory are included as main sources.")
val fileInputs = settingKey[Seq[Glob]]("The file globs that are used by a task. This setting will generally be scoped per task. It will also be used to determine the sources to watch during continuous execution.")
val watchTriggers = settingKey[Seq[Glob]]("Describes files that should trigger a new continuous build.")
// Filters
val includeFilter = settingKey[FileFilter]("Filter for including sources and resources files from default directories.").withRank(CSetting)
@ -157,7 +159,7 @@ object Keys {
val cleanKeepGlobs = settingKey[Seq[Glob]]("Globs to keep during a clean. Must be direct children of target.").withRank(CSetting)
val crossPaths = settingKey[Boolean]("If true, enables cross paths, which distinguish input and output directories for cross-building.").withRank(ASetting)
val taskTemporaryDirectory = settingKey[File]("Directory used for temporary files for tasks that is deleted after each task execution.").withRank(DSetting)
val outputs = taskKey[Seq[Glob]]("Describes the output files of a task")
val fileOutputs = taskKey[Seq[Glob]]("Describes the output files of a task")
// Generators
val sourceGenerators = settingKey[Seq[Task[Seq[File]]]]("List of tasks that generate sources.").withRank(CSetting)

View File

@ -61,6 +61,7 @@ object Clean {
case f => f.toGlob
} ++ cleanKeepGlobs.value
val excludeFilter: TypedPath => Boolean = excludes.toTypedPathFilter
// Don't use a regular logger because the logger actually writes to the target directory.
val debug = (logLevel in scope).?.value.orElse(state.value.get(logLevel.key)) match {
case Some(Level.Debug) =>
(string: String) =>
@ -71,7 +72,7 @@ object Clean {
}
val delete = tryDelete(debug)
cleanFiles.value.sorted.reverseIterator.foreach(delete)
(outputs in scope).value.foreach { g =>
(fileOutputs in scope).value.foreach { g =>
val filter: TypedPath => Boolean = {
val globFilter = g.toTypedPathFilter
tp =>

View File

@ -42,6 +42,7 @@ trait Import {
val ExistsFileFilter = sbt.io.ExistsFileFilter
val FileFilter = sbt.io.FileFilter
type FileFilter = sbt.io.FileFilter
type Glob = sbt.io.Glob
val GlobFilter = sbt.io.GlobFilter
val Hash = sbt.io.Hash
val HiddenFileFilter = sbt.io.HiddenFileFilter

View File

@ -0,0 +1,57 @@
// The project contains two files: { Foo.txt, Bar.md } in the subdirector base/subdir/nested-subdir
// Check that we can correctly extract Foo.txt with a recursive source
val foo = taskKey[Seq[File]]("Retrieve Foo.txt")
foo / inputs += baseDirectory.value ** "*.txt"
foo := (foo / inputs).value.all
val checkFoo = taskKey[Unit]("Check that the Foo.txt file is retrieved")
checkFoo := assert(foo.value == Seq(baseDirectory.value / "base/subdir/nested-subdir/Foo.txt"))
// Check that we can correctly extract Bar.md with a non-recursive source
val bar = taskKey[Seq[File]]("Retrieve Bar.md")
bar / inputs += baseDirectory.value / "base/subdir/nested-subdir" * "*.md"
bar := (bar / inputs).value.all
val checkBar = taskKey[Unit]("Check that the Bar.md file is retrieved")
checkBar := assert(bar.value == Seq(baseDirectory.value / "base/subdir/nested-subdir/Bar.md"))
// Check that we can correctly extract Bar.md and Foo.md with a non-recursive source
val all = taskKey[Seq[File]]("Retrieve all files")
all / inputs += baseDirectory.value / "base" / "subdir" / "nested-subdir" * AllPassFilter
val checkAll = taskKey[Unit]("Check that the Bar.md file is retrieved")
checkAll := {
import sbt.dsl.LinterLevel.Ignore
val expected = Set("Foo.txt", "Bar.md").map(baseDirectory.value / "base/subdir/nested-subdir" / _)
assert((all / inputs).value.all.toSet == expected)
}
val set = taskKey[Seq[File]]("Specify redundant sources in a set")
set / inputs ++= Seq(
baseDirectory.value / "base" ** -DirectoryFilter,
baseDirectory.value / "base" / "subdir" / "nested-subdir" * -DirectoryFilter
)
val checkSet = taskKey[Unit]("Verify that redundant sources are handled")
checkSet := {
val redundant = (set / inputs).value.all
assert(redundant.size == 4) // It should get Foo.txt and Bar.md twice
val deduped = (set / inputs).value.toSet[Glob].all
val expected = Seq("Bar.md", "Foo.txt").map(baseDirectory.value / "base/subdir/nested-subdir" / _)
assert(deduped.sorted == expected)
val altDeduped = (set / inputs).value.unique
assert(altDeduped.sorted == expected)
}

View File

@ -0,0 +1,7 @@
> checkFoo
> checkBar
> checkAll
> checkSet

View File

@ -0,0 +1,42 @@
import sbt.internal.FileTree
import sbt.io.FileTreeDataView
import xsbti.compile.analysis.Stamp
val allInputs = taskKey[Seq[File]]("")
val allInputsExplicit = taskKey[Seq[File]]("")
val checkInputs = inputKey[Unit]("")
val checkInputsExplicit = inputKey[Unit]("")
allInputs := (Compile / unmanagedSources / inputs).value.all
checkInputs := {
val res = allInputs.value
val scala = (Compile / scalaSource).value
val expected = Def.spaceDelimited("<args>").parsed.map(scala / _).toSet
assert(res.toSet == expected)
}
// In this test we override the FileTree.Repository used by the all method.
allInputsExplicit := {
val files = scala.collection.mutable.Set.empty[File]
val underlying = implicitly[FileTree.Repository]
val repo = new FileTree.Repository {
override def get(glob: Glob): Seq[FileTreeDataView.Entry[Stamp]] = {
val res = underlying.get(glob)
files ++= res.map(_.typedPath.toPath.toFile)
res
}
override def close(): Unit = {}
}
val include = (Compile / unmanagedSources / includeFilter).value
val _ = (Compile / unmanagedSources / inputs).value.all(repo).toSet
files.filter(include.accept).toSeq
}
checkInputsExplicit := {
val res = allInputsExplicit.value
val scala = (Compile / scalaSource).value
val expected = Def.spaceDelimited("<args>").parsed.map(scala / _).toSet
assert(res.toSet == expected)
}

View File

@ -0,0 +1,3 @@
package bar
object Bar

View File

@ -0,0 +1,3 @@
package foo
object Foo

View File

@ -0,0 +1,3 @@
> checkInputs foo/Foo.scala bar/Bar.scala
> checkInputsExplicit foo/Foo.scala bar/Bar.scala