From f624998c9116781922f1b9c572272596bfe98025 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:10:06 -0400 Subject: [PATCH] [2.x] fix: Avoid following symlinks during scripted test cleanup (#8985) **Problem** When a scripted test creates a symbolic link within the test directory pointing to a file or directory outside of it, the batch cleanup code follows the symlink and deletes the external target. This is because `view.list(base / **, !keep)` recursively traverses symlinked directories, and the resulting paths resolve to the external location. **Solution** Replace the flat recursive glob listing with manual recursion that checks `FileAttributes.isSymbolicLink` before descending into directories, matching the pattern already used in `Clean.scala`. Symlinks are now deleted as leaf nodes without traversing their targets. Also hoists the cleanup helpers out of the per-test loop since they only depend on the (constant) temp directory. Fixes sbt/sbt#7331 --- .../sbt/scriptedtest/ScriptedTests.scala | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/scripted-sbt/src/main/scala/sbt/scriptedtest/ScriptedTests.scala b/scripted-sbt/src/main/scala/sbt/scriptedtest/ScriptedTests.scala index 837661fb1..14d86a460 100644 --- a/scripted-sbt/src/main/scala/sbt/scriptedtest/ScriptedTests.scala +++ b/scripted-sbt/src/main/scala/sbt/scriptedtest/ScriptedTests.scala @@ -225,6 +225,27 @@ final class ScriptedTests( val seqHandlers = handlers.values.toList runner.initStates(states, seqHandlers) + // Delete test contents between runs, but preserve global dirs. + // Uses manual recursion to avoid following symlinks (sbt/sbt#7331). + val view = sbt.nio.file.FileTreeView.default + val base = tempTestDir.getCanonicalFile.toGlob + val global = base / "global" + val globalLogging = base / ** / "global-logging" + def recursiveFilter(glob: Glob): PathFilter = (glob: PathFilter) || glob / ** + val keep: PathFilter = recursiveFilter(global) || recursiveFilter(globalLogging) + def deleteContents(dir: java.nio.file.Path): Unit = + view + .list(Glob(dir, AnyPath), !keep) + .foreach: + case (p, attrs) if attrs.isDirectory && !attrs.isSymbolicLink => + deleteContents(p) + try Files.deleteIfExists(p) + catch { case _: IOException => } + case (p, _) => + try Files.deleteIfExists(p) + catch { case _: IOException => } + val tempTestPath = tempTestDir.getCanonicalFile.toPath + def runBatchTests = { groupedTests.map { case ((group, name), originalDir) => val label = s"$group/$name" @@ -261,19 +282,7 @@ final class ScriptedTests( // Run the test and delete files (except global that holds local scala jars) val result = runOrHandleDisabled(label, tempTestDir, runTest, buffer) - if (!keepTempDirectory) { - val view = sbt.nio.file.FileTreeView.default - val base = tempTestDir.getCanonicalFile.toGlob - val global = base / "global" - val globalLogging = base / ** / "global-logging" - def recursiveFilter(glob: Glob): PathFilter = (glob: PathFilter) || glob / ** - val keep: PathFilter = recursiveFilter(global) || recursiveFilter(globalLogging) - val toDelete = view.list(base / **, !keep).map(_._1).sorted.reverse - toDelete.foreach { p => - try Files.deleteIfExists(p) - catch { case _: IOException => } - } - } + if (!keepTempDirectory) deleteContents(tempTestPath) result } }