[2.0.x] fix: Report a missing input file clearly instead of an opaque SerializationException (#9275)

When a file referenced by a task's inputs/outputs (e.g. `Compile / resources +=
file("nope.txt")`) does not exist, hashing the task's cache key threw a
NoSuchFileException deep inside sjsonnew serialization. It surfaced as an opaque
sjsonnew.SerializationException that dumped the entire input list, with the real
cause buried several `Caused by:` levels down, so users routinely mistook it for
a corrupt cache and reached for `clean`.

ActionCache.mkInput now catches the hashing failure, detects a NoSuchFileException
anywhere in the cause chain (ActionCache.findMissingFile), and throws a
MessageOnlyException naming the file:

    [error] file referenced by the build does not exist: nope.txt

util-cache gains a dependency on util-control (a leaf module, no cycle) for
MessageOnlyException.

Fixes #9217.

Co-authored-by: Brian Hotopp <brihoto@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
eugene yokota 2026-05-30 21:13:42 -04:00 committed by GitHub
parent a893b1c86d
commit f78a141dd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 53 additions and 3 deletions

View File

@ -365,7 +365,7 @@ lazy val utilCache = project
// we generate JsonCodec only for actionresult.contra
JsonCodecPlugin,
)
.dependsOn(utilLogging)
.dependsOn(utilLogging, utilControl)
.settings(
testedBaseSettings,
name := "Util Cache",

View File

@ -0,0 +1,17 @@
### Clearer error when a referenced file does not exist
If a file added to a task's inputs/outputs (e.g. `Compile / resources += file("nope.txt")`)
does not exist, tasks such as `package` previously failed while hashing the task's cache
key with an opaque `sjsonnew.SerializationException` that dumped the entire input list, with
the real cause (`NoSuchFileException`) buried several `Caused by:` levels down. This was
routinely mistaken for a corrupt cache.
sbt now reports the missing file directly:
```
[error] file referenced by the build does not exist: nope.txt
```
This addresses [#9217][i9217].
[i9217]: https://github.com/sbt/sbt/issues/9217

View File

@ -10,10 +10,11 @@ package sbt.util
import java.io.{ File, IOException, PrintWriter }
import java.nio.charset.StandardCharsets
import java.nio.file.{ Files, Path, Paths, StandardCopyOption }
import java.nio.file.{ Files, NoSuchFileException, Path, Paths, StandardCopyOption }
import sbt.internal.util.{
ActionCacheEvent,
CacheEventLog,
MessageOnlyException,
SpawnExec,
SpawnInput,
StringVirtualFile1
@ -295,10 +296,20 @@ object ActionCache:
extraHash: Digest,
cacheVersion: Long,
): Digest =
// Hashing serializes every task input; surface a missing input file directly rather than as an
// opaque serialization failure that buries it.
val inputHash =
try Hasher.hashUnsafe[I](key)
catch
case NonFatal(t) =>
findMissingFile(t) match
case Some(path) =>
throw MessageOnlyException(s"file referenced by the build does not exist: $path")
case None => throw t
Digest.sha256Hash(
(Vector(
codeContentHash,
Digest.dummy(Hasher.hashUnsafe[I](key)),
Digest.dummy(inputHash),
extraHash
) ++ {
if cacheVersion == 0 then Vector.empty
@ -306,6 +317,13 @@ object ActionCache:
})*
)
/** Walks `t`'s cause chain for a `NoSuchFileException`, returning the missing file's path. */
private[sbt] def findMissingFile(t: Throwable): Option[String] =
t match
case null => None
case e: NoSuchFileException => Option(e.getFile)
case _ => findMissingFile(t.getCause)
private inline def mkValuePath(inputDigest: Digest): String =
s"$${OUT}/value/${inputDigest}.json"

View File

@ -1,6 +1,7 @@
package sbt.util
import java.io.{ IOException, InputStream }
import java.nio.file.NoSuchFileException
import java.nio.charset.StandardCharsets
import sbt.internal.util.CacheEventLog
@ -27,6 +28,20 @@ import ActionCache.InternalActionResult
object ActionCacheTest extends BasicTestSuite:
val tags = CacheLevelTag.all.toList
test("findMissingFile extracts the path from a wrapped NoSuchFileException"):
val chain = new RuntimeException(
"error while writing LList",
new RuntimeException(
"error while writing the field sources",
new NoSuchFileException("not-exists.txt")
)
)
assert(ActionCache.findMissingFile(chain) == Some("not-exists.txt"))
test("findMissingFile returns None when no file is missing"):
val chain = new RuntimeException("boom", new IllegalStateException("unrelated"))
assert(ActionCache.findMissingFile(chain) == None)
test("Disk cache can hold a blob"):
withDiskCache(testHoldBlob)