mirror of https://github.com/sbt/sbt.git
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:
parent
6da876cbe7
commit
1df62b6933
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
> checkFoo
|
||||
|
||||
> checkBar
|
||||
|
||||
> checkAll
|
||||
|
||||
> checkSet
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package bar
|
||||
|
||||
object Bar
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package foo
|
||||
|
||||
object Foo
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
> checkInputs foo/Foo.scala bar/Bar.scala
|
||||
|
||||
> checkInputsExplicit foo/Foo.scala bar/Bar.scala
|
||||
Loading…
Reference in New Issue