mirror of
https://github.com/sbt/sbt.git
synced 2026-09-07 02:55:23 +02:00
[2.x] fix: Fixes filesystem traversal order affecting cache stability (#9646)
**Problem** CompileInputs2.sources and packageBin / mappings are hashed positionally but arrive in filesystem traversal order, which differs between APFS, ext4 and overlayfs. Two machines hash an identical tree into different cache keys and cannot use each other's entries. **Solution** Sort sourcesVF by virtualized id, and mappings where packageConfigurationTask reads it. Both are where the keys are built, so nothing appended later escapes the sort. Generated-by: Claude Opus 5
This commit is contained in:
@@ -625,8 +625,9 @@ object Defaults extends BuildCommon with DefExtra {
|
||||
sources := Classpaths.concatDistinct(unmanagedSources, managedSources).value,
|
||||
sourcesVF := Def.uncached {
|
||||
val conv = fileConverter.value
|
||||
sources.value.toVector.map: x =>
|
||||
val vs = sources.value.toVector.map: x =>
|
||||
(conv.toVirtualFile(x.toPath()): HashedVirtualFileRef)
|
||||
vs.sortBy(_.id)
|
||||
},
|
||||
)
|
||||
lazy val resourceConfigPaths = Seq(
|
||||
@@ -2046,7 +2047,7 @@ object Defaults extends BuildCommon with DefExtra {
|
||||
lazy val packageConfigurationTask: Initialize[Task[Pkg.Configuration]] =
|
||||
Def.task {
|
||||
Pkg.Configuration(
|
||||
mappings.value,
|
||||
mappings.value.sortBy(_._2),
|
||||
artifactPath.value,
|
||||
packageOptions.value,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import sbt.internal.util.CacheEventSummary
|
||||
|
||||
val delClasses = taskKey[Unit]("deletes the classes directory")
|
||||
val delJar = taskKey[Unit]("deletes the packaged jar")
|
||||
val saveJar = taskKey[Unit]("copies the packaged jar aside for later comparison")
|
||||
val checkNoMiss = taskKey[Unit]("asserts the previous command was served entirely from the cache")
|
||||
val checkClasses = taskKey[Unit]("asserts class files exist")
|
||||
val checkJarMatches = taskKey[Unit]("asserts the jar is byte-identical to the saved one")
|
||||
|
||||
Global / localCacheDirectory := baseDirectory.value / "diskcache"
|
||||
|
||||
// A distinct project id keeps this fixture's output paths from colliding with same-named
|
||||
// sibling fixtures in scripted's shared batch directory.
|
||||
lazy val inputOrder = project
|
||||
.in(file("."))
|
||||
.settings(
|
||||
scalaVersion := "3.8.4",
|
||||
// Reversing stands in for a second filesystem's readdir order, which is the only part of
|
||||
// this that cannot be reproduced on a single machine.
|
||||
Compile / sources := {
|
||||
val ss = (Compile / sources).value
|
||||
if ((baseDirectory.value / "reverse.marker").exists) ss.reverse else ss
|
||||
},
|
||||
Compile / packageBin / mappings := {
|
||||
val ms = (Compile / packageBin / mappings).value
|
||||
if ((baseDirectory.value / "reverse.marker").exists) ms.reverse else ms
|
||||
},
|
||||
)
|
||||
|
||||
delClasses := Def.uncached {
|
||||
val dir = (inputOrder / Compile / classDirectory).value
|
||||
IO.delete(dir)
|
||||
streams.value.log.info(s"deleted $dir")
|
||||
}
|
||||
|
||||
delJar := Def.uncached {
|
||||
val conv = fileConverter.value
|
||||
val jar = conv.toPath((inputOrder / Compile / packageBin / artifactPath).value).toFile()
|
||||
IO.delete(jar)
|
||||
streams.value.log.info(s"deleted $jar")
|
||||
}
|
||||
|
||||
saveJar := Def.uncached {
|
||||
val conv = fileConverter.value
|
||||
val jar = conv.toPath((inputOrder / Compile / packageBin).value).toFile()
|
||||
IO.copyFile(jar, baseDirectory.value / "expected.jar")
|
||||
}
|
||||
|
||||
checkJarMatches := Def.uncached {
|
||||
val conv = fileConverter.value
|
||||
val jar = conv.toPath((inputOrder / Compile / packageBin).value).toFile()
|
||||
val expected = baseDirectory.value / "expected.jar"
|
||||
assert(
|
||||
java.util.Arrays.equals(IO.readBytes(jar), IO.readBytes(expected)),
|
||||
s"jar bytes changed after reordering packageBin / mappings"
|
||||
)
|
||||
}
|
||||
|
||||
checkNoMiss := Def.uncached {
|
||||
val config = Def.cacheConfiguration.value
|
||||
val prev = config.cacheEventLog.previous match
|
||||
case data: CacheEventSummary.Data => data
|
||||
case _ => sys.error("empty event log")
|
||||
streams.value.log.info(s"prev hitCount=${prev.hitCount} missCount=${prev.missCount}")
|
||||
assert(prev.missCount == 0, s"expected no cache misses but missCount=${prev.missCount}")
|
||||
}
|
||||
|
||||
checkClasses := Def.uncached {
|
||||
val dir = (inputOrder / Compile / classDirectory).value
|
||||
val classes = (dir ** "*.class").get()
|
||||
assert(classes.nonEmpty, s"no class files under $dir")
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
a=1
|
||||
@@ -0,0 +1 @@
|
||||
z=1
|
||||
@@ -0,0 +1 @@
|
||||
r=1
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.alpha
|
||||
|
||||
class Calpha1
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.alpha
|
||||
|
||||
class Calpha2
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.alpha
|
||||
|
||||
class Calpha3
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.ci
|
||||
|
||||
class Cci1
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.ci
|
||||
|
||||
class Cci2
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.ci
|
||||
|
||||
class Cci3
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.lib
|
||||
|
||||
class Clib1
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.lib
|
||||
|
||||
class Clib2
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.example.lib
|
||||
|
||||
class Clib3
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Regression for #9645: neither the traversal order of Compile / sources nor of
|
||||
# packageBin / mappings may take part in a cache key. When it does, a macOS developer and a
|
||||
# Linux CI runner hash an identical tree into different keys and neither can use the other's
|
||||
# entries.
|
||||
|
||||
# Populate the cache with both input lists in filesystem traversal order.
|
||||
> compile
|
||||
> packageBin
|
||||
> saveJar
|
||||
|
||||
# Reverse both lists. No key may move.
|
||||
$ touch reverse.marker
|
||||
|
||||
> delClasses
|
||||
> compile
|
||||
> checkNoMiss
|
||||
> checkClasses
|
||||
|
||||
> delJar
|
||||
> packageBin
|
||||
> checkNoMiss
|
||||
> checkJarMatches
|
||||
Reference in New Issue
Block a user