[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
This commit is contained in:
azdrojowa123 2026-01-13 01:38:59 +01:00 committed by GitHub
parent 81b6408f49
commit 106b8b9978
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 48 additions and 3 deletions

View File

@ -0,0 +1,4 @@
lazy val root = (project in file(".")).
dependsOn(a)
lazy val a = (project in file("a"))

View File

@ -0,0 +1 @@
object A { val x = 1 }

View File

@ -0,0 +1 @@
object A { val x = 2 }

View File

@ -0,0 +1 @@
object A { def x = 3 }

View File

@ -0,0 +1,3 @@
object B {
def main(args: Array[String]) = assert(args(0).toInt == A.x, s"actual A.x is ${A.x}")
}

View File

@ -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

View File

@ -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] =

View File

@ -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())