From 106b8b9978c0a29b2bf7d98cbc2c1ccd69a72a64 Mon Sep 17 00:00:00 2001 From: azdrojowa123 <33667003+azdrojowa123@users.noreply.github.com> Date: Tue, 13 Jan 2026 01:38:59 +0100 Subject: [PATCH] [2.x] Always create symlinks to the cache in the target locations #8445 (#8461) * always create symlinks to the cache in the target locations, even if the digest matches #8445 * create a test (currently failing even on #develop) that fails because if `zipPath` in `sbt.util.ActionCache.packageDirectory` is a symlink to the CAS, in later calls, this path in the CAS gets overridden by the new sources. - in this test, after "run 1" in line 15, the produced file "target/out/jvm/scala-3.7.4/a/classes.sbtdir.zip" is a symlink to the CAS, let's call it SH1. - when "run 3" is executed, `IO.zip` saves the new value to `zipPath`, which is "target/out/jvm/scala-3.7.4/a/classes.sbtdir.zip -> SH1", so SH1 gets overridden. - when the last "run 1" is executed, the cache retrieves SH1, but it contains the data from "run 3" (the test fails with "actual A.x is 3"). * when packaging a directory into a zip, use a temp directory to avoid overwriting the cache #8461 --- .../cache-immutability-when-package/build.sbt | 4 ++++ .../changes/A1.scala | 1 + .../changes/A2.scala | 1 + .../changes/A3.scala | 1 + .../changes/B.scala | 3 +++ .../changes/build2.sbt | 1 + .../cache-immutability-when-package/test | 24 +++++++++++++++++++ .../src/main/scala/sbt/util/ActionCache.scala | 12 ++++++++-- .../scala/sbt/util/ActionCacheStore.scala | 4 +++- 9 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/build.sbt create mode 100644 sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A1.scala create mode 100644 sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A2.scala create mode 100644 sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A3.scala create mode 100644 sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/B.scala create mode 100644 sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/build2.sbt create mode 100644 sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/test diff --git a/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/build.sbt b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/build.sbt new file mode 100644 index 000000000..aed35e96e --- /dev/null +++ b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/build.sbt @@ -0,0 +1,4 @@ +lazy val root = (project in file(".")). + dependsOn(a) + +lazy val a = (project in file("a")) diff --git a/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A1.scala b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A1.scala new file mode 100644 index 000000000..210adf865 --- /dev/null +++ b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A1.scala @@ -0,0 +1 @@ +object A { val x = 1 } diff --git a/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A2.scala b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A2.scala new file mode 100644 index 000000000..799400c8c --- /dev/null +++ b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A2.scala @@ -0,0 +1 @@ +object A { val x = 2 } diff --git a/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A3.scala b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A3.scala new file mode 100644 index 000000000..25fb93965 --- /dev/null +++ b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/A3.scala @@ -0,0 +1 @@ +object A { def x = 3 } diff --git a/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/B.scala b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/B.scala new file mode 100644 index 000000000..1e08585d1 --- /dev/null +++ b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/B.scala @@ -0,0 +1,3 @@ +object B { + def main(args: Array[String]) = assert(args(0).toInt == A.x, s"actual A.x is ${A.x}") +} diff --git a/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/build2.sbt b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/build2.sbt new file mode 100644 index 000000000..0f5735bc8 --- /dev/null +++ b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/changes/build2.sbt @@ -0,0 +1 @@ +exportJars := true \ No newline at end of file diff --git a/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/test b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/test new file mode 100644 index 000000000..c7987f104 --- /dev/null +++ b/sbt-app/src/sbt-test/source-dependencies/cache-immutability-when-package/test @@ -0,0 +1,24 @@ +$ copy-file changes/B.scala B.scala + +$ copy-file changes/A1.scala a/A.scala +$ sleep 1000 + +> run 1 +$ copy-file changes/A2.scala a/A.scala +$ sleep 1000 + +# done this way because last modified times often have ~1s resolution +> run 2 +$ copy-file changes/A1.scala a/A.scala +$ sleep 1000 + +> run 1 + +$ copy-file changes/A3.scala a/A.scala +$ sleep 1000 + +> run 3 +$ copy-file changes/A1.scala a/A.scala +$ sleep 1000 + +> run 1 diff --git a/util-cache/src/main/scala/sbt/util/ActionCache.scala b/util-cache/src/main/scala/sbt/util/ActionCache.scala index c540cf7e2..506f6ee73 100644 --- a/util-cache/src/main/scala/sbt/util/ActionCache.scala +++ b/util-cache/src/main/scala/sbt/util/ActionCache.scala @@ -10,7 +10,7 @@ package sbt.util import java.io.File import java.nio.charset.StandardCharsets -import java.nio.file.{ Path, Paths } +import java.nio.file.{ Files, Path, Paths, StandardCopyOption } import sbt.internal.util.{ ActionCacheEvent, CacheEventLog, StringVirtualFile1 } import sbt.io.syntax.* import sbt.io.IO @@ -268,7 +268,15 @@ object ActionCache: case p if p == dirPath => Nil case p if p == mPath => (mPath.toFile() -> manifestFileName) :: Nil case f => (f.toFile() -> outputDirectory.relativize(f).toString) :: Nil - IO.zip((allPaths ++ Seq(mPath)).flatMap(rebase), zipPath.toFile(), Some(default2010Timestamp)) + // Create the zip in a temp directory to avoid overwriting the cache if `zipPath` is a symlink to the CAS + val tempZipPath = (tempDir / (dirPath.getFileName.toString + dirZipExt)).toPath() + IO.zip( + (allPaths ++ Seq(mPath)).flatMap(rebase), + tempZipPath.toFile(), + Some(default2010Timestamp) + ) + Files.copy(tempZipPath, zipPath, StandardCopyOption.REPLACE_EXISTING) + conv.toVirtualFile(zipPath) inline def actionResult[A1](inline value: A1): InternalActionResult[A1] = diff --git a/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala b/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala index 8e358fbdb..1f16ce959 100644 --- a/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala +++ b/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala @@ -318,7 +318,9 @@ class DiskActionCacheStore(base: Path, converter: FileConverter) extends Abstrac writeFileAndNotify(p) case p => try - if Digest.sameDigest(p, d) then p + // `!symlinkSupported` prevents unnecessary deletion of files and then copying them again + // in #writeFileAndNotify on machines that don't support symlinks. + if Digest.sameDigest(p, d) && (!symlinkSupported.get() || Files.isSymbolicLink(p)) then p else // println(s"- syncFile: $p has different digest") IO.delete(p.toFile())