From 0b4d57b893cbd44e571f174f5409ec7b95c93f5a Mon Sep 17 00:00:00 2001 From: carlos4s Date: Fri, 10 Apr 2026 00:27:29 +0200 Subject: [PATCH 1/3] [2.x] fix: Allow cleanFileOutputTask to delete files outside target directory **Problem** When sourceManaged or resourceManaged is set to a path outside the target directory, neither clean nor managedSourcePaths/clean removes the generated files. The cleanFileOutputTask has a hard guard that skips any file not inside the target directory, even though these files are tracked via the previous mechanism and were produced by the task itself. **Solution** Replace the targetDir guard in cleanFileOutputTask with a baseDirectory guard. Files tracked by previous are task-produced outputs that are safe to delete as long as they are within the project root. The cleanKeepFiles/cleanKeepGlobs filter still applies for user-specified exclusions. Fixes #6895 --- main/src/main/scala/sbt/internal/Clean.scala | 5 ++--- .../actions/clean-managed-outside-target/build.sbt | 8 ++++++++ .../sbt-test/actions/clean-managed-outside-target/test | 6 ++++++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 sbt-app/src/sbt-test/actions/clean-managed-outside-target/build.sbt create mode 100644 sbt-app/src/sbt-test/actions/clean-managed-outside-target/test diff --git a/main/src/main/scala/sbt/internal/Clean.scala b/main/src/main/scala/sbt/internal/Clean.scala index 9d0094ce3..0d1d9eb96 100644 --- a/main/src/main/scala/sbt/internal/Clean.scala +++ b/main/src/main/scala/sbt/internal/Clean.scala @@ -167,10 +167,9 @@ private[sbt] object Clean { }) .flatMapTask { case scope => Def.task { - val targetDir = (scope / target).value.toPath + val baseDir = (scope / baseDirectory).value.toPath val filter = cleanFilter(scope).value - // We do not want to inadvertently delete files that are not in the target directory. - val excludeFilter: Path => Boolean = path => !path.startsWith(targetDir) || filter(path) + val excludeFilter: Path => Boolean = path => !path.startsWith(baseDir) || filter(path) val delete = cleanDelete(scope).value val st = (scope / streams).value taskKey.previous.foreach(_.toSeqPath.foreach(p => if (!excludeFilter(p)) delete(p))) diff --git a/sbt-app/src/sbt-test/actions/clean-managed-outside-target/build.sbt b/sbt-app/src/sbt-test/actions/clean-managed-outside-target/build.sbt new file mode 100644 index 000000000..77a169f59 --- /dev/null +++ b/sbt-app/src/sbt-test/actions/clean-managed-outside-target/build.sbt @@ -0,0 +1,8 @@ +name := "clean-managed-outside-target" +scalaVersion := "3.3.1" +Compile / sourceManaged := baseDirectory.value / "src_managed" +Compile / sourceGenerators += Def.task { + val file = (Compile / sourceManaged).value / "demo" / "Test.scala" + IO.write(file, """object Test extends App { println("Hi") }""") + Seq(file) +}.taskValue diff --git a/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test b/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test new file mode 100644 index 000000000..b65fdc4db --- /dev/null +++ b/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test @@ -0,0 +1,6 @@ +> compile +$ exists src_managed/demo/Test.scala + +> Compile / managedSourcePaths +> Compile / managedSourcePaths / clean +$ absent src_managed/demo/Test.scala From 730f4a525d113192cf14391c220a8eb37fc8af1d Mon Sep 17 00:00:00 2001 From: carlos4s <71615127+carlos4s@users.noreply.github.com> Date: Mon, 13 Apr 2026 04:40:31 +0200 Subject: [PATCH 2/3] fix: Fix clean task to delete managed sources located outside the target directory --- main/src/main/scala/sbt/Defaults.scala | 13 ++++++++++++- main/src/main/scala/sbt/internal/Clean.scala | 5 ++++- .../actions/clean-managed-outside-target/test | 8 ++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 1b567de2c..618d0f311 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1965,7 +1965,18 @@ object Defaults extends BuildCommon with DefExtra { } /** Implements `cleanFiles` task. */ - private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] } + private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = { + import ScopeFilter.Make.* + val allConfigs = ScopeFilter(configurations = inAnyConfiguration) + Def.task { + val targetDir = target.value.toPath + val managedSrcDirs = managedSourceDirectories.?.all(allConfigs).value.flatten.flatten + val managedRscDirs = managedResourceDirectories.?.all(allConfigs).value.flatten.flatten + (managedSrcDirs ++ managedRscDirs) + .filter(d => !d.toPath.startsWith(targetDir)) + .toVector + } + } def runMainTask( classpath: Initialize[Task[Classpath]], diff --git a/main/src/main/scala/sbt/internal/Clean.scala b/main/src/main/scala/sbt/internal/Clean.scala index 0d1d9eb96..e154a6835 100644 --- a/main/src/main/scala/sbt/internal/Clean.scala +++ b/main/src/main/scala/sbt/internal/Clean.scala @@ -107,6 +107,7 @@ private[sbt] object Clean { val excludeFilter = cleanFilter(scope).value val delete = cleanDelete(scope).value val targetDir = (scope / target).?.value.map(_.toPath) + val baseDir = (scope / baseDirectory).?.value.map(_.toPath) targetDir.withFilter(_ => full).foreach(deleteContents(_, excludeFilter, view, delete)) (scope / cleanFiles).?.value.getOrElse(Nil).foreach { x => @@ -123,8 +124,10 @@ private[sbt] object Clean { val streamsGlobs = (streamsKey.toSeq ++ stampsKey) .map(k => manager(k).cacheDirectory.toPath.toGlob / **) + // Use baseDirectory instead of target so that file outputs outside the + // target directory but within the project root are also cleaned. ((scope / fileOutputs).value.filter { g => - targetDir.fold(true)(g.base.startsWith) + baseDir.fold(true)(g.base.startsWith) } ++ streamsGlobs) .foreach { g => val filter: Path => Boolean = { path => diff --git a/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test b/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test index b65fdc4db..51b6bcbe5 100644 --- a/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test +++ b/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test @@ -1,6 +1,14 @@ +# Test that managedSourcePaths / clean removes managed sources outside target > compile $ exists src_managed/demo/Test.scala > Compile / managedSourcePaths > Compile / managedSourcePaths / clean $ absent src_managed/demo/Test.scala + +# Test that clean also removes managed sources outside target +> compile +$ exists src_managed/demo/Test.scala + +> clean +$ absent src_managed/demo/Test.scala From d028427067cfb500c6f91a86c1b576584fe770b3 Mon Sep 17 00:00:00 2001 From: carlos4s <71615127+carlos4s@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:40:13 +0200 Subject: [PATCH 3/3] Fix clean task to delete managed sources located outside the target directory --- main/src/main/scala/sbt/Defaults.scala | 13 +------------ main/src/main/scala/sbt/internal/Clean.scala | 5 +---- .../actions/clean-managed-outside-target/test | 7 ------- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 618d0f311..1b567de2c 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1965,18 +1965,7 @@ object Defaults extends BuildCommon with DefExtra { } /** Implements `cleanFiles` task. */ - private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = { - import ScopeFilter.Make.* - val allConfigs = ScopeFilter(configurations = inAnyConfiguration) - Def.task { - val targetDir = target.value.toPath - val managedSrcDirs = managedSourceDirectories.?.all(allConfigs).value.flatten.flatten - val managedRscDirs = managedResourceDirectories.?.all(allConfigs).value.flatten.flatten - (managedSrcDirs ++ managedRscDirs) - .filter(d => !d.toPath.startsWith(targetDir)) - .toVector - } - } + private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] } def runMainTask( classpath: Initialize[Task[Classpath]], diff --git a/main/src/main/scala/sbt/internal/Clean.scala b/main/src/main/scala/sbt/internal/Clean.scala index e154a6835..0d1d9eb96 100644 --- a/main/src/main/scala/sbt/internal/Clean.scala +++ b/main/src/main/scala/sbt/internal/Clean.scala @@ -107,7 +107,6 @@ private[sbt] object Clean { val excludeFilter = cleanFilter(scope).value val delete = cleanDelete(scope).value val targetDir = (scope / target).?.value.map(_.toPath) - val baseDir = (scope / baseDirectory).?.value.map(_.toPath) targetDir.withFilter(_ => full).foreach(deleteContents(_, excludeFilter, view, delete)) (scope / cleanFiles).?.value.getOrElse(Nil).foreach { x => @@ -124,10 +123,8 @@ private[sbt] object Clean { val streamsGlobs = (streamsKey.toSeq ++ stampsKey) .map(k => manager(k).cacheDirectory.toPath.toGlob / **) - // Use baseDirectory instead of target so that file outputs outside the - // target directory but within the project root are also cleaned. ((scope / fileOutputs).value.filter { g => - baseDir.fold(true)(g.base.startsWith) + targetDir.fold(true)(g.base.startsWith) } ++ streamsGlobs) .foreach { g => val filter: Path => Boolean = { path => diff --git a/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test b/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test index 51b6bcbe5..18de18703 100644 --- a/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test +++ b/sbt-app/src/sbt-test/actions/clean-managed-outside-target/test @@ -5,10 +5,3 @@ $ exists src_managed/demo/Test.scala > Compile / managedSourcePaths > Compile / managedSourcePaths / clean $ absent src_managed/demo/Test.scala - -# Test that clean also removes managed sources outside target -> compile -$ exists src_managed/demo/Test.scala - -> clean -$ absent src_managed/demo/Test.scala