[2.x] fix: hash the boot classpath in a deterministic order (#9809)

The launcher enumerates the boot directory, so `AppProvider.mainClasspath` and `ScalaProvider.jars` come back
in filesystem order, and sbt 2 folds that classpath into the metabuild's `CompileInputs2`. Two machines with
identical inputs therefore compute different keys: measured on one project at the same commit, same sbt and
same JDK, 439 classpath entries on both sides, the same set, 83 of them at a different index and all under
${SBT_BOOT}.

Sorting happens after conversion to the rooted virtual path, so the order is independent of the filesystem
and of where the boot directory lives. The order being replaced is a directory listing, not one anybody
chose. `Main.rawEval` builds the same list but never reaches a cache key, so it is left alone.

Generated-by: Claude Opus 5 (Claude Code)
This commit is contained in:
Mai Huy Hoàng
2026-09-22 01:41:59 -04:00
committed by GitHub
parent 2124f13ac6
commit 52ac4e02ee
2 changed files with 4 additions and 1 deletions
+2
View File
@@ -4702,9 +4702,11 @@ object Classpaths:
val app = appConfiguration.value
def isJansiOrJLine(f: File) = f.getName.contains("jline") || f.getName.contains("jansi")
val scalaInstanceJars = app.provider.scalaProvider.jars.filterNot(isJansiOrJLine)
// Sorted for the same reason as the metabuild classpath in `Load`.
val sbtCp = (scalaInstanceJars ++ app.provider.mainClasspath)
.map(_.toPath)
.map(p => converter.toVirtualFile(p): HashedVirtualFileRef)
.sortBy(_.id)
.map(Attributed.blank)
val cpConfig = classpathConfiguration.value
val up = update.value
+2 -1
View File
@@ -100,8 +100,9 @@ private[sbt] object Load:
val loader = getClass.getClassLoader
val converter = MappedFileConverter(rootPaths, true)
val cp0 = provider.mainClasspath.toIndexedSeq ++ scalaProvider.jars.toIndexedSeq
// The launcher's order is a directory listing, and this classpath is hashed into the compile key.
val classpath = Attributed.blankSeq(
cp0.map(_.toPath).map(p => converter.toVirtualFile(p): HashedVirtualFileRef)
cp0.map(_.toPath).map(p => converter.toVirtualFile(p): HashedVirtualFileRef).sortBy(_.id)
)
val csrConfig = CoursierConfiguration()
.withResolvers(Resolver.combineDefaultResolvers(Vector.empty).toVector)