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