From 52ac4e02ee46261636954baa97670357ddfe638d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mai=20Huy=20Ho=C3=A0ng?= Date: Tue, 22 Sep 2026 12:41:59 +0700 Subject: [PATCH] [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) --- main/src/main/scala/sbt/Defaults.scala | 2 ++ main/src/main/scala/sbt/internal/Load.scala | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 18a4ac4d2..4f638203b 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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 diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index 974aca615..d365b7557 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -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)