diff --git a/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala b/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala index 04f02a12a..77cd12a91 100644 --- a/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala +++ b/util-cache/src/main/scala/sbt/util/ActionCacheStore.scala @@ -318,9 +318,8 @@ case class DiskActionCacheStore(base: Path, converter: FileConverter) // See https://github.com/sbt/sbt/issues/7656 // On Windows, the program has be running under the Administrator privileges or the // user enable Developer Mode on Windows 10+ to create symbolic links. - def writeFileAndNotify(outPath: Path): Path = - Option(outPath.getParent()).foreach(parent => IO.createDirectory(parent.toFile())) - val result = Retry: + def linkOrCopy(outPath: Path): Path = + Retry: if Files.exists(outPath) then IO.delete(outPath.toFile()) if symlinkSupported.get() && Files.exists(casFile) then try Files.createSymbolicLink(outPath, casFile) @@ -339,6 +338,9 @@ case class DiskActionCacheStore(base: Path, converter: FileConverter) symlinkSupported.set(false) copyFile(outPath) else copyFile(outPath) + def writeFileAndNotify(outPath: Path): Path = + Option(outPath.getParent()).foreach(parent => IO.createDirectory(parent.toFile())) + val result = linkOrCopy(outPath) afterFileWrite(ref, result, outputDirectory) result val resolvedPath = converter.toPath(ref) match @@ -350,11 +352,11 @@ case class DiskActionCacheStore(base: Path, converter: FileConverter) writeFileAndNotify(p) case p => try - // `!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 - afterFileUpToDate(ref, p, outputDirectory) - p + if Digest.sameDigest(p, d) then + val result = + if symlinkSupported.get() && !Files.isSymbolicLink(p) then linkOrCopy(p) else p + afterFileUpToDate(ref, result, outputDirectory) + result else // println(s"- syncFile: $p has different digest") IO.delete(p.toFile()) diff --git a/util-cache/src/test/scala/sbt/util/ActionCacheTest.scala b/util-cache/src/test/scala/sbt/util/ActionCacheTest.scala index 6018036d7..82adab94c 100644 --- a/util-cache/src/test/scala/sbt/util/ActionCacheTest.scala +++ b/util-cache/src/test/scala/sbt/util/ActionCacheTest.scala @@ -115,6 +115,63 @@ object ActionCacheTest extends BasicTestSuite: assert((dir / "a.txt").exists, "a.txt not re-extracted after the directory was deleted") assert((dir / "b.txt").exists, "b.txt not re-extracted after the directory was deleted") + test("Disk cache does not re-extract a dirzip whose archive digest already matches"): + withDiskCache: cache => + IO.withTemporaryDirectory: tempDir => + val outputDirectory = tempDir.toPath() + val dir = tempDir / "gen-dir" + IO.write(dir / "a.txt", "contents A") + val zipVf = ActionCache.packageDirectory( + binaryFileConverter.toVirtualFile(dir.toPath()), + binaryFileConverter, + outputDirectory, + ) + val refs = cache.putBlobs(Seq(zipVf)) + + // packageDirectory leaves a regular file whose digest already matches the blob, so the + // extracted tree is in sync by construction and syncing must not unpackage it again. + IO.write(dir / "a.txt", "diverged") + cache.syncBlobs(refs, outputDirectory) + assert(IO.read(dir / "a.txt") == "diverged") + + test("Disk cache relinks a digest-matching dirzip to the CAS"): + withDiskCache: cache => + IO.withTemporaryDirectory: tempDir => + val outputDirectory = tempDir.toPath() + val dir = tempDir / "gen-dir" + IO.write(dir / "a.txt", "contents A") + val zipVf = ActionCache.packageDirectory( + binaryFileConverter.toVirtualFile(dir.toPath()), + binaryFileConverter, + outputDirectory, + ) + val refs = cache.putBlobs(Seq(zipVf)) + val zipPath = Paths.get(dir.toString + ActionCache.dirZipExt) + assert(!Files.isSymbolicLink(zipPath), "packageDirectory should leave a regular file") + + cache.syncBlobs(refs, outputDirectory) + assert(Files.isSymbolicLink(zipPath), "digest-matching archive was not relinked to the CAS") + + test("Disk cache re-extracts a dirzip whose archive digest differs"): + withDiskCache: cache => + IO.withTemporaryDirectory: tempDir => + val outputDirectory = tempDir.toPath() + val dir = tempDir / "gen-dir" + IO.write(dir / "a.txt", "contents A") + val zipVf = ActionCache.packageDirectory( + binaryFileConverter.toVirtualFile(dir.toPath()), + binaryFileConverter, + outputDirectory, + ) + val refs = cache.putBlobs(Seq(zipVf)) + + IO.write(Paths.get(dir.toString + ActionCache.dirZipExt).toFile(), "not an archive") + IO.write(dir / "a.txt", "diverged") + IO.write(dir / "stray.txt", "stray") + cache.syncBlobs(refs, outputDirectory) + assert(IO.read(dir / "a.txt") == "contents A", "diverged file was not restored") + assert(!(dir / "stray.txt").exists, "stray file was not removed") + test("In-memory cache can hold action value"): withInMemoryCache(testActionCacheBasic)