2424import javax .inject .Provider ;
2525
2626import java .io .File ;
27- import java .io .FileNotFoundException ;
2827import java .io .IOException ;
2928import java .lang .reflect .Field ;
3029import java .lang .reflect .InvocationTargetException ;
4847import java .util .Set ;
4948import java .util .TreeSet ;
5049import java .util .UUID ;
50+ import java .util .concurrent .CompletableFuture ;
5151import java .util .concurrent .ConcurrentHashMap ;
5252import java .util .concurrent .ConcurrentMap ;
5353import java .util .concurrent .Future ;
@@ -479,23 +479,59 @@ private Future<File> createDownloadTask(
479479 CacheContext context ,
480480 MavenProject project ,
481481 Artifact artifact ,
482- String originalVersion ) {
483- final FutureTask <File > downloadTask = new FutureTask <>(() -> {
484- LOGGER .debug ("Downloading artifact {}" , artifact .getArtifactId ());
485- final Path artifactFile = localCache .getArtifactFile (context , cacheResult .getSource (), artifact );
482+ String originalVersion ) throws IOException {
483+
484+ // Check file existence BEFORE creating restoration task
485+ // This avoids exception-based control flow for the expected "missing file" condition
486+ final Path artifactFile = localCache .getArtifactFile (context , cacheResult .getSource (), artifact );
487+
488+ if (!Files .exists (artifactFile )) {
489+ LOGGER .warn ("Missing cached artifact file, cannot restore: {}" , artifactFile );
490+ // Return pre-failed FutureTask for uniform API
491+ FutureTask <File > failedTask = new FutureTask <>(() -> {
492+ throw new IOException ("Cached artifact file not found: " + artifactFile );
493+ });
494+ failedTask .run (); // Execute to capture the exception
495+ return failedTask ;
496+ }
486497
487- if (!Files .exists (artifactFile )) {
488- throw new FileNotFoundException ("Missing file for cached build, cannot restore. File: " + artifactFile );
489- }
490- LOGGER .debug ("Downloaded artifact {} to: {}" , artifact .getArtifactId (), artifactFile );
491- return restoreArtifactHandler
492- .adjustArchiveArtifactVersion (project , originalVersion , artifactFile )
493- .toFile ();
494- });
498+ // Create restoration task (file exists, so restoration should succeed barring I/O errors)
499+ FutureTask <File > task = new FutureTask <>(() -> restoreArtifactFromCache (
500+ context , cacheResult .getSource (), artifact , project , originalVersion ));
501+
502+ // Eager restore: execute immediately, return completed task with cached result
495503 if (!cacheConfig .isLazyRestore ()) {
496- downloadTask .run ();
504+ task .run ();
497505 }
498- return downloadTask ;
506+
507+ // Lazy restore: return task without executing (deferred until RestoredArtifact.getFile())
508+ return task ;
509+ }
510+
511+ /**
512+ * Restores a single artifact file from cache.
513+ *
514+ * <p>Precondition: File existence already verified by caller (createDownloadTask).
515+ * This method only handles true I/O failures, not the expected "missing file" case.
516+ *
517+ * @return the restored file
518+ * @throws IOException if I/O error occurs during restoration
519+ */
520+ private File restoreArtifactFromCache (
521+ CacheContext context ,
522+ CacheSource source ,
523+ Artifact artifact ,
524+ MavenProject project ,
525+ String originalVersion ) throws IOException {
526+
527+ LOGGER .debug ("Restoring artifact {} from cache" , artifact .getArtifactId ());
528+ final Path artifactFile = localCache .getArtifactFile (context , source , artifact );
529+
530+ // File existence already checked by caller
531+ // Any IOException here is truly exceptional (I/O error, not missing file)
532+ return restoreArtifactHandler
533+ .adjustArchiveArtifactVersion (project , originalVersion , artifactFile )
534+ .toFile ();
499535 }
500536
501537 @ Override
@@ -528,7 +564,8 @@ public void save(
528564 // Cache compile outputs (classes, test-classes, generated sources) if enabled
529565 // This allows compile-only builds to create restorable cache entries
530566 // Can be disabled with -Dmaven.build.cache.cacheCompile=false to reduce IO overhead
531- if (cacheConfig .isCacheCompile ()) {
567+ final boolean cacheCompile = cacheConfig .isCacheCompile ();
568+ if (cacheCompile ) {
532569 attachGeneratedSources (project , state , buildStartTime );
533570 attachOutputs (project , state , buildStartTime );
534571 }
@@ -540,6 +577,20 @@ public void save(
540577 final Artifact projectArtifactDto = hasPackagePhase ? artifactDto (project .getArtifact (), algorithm , project , state )
541578 : null ;
542579
580+ // CRITICAL: Don't create incomplete cache entries!
581+ // Only save cache entry if we have SOMETHING useful to restore.
582+ // Exclude consumer POMs (Maven metadata) from the "useful artifacts" check.
583+ // This prevents the bug where:
584+ // 1. mvn compile (cacheCompile=false) creates cache entry with only metadata
585+ // 2. mvn compile (cacheCompile=true) tries to restore incomplete cache and fails
586+ boolean hasUsefulArtifacts = projectArtifactDto != null
587+ || attachedArtifactDtos .stream ()
588+ .anyMatch (a -> !"consumer" .equals (a .getClassifier ()) || !"pom" .equals (a .getType ()));
589+ if (!hasUsefulArtifacts ) {
590+ LOGGER .info ("Skipping cache save: no artifacts to save (only metadata present)" );
591+ return ;
592+ }
593+
543594 List <CompletedExecution > completedExecution = buildExecutionInfo (mojoExecutions , executionEvents );
544595
545596 final Build build = new Build (
0 commit comments