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