From 172c8e9a0baa90749f5ceb609a7e465c2c721d13 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Fri, 1 Feb 2019 18:33:07 -0800 Subject: [PATCH] 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. --- main/src/main/scala/sbt/Defaults.scala | 79 ++++++++++++------- main/src/main/scala/sbt/Keys.scala | 1 + .../sbt-test/actions/clean-managed/build.sbt | 7 ++ sbt/src/sbt-test/actions/clean-managed/test | 6 ++ .../sbt-test/actions/compile-clean/build.sbt | 2 + .../compile-clean/src/main/scala/A.scala | 3 + .../compile-clean/src/main/scala/X.scala | 3 + .../compile-clean/src/test/scala/B.scala | 3 + sbt/src/sbt-test/actions/compile-clean/test | 22 ++++++ 9 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 sbt/src/sbt-test/actions/clean-managed/build.sbt create mode 100644 sbt/src/sbt-test/actions/clean-managed/test create mode 100644 sbt/src/sbt-test/actions/compile-clean/build.sbt create mode 100644 sbt/src/sbt-test/actions/compile-clean/src/main/scala/A.scala create mode 100644 sbt/src/sbt-test/actions/compile-clean/src/main/scala/X.scala create mode 100644 sbt/src/sbt-test/actions/compile-clean/src/test/scala/B.scala create mode 100644 sbt/src/sbt-test/actions/compile-clean/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index ff8a28c88..a9ce86ebc 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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 diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 05dced647..9f144259f 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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) diff --git a/sbt/src/sbt-test/actions/clean-managed/build.sbt b/sbt/src/sbt-test/actions/clean-managed/build.sbt new file mode 100644 index 000000000..ffa46f97d --- /dev/null +++ b/sbt/src/sbt-test/actions/clean-managed/build.sbt @@ -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 diff --git a/sbt/src/sbt-test/actions/clean-managed/test b/sbt/src/sbt-test/actions/clean-managed/test new file mode 100644 index 000000000..f6fd6ce8f --- /dev/null +++ b/sbt/src/sbt-test/actions/clean-managed/test @@ -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 diff --git a/sbt/src/sbt-test/actions/compile-clean/build.sbt b/sbt/src/sbt-test/actions/compile-clean/build.sbt new file mode 100644 index 000000000..2e29f6de6 --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/build.sbt @@ -0,0 +1,2 @@ +cleanKeepGlobs in Compile += + ((classDirectory in Compile in compile).value / "X.class").toGlob diff --git a/sbt/src/sbt-test/actions/compile-clean/src/main/scala/A.scala b/sbt/src/sbt-test/actions/compile-clean/src/main/scala/A.scala new file mode 100644 index 000000000..6da20a96e --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/src/main/scala/A.scala @@ -0,0 +1,3 @@ +class A { + val x: Int = 1 +} diff --git a/sbt/src/sbt-test/actions/compile-clean/src/main/scala/X.scala b/sbt/src/sbt-test/actions/compile-clean/src/main/scala/X.scala new file mode 100644 index 000000000..bd84382cd --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/src/main/scala/X.scala @@ -0,0 +1,3 @@ +class X { + val y: Int = 0 +} diff --git a/sbt/src/sbt-test/actions/compile-clean/src/test/scala/B.scala b/sbt/src/sbt-test/actions/compile-clean/src/test/scala/B.scala new file mode 100644 index 000000000..4e79fe7a2 --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/src/test/scala/B.scala @@ -0,0 +1,3 @@ +class B { + val x: Int = 2 +} diff --git a/sbt/src/sbt-test/actions/compile-clean/test b/sbt/src/sbt-test/actions/compile-clean/test new file mode 100644 index 000000000..2e805ffd0 --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/test @@ -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