fix: Fix NoClassDefFoundError after changing val in build.sbt (#8521)

**Problem**

When a val definition in build.sbt changes and the user runs `reload`, sbt crashes with `NoClassDefFoundError: $Wrap<hash>$`.

```
java.lang.NoClassDefFoundError: $Wrape8743d4f36$
  at $Wrap0b8ea34d40$.$anonfun$1(build.sbt:1)
```

The workaround was to delete `project/target` directory.

**Solution**

The root cause was that imports were not included in the hash calculation when evaluating build.sbt expressions. When a val definition changes:
1. Its `$Wrap` module gets a new hash
2. Settings that reference the val get new imports pointing to the new module
3. But if the setting expression didn't change, its hash was the same
4. The old cached class was loaded with bytecode referencing the old module

Fix: Include imports in the hash calculation in `Eval.evalCommon`. Also re-enable `cleanEvalClasses` in `Load.scala` which was disabled pending this fix.
This commit is contained in:
Match 2026-01-13 15:45:16 -08:00 committed by GitHub
parent dc0d055069
commit 727f4b05c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 16 additions and 5 deletions

View File

@ -216,6 +216,10 @@ class Eval(
digester.update(bytes(ev.extraHash))
// Include SNAPSHOT classpath hash to invalidate cache when sbt version changes (fixes #7713)
digester.update(bytes(snapshotClasspathHash))
// Include imports in hash to invalidate cache when definition module names change (fixes #7424)
imports.strings.foreach { imp =>
digester.update(bytes(imp))
}
val d = digester.digest()
val hash = Hash.toHex(d)
val moduleName = makeModuleName(hash)

View File

@ -866,11 +866,9 @@ private[sbt] object Load {
)
}
val loadedProjects = processAutoAggregate(loadedProjects0, uri)
// TODO: Uncomment when we fixed https://github.com/sbt/sbt/issues/7424
// likely keepClassFiles isn't covering enough.
// timed("Load.loadUnit: cleanEvalClasses", log) {
// cleanEvalClasses(defDir, keepClassFiles)
// }
timed("Load.loadUnit: cleanEvalClasses", log) {
cleanEvalClasses(defDir, keepClassFiles)
}
val defs = if (defsScala.isEmpty) defaultBuildIfNone :: Nil else defsScala
// HERE we pull out the defined vals from memoSettings and unify them all so
// we can use them later.

View File

@ -0,0 +1,2 @@
val myVersion = "1.0.0"
name := myVersion

View File

@ -0,0 +1,2 @@
val myVersion = "2.0.0"
name := myVersion

View File

@ -0,0 +1,5 @@
# https://github.com/sbt/sbt/issues/7424
> name
$ copy-file changes/build.sbt build.sbt
> reload
> name