mirror of https://github.com/sbt/sbt.git
[2.x] fix: Report a missing input file clearly instead of an opaque SerializationException (#9271)
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: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ffa8c14ca6
commit
d4f54fd5b6
|
|
@ -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
|
||||
|
|
@ -10,10 +10,18 @@ package sbt.util
|
|||
|
||||
import java.io.{ File, IOException, PrintWriter }
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.{ AtomicMoveNotSupportedException, Files, Path, Paths, StandardCopyOption }
|
||||
import java.nio.file.{
|
||||
AtomicMoveNotSupportedException,
|
||||
Files,
|
||||
NoSuchFileException,
|
||||
Path,
|
||||
Paths,
|
||||
StandardCopyOption
|
||||
}
|
||||
import sbt.internal.util.{
|
||||
ActionCacheEvent,
|
||||
CacheEventLog,
|
||||
MessageOnlyException,
|
||||
SpawnExec,
|
||||
SpawnInput,
|
||||
StringVirtualFile1
|
||||
|
|
@ -295,10 +303,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 +324,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"
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package sbt.util
|
|||
|
||||
import java.io.{ IOException, InputStream }
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.{ Files, Path, Paths }
|
||||
import java.nio.file.{ Files, NoSuchFileException, Path, Paths }
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.{ CyclicBarrier, ExecutorService, Executors, TimeUnit }
|
||||
|
||||
|
|
@ -27,6 +27,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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue