diff --git a/notes/2.0.0/missing-file-error.md b/notes/2.0.0/missing-file-error.md new file mode 100644 index 000000000..27baec1dc --- /dev/null +++ b/notes/2.0.0/missing-file-error.md @@ -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 diff --git a/util-cache/src/main/scala/sbt/util/ActionCache.scala b/util-cache/src/main/scala/sbt/util/ActionCache.scala index ed36ac63c..376ed87d9 100644 --- a/util-cache/src/main/scala/sbt/util/ActionCache.scala +++ b/util-cache/src/main/scala/sbt/util/ActionCache.scala @@ -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" diff --git a/util-cache/src/test/scala/sbt/util/ActionCacheTest.scala b/util-cache/src/test/scala/sbt/util/ActionCacheTest.scala index 06bb870b2..5978aeee5 100644 --- a/util-cache/src/test/scala/sbt/util/ActionCacheTest.scala +++ b/util-cache/src/test/scala/sbt/util/ActionCacheTest.scala @@ -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)