From e0bdb531f73bc2737fd64339614896f7ef908c5a Mon Sep 17 00:00:00 2001 From: BitToby <218712309+bittoby@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:44:08 -0600 Subject: [PATCH] [2.x] fix: Widen IOException catch in organicTask to cover result serialization (#9050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem** When a file referenced during cache serialization is deleted between task execution and cache storage, sbt crashes with an uncaught NoSuchFileException. The IOException catch added in #8699 only wraps store.put(), but Converter.toJsonUnsafe(result) and mkInput() also call hashedVirtualFileRefToStr which calls Files.readAttributes — and these are outside the try-catch. **Solution** Move the try/catch IOException to wrap the entire post-action cache storage section, so NoSuchFileException from result serialization or cache key computation is caught and handled gracefully (skip caching, return result). Fixes #9044 Co-authored-by: bittoby <218712309+bittoby@users.noreply.github.co> --- .../src/main/scala/sbt/util/ActionCache.scala | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/util-cache/src/main/scala/sbt/util/ActionCache.scala b/util-cache/src/main/scala/sbt/util/ActionCache.scala index 04c62f8f9..0b60013e3 100644 --- a/util-cache/src/main/scala/sbt/util/ActionCache.scala +++ b/util-cache/src/main/scala/sbt/util/ActionCache.scala @@ -82,40 +82,40 @@ object ActionCache: case e: Exception => cacheEventLog.append(ActionCacheEvent.Error) throw e - val json = Converter.toJsonUnsafe(result) - val normalizedOutputDir = outputDirectory.toAbsolutePath.normalize() - val uncacheableOutputs = - outputs.filter(f => - f match - case vf if vf.id.endsWith(ActionCache.dirZipExt) => - false - case _ => - val outputPath = fileConverter.toPath(f).toAbsolutePath.normalize() - !outputPath.startsWith(normalizedOutputDir) - ) - if uncacheableOutputs.nonEmpty then - cacheEventLog.append(ActionCacheEvent.Error) - logger.error( - s"Cannot cache task because its output files are outside the output directory: \n" + - uncacheableOutputs.mkString(" - ", "\n - ", "") - ) - result - else - cacheEventLog.append(ActionCacheEvent.OnsiteTask) - val (input, valuePath) = mkInput(key, codeContentHash, extraHash, config.cacheVersion) - val valueFile = StringVirtualFile1(valuePath, CompactPrinter(json)) - val newOutputs = Vector(valueFile) ++ outputs.toVector - try + try + val json = Converter.toJsonUnsafe(result) + val normalizedOutputDir = outputDirectory.toAbsolutePath.normalize() + val uncacheableOutputs = + outputs.filter(f => + f match + case vf if vf.id.endsWith(ActionCache.dirZipExt) => + false + case _ => + val outputPath = fileConverter.toPath(f).toAbsolutePath.normalize() + !outputPath.startsWith(normalizedOutputDir) + ) + if uncacheableOutputs.nonEmpty then + cacheEventLog.append(ActionCacheEvent.Error) + logger.error( + s"Cannot cache task because its output files are outside the output directory: \n" + + uncacheableOutputs.mkString(" - ", "\n - ", "") + ) + result + else + cacheEventLog.append(ActionCacheEvent.OnsiteTask) + val (input, valuePath) = mkInput(key, codeContentHash, extraHash, config.cacheVersion) + val valueFile = StringVirtualFile1(valuePath, CompactPrinter(json)) + val newOutputs = Vector(valueFile) ++ outputs.toVector store.put(UpdateActionResultRequest(input, newOutputs, exitCode = 0)) match case Right(cachedResult) => store.syncBlobs(cachedResult.outputFiles, outputDirectory) result case Left(e) => throw e - catch - case e: IOException => - logger.debug(s"Skipping cache storage due to error: ${e.getMessage}") - cacheEventLog.append(ActionCacheEvent.Error) - result + catch + case e: IOException => + logger.debug(s"Skipping cache storage due to error: ${e.getMessage}") + cacheEventLog.append(ActionCacheEvent.Error) + result // Single cache lookup - use exitCode to distinguish success from failure getWithFailure(key, codeContentHash, extraHash, tags, config) match