Support custom clean tasks

This rewroks the cleanTask so that it only removes a subset of the
files in the target directory. To do this, I add a new task, outputs,
that returns the glob representation of the possible output files for
the task. It must be a task because some outputs will depend on streams.
For each project, the default outputs are all of the files in
baseDirectory / target.

Long term, we could enhance the clean task to be automatically generated
in any scope (as an input task). We could then add the option for the
task scoped clean to delete all of the transitive outputs of the class.
That is beyond the scope of this commit, however.

I copied the scripted tests from #3678 and added an additional test to
make sure that the manage source directory was explicitly cleaned.
This commit is contained in:
Ethan Atkins 2019-02-01 18:33:07 -08:00
parent c77a26e832
commit 172c8e9a0b
9 changed files with 99 additions and 27 deletions

View File

@ -405,6 +405,7 @@ object Defaults extends BuildCommon {
managedSourceDirectories := Seq(sourceManaged.value),
managedSources := generate(sourceGenerators).value,
sourceGenerators :== Nil,
sourceGenerators / outputs := Seq(managedDirectory.value ** AllPassFilter),
sourceDirectories := Classpaths
.concatSettings(unmanagedSourceDirectories, managedSourceDirectories)
.value,
@ -568,9 +569,14 @@ object Defaults extends BuildCommon {
globalDefaults(enableBinaryCompileAnalysis := true)
lazy val configTasks: Seq[Setting[_]] = docTaskSettings(doc) ++ inTask(compile)(
compileInputsSettings
compileInputsSettings :+ (clean := cleanTaskIn(ThisScope).value)
) ++ configGlobal ++ defaultCompileSettings ++ compileAnalysisSettings ++ Seq(
outputs := Seq(
compileAnalysisFileTask.value.toGlob,
classDirectory.value ** "*.class"
) ++ (sourceGenerators / outputs).value,
compile := compileTask.value,
clean := cleanTaskIn(ThisScope).value,
manipulateBytecode := compileIncremental.value,
compileIncremental := (compileIncrementalTask tag (Tags.Compile, Tags.CPU)).value,
printWarnings := printWarningsTask.value,
@ -581,7 +587,7 @@ object Defaults extends BuildCommon {
val extra =
if (crossPaths.value) s"_$binVersion"
else ""
s"inc_compile${extra}.zip"
s"inc_compile$extra.zip"
},
compileIncSetup := compileIncSetupTask.value,
console := consoleTask.value,
@ -616,7 +622,7 @@ object Defaults extends BuildCommon {
cleanFiles := cleanFilesTask.value,
cleanKeepFiles := Vector.empty,
cleanKeepGlobs := historyPath.value.map(_.toGlob).toSeq,
clean := (cleanTask tag Tags.Clean).value,
clean := cleanTaskIn(ThisScope).value,
consoleProject := consoleProjectTask.value,
watchTransitiveSources := watchTransitiveSourcesTask.value,
watchProjectTransitiveSources := watchTransitiveSourcesTaskImpl(watchProjectSources).value,
@ -657,6 +663,7 @@ object Defaults extends BuildCommon {
},
watchStartMessage := Watched.projectOnWatchMessage(thisProjectRef.value.project),
watch := watchSetting.value,
outputs += target.value ** AllPassFilter,
)
def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] =
@ -1304,30 +1311,45 @@ object Defaults extends BuildCommon {
}
/** Implements `cleanFiles` task. */
def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] }
private[this] def cleanTask: Initialize[Task[Unit]] = Def.task {
val defaults = Seq(managedDirectory.value ** AllPassFilter, target.value ** AllPassFilter)
val excludes = cleanKeepFiles.value.map {
// This mimics the legacy behavior of cleanFilesTask
case f if f.isDirectory => f * AllPassFilter
case f => f.toGlob
} ++ cleanKeepGlobs.value
val excludeFilter: File => Boolean = excludes.toFileFilter.accept
val globDeletions = defaults.unique.filterNot(excludeFilter)
val toDelete = cleanFiles.value.filterNot(excludeFilter) match {
case f @ Seq(_, _*) => (globDeletions ++ f).distinct
case _ => globDeletions
}
val logger = streams.value.log
toDelete.sorted.reverseIterator.foreach { f =>
logger.debug(s"clean -- deleting file $f")
try Files.deleteIfExists(f.toPath)
catch {
case _: DirectoryNotEmptyException =>
logger.debug(s"clean -- unable to delete non-empty directory $f")
private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] }
/**
* Provides an implementation for the clean task. It delegates to [[cleanTaskIn]] using the
* resolvedScoped key to set the scope.
* @return the clean task definition.
*/
def cleanTask: Initialize[Task[Unit]] =
Def.taskDyn(cleanTaskIn(resolvedScoped.value.scope)) tag Tags.Clean
/**
* Implements the clean task in a given scope. It uses the outputs task value in the provided
* scope to determine which files to delete.
* @param scope the scope in which the clean task is implemented
* @return the clean task definition.
*/
def cleanTaskIn(scope: Scope): Initialize[Task[Unit]] =
Def.task {
val excludes = cleanKeepFiles.value.map {
// This mimics the legacy behavior of cleanFilesTask
case f if f.isDirectory => f * AllPassFilter
case f => f.toGlob
} ++ cleanKeepGlobs.value
val excludeFilter: File => Boolean = excludes.toFileFilter.accept
val globDeletions = (outputs in scope).value.unique.filterNot(excludeFilter)
val toDelete = cleanFiles.value.filterNot(excludeFilter) match {
case f @ Seq(_, _*) => (globDeletions ++ f).distinct
case _ => globDeletions
}
}
}
val logger = streams.value.log
toDelete.sorted.reverseIterator.foreach { f =>
logger.debug(s"clean -- deleting file $f")
try Files.deleteIfExists(f.toPath)
catch {
case _: DirectoryNotEmptyException =>
logger.debug(s"clean -- unable to delete non-empty directory $f")
}
}
} tag Tags.Clean
def bgRunMainTask(
products: Initialize[Task[Classpath]],
@ -1636,6 +1658,8 @@ object Defaults extends BuildCommon {
incCompiler.compile(i, s.log)
} finally x.close() // workaround for #937
}
private def compileAnalysisFileTask: Def.Initialize[Task[File]] =
Def.task(streams.value.cacheDirectory / compileAnalysisFilename.value)
def compileIncSetupTask = Def.task {
val lookup = new PerClasspathEntryLookup {
private val cachedAnalysisMap = analysisMap(dependencyClasspath.value)
@ -1650,7 +1674,7 @@ object Defaults extends BuildCommon {
lookup,
(skip in compile).value,
// TODO - this is kind of a bad way to grab the cache directory for streams...
streams.value.cacheDirectory / compileAnalysisFilename.value,
compileAnalysisFileTask.value,
compilerCache.value,
incOptions.value,
(compilerReporter in compile).value,
@ -2049,6 +2073,7 @@ object Classpaths {
transitiveClassifiers :== Seq(SourceClassifier, DocClassifier),
sourceArtifactTypes :== Artifact.DefaultSourceTypes.toVector,
docArtifactTypes :== Artifact.DefaultDocTypes.toVector,
outputs :== Nil,
sbtDependency := {
val app = appConfiguration.value
val id = app.provider.id

View File

@ -157,6 +157,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")
// Generators
val sourceGenerators = settingKey[Seq[Task[Seq[File]]]]("List of tasks that generate sources.").withRank(CSetting)

View File

@ -0,0 +1,7 @@
Compile / sourceGenerators += Def.task {
val files = Seq(sourceManaged.value / "foo.txt", sourceManaged.value / "bar.txt")
files.foreach(IO.touch(_))
files
}
cleanKeepGlobs += (sourceManaged.value / "bar.txt").toGlob

View File

@ -0,0 +1,6 @@
> compile
$ exists target/scala-2.12/src_managed/foo.txt target/scala-2.12/src_managed/bar.txt
> clean
$ absent target/scala-2.12/src_managed/foo.txt
$ exists target/scala-2.12/src_managed/bar.txt

View File

@ -0,0 +1,2 @@
cleanKeepGlobs in Compile +=
((classDirectory in Compile in compile).value / "X.class").toGlob

View File

@ -0,0 +1,3 @@
class A {
val x: Int = 1
}

View File

@ -0,0 +1,3 @@
class X {
val y: Int = 0
}

View File

@ -0,0 +1,3 @@
class B {
val x: Int = 2
}

View File

@ -0,0 +1,22 @@
$ touch target/cant-touch-this
> Test/compile
$ exists target/scala-2.12/classes/A.class
$ exists target/scala-2.12/test-classes/B.class
> Test/clean
$ exists target/cant-touch-this
# it should clean only compile classes
$ exists target/scala-2.12/classes/A.class
$ exists target/scala-2.12/classes/X.class
$ absent target/scala-2.12/test-classes/B.class
# compiling everything again, but now cleaning only compile classes
> Test/compile
> Compile/clean
$ exists target/cant-touch-this
# it should clean only compile classes
$ absent target/scala-2.12/classes/A.class
$ exists target/scala-2.12/test-classes/B.class
# and X has to be kept, because of the cleanKeepFiles override
$ exists target/scala-2.12/classes/X.class