1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.buildcache;
20
21 import javax.annotation.Nonnull;
22 import javax.inject.Inject;
23 import javax.inject.Named;
24 import javax.inject.Provider;
25
26 import java.io.File;
27 import java.io.FileNotFoundException;
28 import java.io.IOException;
29 import java.lang.reflect.Field;
30 import java.lang.reflect.InvocationTargetException;
31 import java.lang.reflect.Method;
32 import java.nio.charset.StandardCharsets;
33 import java.nio.file.FileAlreadyExistsException;
34 import java.nio.file.FileVisitResult;
35 import java.nio.file.Files;
36 import java.nio.file.Path;
37 import java.nio.file.Paths;
38 import java.nio.file.SimpleFileVisitor;
39 import java.nio.file.StandardCopyOption;
40 import java.nio.file.attribute.BasicFileAttributes;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Objects;
48 import java.util.Optional;
49 import java.util.Set;
50 import java.util.TreeSet;
51 import java.util.UUID;
52 import java.util.concurrent.ConcurrentHashMap;
53 import java.util.concurrent.ConcurrentMap;
54 import java.util.concurrent.Future;
55 import java.util.concurrent.FutureTask;
56 import java.util.concurrent.atomic.AtomicBoolean;
57 import java.util.function.UnaryOperator;
58 import java.util.regex.Pattern;
59
60 import org.apache.commons.io.FilenameUtils;
61 import org.apache.commons.lang3.StringUtils;
62 import org.apache.commons.lang3.Strings;
63 import org.apache.commons.lang3.mutable.MutableBoolean;
64 import org.apache.maven.SessionScoped;
65 import org.apache.maven.artifact.handler.ArtifactHandler;
66 import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
67 import org.apache.maven.buildcache.artifact.ArtifactRestorationReport;
68 import org.apache.maven.buildcache.artifact.OutputType;
69 import org.apache.maven.buildcache.artifact.RestoredArtifact;
70 import org.apache.maven.buildcache.checksum.MavenProjectInput;
71 import org.apache.maven.buildcache.hash.HashAlgorithm;
72 import org.apache.maven.buildcache.hash.HashFactory;
73 import org.apache.maven.buildcache.xml.Build;
74 import org.apache.maven.buildcache.xml.CacheConfig;
75 import org.apache.maven.buildcache.xml.CacheSource;
76 import org.apache.maven.buildcache.xml.DtoUtils;
77 import org.apache.maven.buildcache.xml.XmlService;
78 import org.apache.maven.buildcache.xml.build.Artifact;
79 import org.apache.maven.buildcache.xml.build.CompletedExecution;
80 import org.apache.maven.buildcache.xml.build.DigestItem;
81 import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
82 import org.apache.maven.buildcache.xml.build.Scm;
83 import org.apache.maven.buildcache.xml.config.DirName;
84 import org.apache.maven.buildcache.xml.config.PropertyName;
85 import org.apache.maven.buildcache.xml.config.TrackedProperty;
86 import org.apache.maven.buildcache.xml.diff.Diff;
87 import org.apache.maven.buildcache.xml.report.CacheReport;
88 import org.apache.maven.buildcache.xml.report.ProjectReport;
89 import org.apache.maven.execution.MavenSession;
90 import org.apache.maven.execution.MojoExecutionEvent;
91 import org.apache.maven.plugin.MojoExecution;
92 import org.apache.maven.plugin.descriptor.Parameter;
93 import org.apache.maven.project.MavenProject;
94 import org.apache.maven.project.MavenProjectHelper;
95 import org.codehaus.plexus.util.ReflectionUtils;
96 import org.eclipse.aether.RepositorySystem;
97 import org.slf4j.Logger;
98 import org.slf4j.LoggerFactory;
99
100 import static java.nio.file.StandardOpenOption.CREATE;
101 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
102 import static org.apache.commons.lang3.StringUtils.isNotBlank;
103 import static org.apache.commons.lang3.StringUtils.split;
104 import static org.apache.maven.buildcache.CacheResult.empty;
105 import static org.apache.maven.buildcache.CacheResult.failure;
106 import static org.apache.maven.buildcache.CacheResult.partialSuccess;
107 import static org.apache.maven.buildcache.CacheResult.success;
108 import static org.apache.maven.buildcache.RemoteCacheRepository.BUILDINFO_XML;
109 import static org.apache.maven.buildcache.checksum.KeyUtils.getVersionlessProjectKey;
110 import static org.apache.maven.buildcache.checksum.MavenProjectInput.CACHE_IMPLEMENTATION_VERSION;
111
112
113
114
115 @SessionScoped
116 @Named
117 @SuppressWarnings("unused")
118 public class CacheControllerImpl implements CacheController {
119
120 private static final Logger LOGGER = LoggerFactory.getLogger(CacheControllerImpl.class);
121 private static final String DEFAULT_FILE_GLOB = "*";
122 public static final String ERROR_MSG_RESTORATION_OUTSIDE_PROJECT =
123 "Blocked an attempt to restore files outside of a project directory: ";
124
125 private final MavenProjectHelper projectHelper;
126 private final ArtifactHandlerManager artifactHandlerManager;
127 private final XmlService xmlService;
128 private final CacheConfig cacheConfig;
129 private final LocalCacheRepository localCache;
130 private final RemoteCacheRepository remoteCache;
131 private final ConcurrentMap<String, CacheResult> cacheResults = new ConcurrentHashMap<>();
132 private final Provider<LifecyclePhasesHelper> providerLifecyclePhasesHelper;
133 private volatile Map<String, MavenProject> projectIndex;
134 private final ProjectInputCalculator projectInputCalculator;
135 private final RestoredArtifactHandler restoreArtifactHandler;
136 private volatile Scm scm;
137
138
139
140
141
142 private static class ProjectCacheState {
143 final Map<String, Path> attachedResourcesPathsById = new HashMap<>();
144 int attachedResourceCounter = 0;
145 final Set<String> restoredOutputClassifiers = new HashSet<>();
146
147
148
149
150
151 Path stagingDirectory;
152 }
153
154 private final ConcurrentMap<String, ProjectCacheState> projectStates = new ConcurrentHashMap<>();
155
156
157
158
159 private ProjectCacheState getProjectState(MavenProject project) {
160 String key = getVersionlessProjectKey(project);
161 return projectStates.computeIfAbsent(key, k -> new ProjectCacheState());
162 }
163
164 @Inject
165 public CacheControllerImpl(
166 MavenProjectHelper projectHelper,
167 RepositorySystem repoSystem,
168 ArtifactHandlerManager artifactHandlerManager,
169 XmlService xmlService,
170 LocalCacheRepository localCache,
171 RemoteCacheRepository remoteCache,
172 CacheConfig cacheConfig,
173 ProjectInputCalculator projectInputCalculator,
174 RestoredArtifactHandler restoreArtifactHandler,
175 Provider<LifecyclePhasesHelper> providerLifecyclePhasesHelper) {
176
177 this.projectHelper = projectHelper;
178 this.localCache = localCache;
179 this.remoteCache = remoteCache;
180 this.cacheConfig = cacheConfig;
181 this.artifactHandlerManager = artifactHandlerManager;
182 this.xmlService = xmlService;
183 this.providerLifecyclePhasesHelper = providerLifecyclePhasesHelper;
184 this.projectInputCalculator = projectInputCalculator;
185 this.restoreArtifactHandler = restoreArtifactHandler;
186 }
187
188 @Override
189 @Nonnull
190 public CacheResult findCachedBuild(
191 MavenSession session, MavenProject project, List<MojoExecution> mojoExecutions, boolean skipCache) {
192 final LifecyclePhasesHelper lifecyclePhasesHelper = providerLifecyclePhasesHelper.get();
193 final String highestPhase = lifecyclePhasesHelper.resolveHighestLifecyclePhase(project, mojoExecutions);
194
195 if (!lifecyclePhasesHelper.isLaterPhaseThanClean(highestPhase)) {
196 return empty();
197 }
198
199 String projectName = getVersionlessProjectKey(project);
200
201 ProjectsInputInfo inputInfo = projectInputCalculator.calculateInput(project);
202
203 final CacheContext context = new CacheContext(project, inputInfo, session);
204
205 CacheResult result = empty(context);
206 if (!skipCache) {
207
208 LOGGER.info("Attempting to restore project {} from build cache", projectName);
209
210
211 if (cacheConfig.isRemoteCacheEnabled()) {
212 result = findCachedBuild(mojoExecutions, context);
213 if (!result.isSuccess() && result.getContext() != null) {
214 LOGGER.info("Remote cache is incomplete or missing, trying local build for {}", projectName);
215 }
216 }
217
218 if (!result.isSuccess() && result.getContext() != null) {
219 CacheResult localBuild = findLocalBuild(mojoExecutions, context);
220 if (localBuild.isSuccess() || (localBuild.isPartialSuccess() && !result.isPartialSuccess())) {
221 result = localBuild;
222 } else {
223 LOGGER.info(
224 "Local build was not found by checksum {} for {}", inputInfo.getChecksum(), projectName);
225 }
226 }
227 } else {
228 LOGGER.info(
229 "Project {} is marked as requiring force rebuild, will skip lookup in build cache", projectName);
230 }
231 cacheResults.put(getVersionlessProjectKey(project), result);
232
233 return result;
234 }
235
236 private CacheResult findCachedBuild(List<MojoExecution> mojoExecutions, CacheContext context) {
237 Optional<Build> cachedBuild = Optional.empty();
238 try {
239 cachedBuild = localCache.findBuild(context);
240 if (cachedBuild.isPresent()) {
241 return analyzeResult(context, mojoExecutions, cachedBuild.get());
242 }
243 } catch (Exception e) {
244 LOGGER.error("Cannot read cached remote build", e);
245 }
246 return cachedBuild.map(build -> failure(build, context)).orElseGet(() -> empty(context));
247 }
248
249 private CacheResult findLocalBuild(List<MojoExecution> mojoExecutions, CacheContext context) {
250 Optional<Build> localBuild = Optional.empty();
251 try {
252 localBuild = localCache.findLocalBuild(context);
253 if (localBuild.isPresent()) {
254 return analyzeResult(context, mojoExecutions, localBuild.get());
255 }
256 } catch (Exception e) {
257 LOGGER.error("Cannot read local build", e);
258 }
259 return localBuild.map(build -> failure(build, context)).orElseGet(() -> empty(context));
260 }
261
262 private CacheResult analyzeResult(CacheContext context, List<MojoExecution> mojoExecutions, Build build) {
263 try {
264 final ProjectsInputInfo inputInfo = context.getInputInfo();
265 String projectName = getVersionlessProjectKey(context.getProject());
266
267 LOGGER.info(
268 "Found cached build, restoring {} from cache by checksum {}", projectName, inputInfo.getChecksum());
269 LOGGER.debug("Cached build details: {}", build);
270
271 final String cacheImplementationVersion = build.getCacheImplementationVersion();
272 if (!CACHE_IMPLEMENTATION_VERSION.equals(cacheImplementationVersion)) {
273 LOGGER.warn(
274 "Maven and cached build implementations mismatch, caching might not work correctly. "
275 + "Implementation version: " + CACHE_IMPLEMENTATION_VERSION + ", cached build: {}",
276 build.getCacheImplementationVersion());
277 }
278
279 final LifecyclePhasesHelper lifecyclePhasesHelper = providerLifecyclePhasesHelper.get();
280 List<MojoExecution> cachedSegment =
281 lifecyclePhasesHelper.getCachedSegment(context.getProject(), mojoExecutions, build);
282 List<MojoExecution> missingMojos = build.getMissingExecutions(cachedSegment);
283
284 if (!missingMojos.isEmpty()) {
285 LOGGER.warn(
286 "Cached build doesn't contains all requested plugin executions "
287 + "(missing: {}), cannot restore",
288 missingMojos);
289 return failure(build, context);
290 }
291
292 if (!isCachedSegmentPropertiesPresent(context.getProject(), build, cachedSegment)) {
293 LOGGER.info("Cached build violates cache rules, cannot restore");
294 return failure(build, context);
295 }
296
297 final String highestRequestPhase =
298 lifecyclePhasesHelper.resolveHighestLifecyclePhase(context.getProject(), mojoExecutions);
299
300 if (lifecyclePhasesHelper.isLaterPhaseThanBuild(highestRequestPhase, build)
301 && !canIgnoreMissingSegment(context.getProject(), build, mojoExecutions)) {
302 LOGGER.info(
303 "Project {} restored partially. Highest cached goal: {}, requested: {}",
304 projectName,
305 build.getHighestCompletedGoal(),
306 highestRequestPhase);
307 return partialSuccess(build, context);
308 }
309
310 return success(build, context);
311
312 } catch (Exception e) {
313 LOGGER.error("Failed to restore project", e);
314 localCache.clearCache(context);
315 return failure(build, context);
316 }
317 }
318
319 private boolean canIgnoreMissingSegment(MavenProject project, Build info, List<MojoExecution> mojoExecutions) {
320 final LifecyclePhasesHelper lifecyclePhasesHelper = providerLifecyclePhasesHelper.get();
321 final List<MojoExecution> postCachedSegment =
322 lifecyclePhasesHelper.getPostCachedSegment(project, mojoExecutions, info);
323
324 for (MojoExecution mojoExecution : postCachedSegment) {
325 if (!cacheConfig.canIgnore(mojoExecution)) {
326 return false;
327 }
328 }
329 return true;
330 }
331
332 private UnaryOperator<File> createRestorationToDiskConsumer(final MavenProject project, final Artifact artifact) {
333
334 if (cacheConfig.isRestoreOnDiskArtifacts() && MavenProjectInput.isRestoreOnDiskArtifacts(project)) {
335 Path restorationPath = project.getBasedir().toPath().resolve(artifact.getFilePath());
336 final AtomicBoolean restored = new AtomicBoolean(false);
337 return file -> {
338
339 if (restored.compareAndSet(false, true)) {
340 verifyRestorationInsideProject(project, restorationPath);
341 try {
342 restoreArtifactToDisk(file, artifact, restorationPath);
343 } catch (IOException e) {
344 LOGGER.error("Cannot restore file " + artifact.getFileName(), e);
345 throw new RuntimeException(e);
346 }
347 }
348 return restorationPath.toFile();
349 };
350 }
351
352 return file -> file;
353 }
354
355
356
357
358
359 private void restoreArtifactToDisk(File cachedFile, Artifact artifact, Path restorationPath) throws IOException {
360
361
362 if (artifact.isIsDirectory()) {
363 restoreDirectoryArtifact(cachedFile, artifact, restorationPath);
364 } else {
365 restoreRegularFileArtifact(cachedFile, artifact, restorationPath);
366 }
367 }
368
369
370
371
372 private void restoreDirectoryArtifact(File cachedZip, Artifact artifact, Path restorationPath) throws IOException {
373 if (!Files.exists(restorationPath)) {
374 Files.createDirectories(restorationPath);
375 }
376 CacheUtils.unzip(cachedZip.toPath(), restorationPath, cacheConfig.isPreservePermissions());
377 LOGGER.debug("Restored directory artifact by unzipping: {} -> {}", artifact.getFileName(), restorationPath);
378 }
379
380
381
382
383 private void restoreRegularFileArtifact(File cachedFile, Artifact artifact, Path restorationPath)
384 throws IOException {
385 Files.createDirectories(restorationPath.getParent());
386 Files.copy(cachedFile.toPath(), restorationPath, StandardCopyOption.REPLACE_EXISTING);
387 LOGGER.debug("Restored file on disk ({} to {})", artifact.getFileName(), restorationPath);
388 }
389
390 private boolean isPathInsideProject(final MavenProject project, Path path) {
391 Path restorationPath = path.toAbsolutePath().normalize();
392 return restorationPath.startsWith(project.getBasedir().toPath());
393 }
394
395 private void verifyRestorationInsideProject(final MavenProject project, Path path) {
396 if (!isPathInsideProject(project, path)) {
397 Path normalized = path.toAbsolutePath().normalize();
398 LOGGER.error(ERROR_MSG_RESTORATION_OUTSIDE_PROJECT + normalized);
399 throw new RuntimeException(ERROR_MSG_RESTORATION_OUTSIDE_PROJECT + normalized);
400 }
401 }
402
403 @Override
404 public ArtifactRestorationReport restoreProjectArtifacts(CacheResult cacheResult) {
405
406 LOGGER.debug("Restore project artifacts");
407 final Build build = cacheResult.getBuildInfo();
408 final CacheContext context = cacheResult.getContext();
409 final MavenProject project = context.getProject();
410 final ProjectCacheState state = getProjectState(project);
411 ArtifactRestorationReport restorationReport = new ArtifactRestorationReport();
412
413 try {
414 RestoredArtifact restoredProjectArtifact = null;
415 List<RestoredArtifact> restoredAttachedArtifacts = new ArrayList<>();
416
417 if (build.getArtifact() != null && isNotBlank(build.getArtifact().getFileName())) {
418 final Artifact artifactInfo = build.getArtifact();
419 String originalVersion = artifactInfo.getVersion();
420 artifactInfo.setVersion(project.getVersion());
421
422 final Future<File> downloadTask =
423 createDownloadTask(cacheResult, context, project, artifactInfo, originalVersion);
424 restoredProjectArtifact = restoredArtifact(
425 project.getArtifact(),
426 artifactInfo.getType(),
427 artifactInfo.getClassifier(),
428 downloadTask,
429 createRestorationToDiskConsumer(project, artifactInfo));
430 if (!cacheConfig.isLazyRestore()) {
431 restoredProjectArtifact.getFile();
432 }
433 }
434
435 for (Artifact attachedArtifactInfo : build.getAttachedArtifacts()) {
436 String originalVersion = attachedArtifactInfo.getVersion();
437 attachedArtifactInfo.setVersion(project.getVersion());
438 if (isNotBlank(attachedArtifactInfo.getFileName())) {
439 OutputType outputType = OutputType.fromClassifier(attachedArtifactInfo.getClassifier());
440 if (OutputType.ARTIFACT != outputType) {
441
442
443
444 if (cacheConfig.isRestoreGeneratedSources()
445 && MavenProjectInput.isRestoreGeneratedSources(project)) {
446
447 restorationReport.setRestoredFilesInProjectDirectory(true);
448
449 final Path attachedArtifactFile =
450 localCache.getArtifactFile(context, cacheResult.getSource(), attachedArtifactInfo);
451 restoreGeneratedSources(attachedArtifactInfo, attachedArtifactFile, project);
452
453 state.restoredOutputClassifiers.add(attachedArtifactInfo.getClassifier());
454 }
455 } else {
456 Future<File> downloadTask = createDownloadTask(
457 cacheResult, context, project, attachedArtifactInfo, originalVersion);
458 final RestoredArtifact restoredAttachedArtifact = restoredArtifact(
459 restoredProjectArtifact == null ? project.getArtifact() : restoredProjectArtifact,
460 attachedArtifactInfo.getType(),
461 attachedArtifactInfo.getClassifier(),
462 downloadTask,
463 createRestorationToDiskConsumer(project, attachedArtifactInfo));
464 if (!cacheConfig.isLazyRestore()) {
465 restoredAttachedArtifact.getFile();
466 }
467 restoredAttachedArtifacts.add(restoredAttachedArtifact);
468 }
469 }
470 }
471
472
473 if (restoredProjectArtifact != null) {
474 project.setArtifact(restoredProjectArtifact);
475
476 if (!project.hasLifecyclePhase("package")) {
477 project.addLifecyclePhase("package");
478 }
479 }
480 restoredAttachedArtifacts.forEach(project::addAttachedArtifact);
481 restorationReport.setSuccess(true);
482 } catch (Exception e) {
483 LOGGER.debug("Cannot restore cache, continuing with normal build.", e);
484 }
485 return restorationReport;
486 }
487
488
489
490
491
492 private RestoredArtifact restoredArtifact(
493 org.apache.maven.artifact.Artifact parent,
494 String artifactType,
495 String artifactClassifier,
496 Future<File> artifactFile,
497 UnaryOperator<File> restoreToDiskConsumer) {
498 ArtifactHandler handler = null;
499
500 if (artifactType != null) {
501 handler = artifactHandlerManager.getArtifactHandler(artifactType);
502 }
503
504 if (handler == null) {
505 handler = artifactHandlerManager.getArtifactHandler("jar");
506 }
507
508
509 RestoredArtifact artifact = new RestoredArtifact(
510 parent, artifactFile, artifactType, artifactClassifier, handler, restoreToDiskConsumer);
511 artifact.setResolved(true);
512
513 return artifact;
514 }
515
516 private Future<File> createDownloadTask(
517 CacheResult cacheResult,
518 CacheContext context,
519 MavenProject project,
520 Artifact artifact,
521 String originalVersion) {
522 final FutureTask<File> downloadTask = new FutureTask<>(() -> {
523 LOGGER.debug("Downloading artifact {}", artifact.getArtifactId());
524 final Path artifactFile = localCache.getArtifactFile(context, cacheResult.getSource(), artifact);
525
526 if (!Files.exists(artifactFile)) {
527 throw new FileNotFoundException("Missing file for cached build, cannot restore. File: " + artifactFile);
528 }
529 LOGGER.debug("Downloaded artifact {} to: {}", artifact.getArtifactId(), artifactFile);
530 return restoreArtifactHandler
531 .adjustArchiveArtifactVersion(project, originalVersion, artifactFile)
532 .toFile();
533 });
534 if (!cacheConfig.isLazyRestore()) {
535 downloadTask.run();
536 }
537 return downloadTask;
538 }
539
540 @Override
541 public void save(
542 CacheResult cacheResult,
543 List<MojoExecution> mojoExecutions,
544 Map<String, MojoExecutionEvent> executionEvents) {
545 CacheContext context = cacheResult.getContext();
546
547 if (context == null || context.getInputInfo() == null) {
548 LOGGER.info("Cannot save project in cache, skipping");
549 return;
550 }
551
552 final MavenProject project = context.getProject();
553 final MavenSession session = context.getSession();
554 final ProjectCacheState state = getProjectState(project);
555 try {
556 state.attachedResourcesPathsById.clear();
557 state.attachedResourceCounter = 0;
558
559
560 final long buildStartTime = session.getRequest().getStartTime().getTime();
561
562 final HashFactory hashFactory = cacheConfig.getHashFactory();
563 final HashAlgorithm algorithm = hashFactory.createAlgorithm();
564 final org.apache.maven.artifact.Artifact projectArtifact = project.getArtifact();
565
566
567
568
569 final boolean cacheCompile = cacheConfig.isCacheCompile();
570 if (cacheCompile) {
571 attachGeneratedSources(project, state, buildStartTime);
572 attachOutputs(project, state, buildStartTime);
573 }
574
575 final List<org.apache.maven.artifact.Artifact> attachedArtifacts =
576 project.getAttachedArtifacts() != null ? project.getAttachedArtifacts() : Collections.emptyList();
577 final List<Artifact> attachedArtifactDtos = artifactDtos(attachedArtifacts, algorithm, project, state);
578
579
580 final Artifact projectArtifactDto = artifactDto(project.getArtifact(), algorithm, project, state);
581
582 List<CompletedExecution> completedExecution = buildExecutionInfo(mojoExecutions, executionEvents);
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602 boolean hasArtifactFile = projectArtifact.getFile() != null
603 && projectArtifact.getFile().exists()
604 && (projectArtifact.getFile().isFile()
605 || (cacheCompile && projectArtifact.getFile().isDirectory()));
606 boolean hasAttachedArtifacts = !attachedArtifactDtos.isEmpty()
607 && attachedArtifactDtos.stream()
608 .anyMatch(a -> !"consumer".equals(a.getClassifier()) || !"pom".equals(a.getType()));
609
610 boolean isPomProjectWithWork = "pom".equals(project.getPackaging()) && !completedExecution.isEmpty();
611
612 if (!hasArtifactFile && !hasAttachedArtifacts && !isPomProjectWithWork) {
613 LOGGER.info(
614 "Skipping cache save: no artifacts to save ({}only metadata present)",
615 cacheCompile ? "" : "cacheCompile=false, ");
616 return;
617 }
618
619 final Build build = new Build(
620 session.getGoals(),
621 projectArtifactDto,
622 attachedArtifactDtos,
623 context.getInputInfo(),
624 completedExecution,
625 hashFactory.getAlgorithm());
626 populateGitInfo(build, session);
627 build.getDto().set_final(cacheConfig.isSaveToRemoteFinal());
628 cacheResults.put(getVersionlessProjectKey(project), CacheResult.rebuilt(cacheResult, build));
629
630 localCache.beforeSave(context);
631
632
633 if (projectArtifact.getFile() != null) {
634 saveProjectArtifact(cacheResult, projectArtifact, project);
635 }
636 for (org.apache.maven.artifact.Artifact attachedArtifact : attachedArtifacts) {
637 if (attachedArtifact.getFile() != null) {
638 boolean storeArtifact =
639 isOutputArtifact(attachedArtifact.getFile().getName());
640 if (storeArtifact) {
641 localCache.saveArtifactFile(cacheResult, attachedArtifact);
642 } else {
643 LOGGER.debug(
644 "Skipping attached project artifact '{}' = "
645 + " it is marked for exclusion from caching",
646 attachedArtifact.getFile().getName());
647 }
648 }
649 }
650
651 localCache.saveBuildInfo(cacheResult, build);
652
653 if (cacheConfig.isBaselineDiffEnabled()) {
654 produceDiffReport(cacheResult, build);
655 }
656
657 } catch (Exception e) {
658 LOGGER.error("Failed to save project, cleaning cache. Project: {}", project, e);
659 try {
660 localCache.clearCache(context);
661 } catch (Exception ex) {
662 LOGGER.error("Failed to clean cache due to unexpected error:", ex);
663 }
664 } finally {
665
666
667 state.attachedResourcesPathsById.clear();
668 state.attachedResourceCounter = 0;
669 state.restoredOutputClassifiers.clear();
670
671 }
672 }
673
674
675
676
677
678
679 private void saveProjectArtifact(
680 CacheResult cacheResult, org.apache.maven.artifact.Artifact projectArtifact, MavenProject project)
681 throws IOException {
682 File originalFile = projectArtifact.getFile();
683 try {
684 if (originalFile.isDirectory()) {
685 saveDirectoryArtifact(cacheResult, projectArtifact, project, originalFile);
686 } else {
687
688 localCache.saveArtifactFile(cacheResult, projectArtifact);
689 }
690 } finally {
691
692 projectArtifact.setFile(originalFile);
693 }
694 }
695
696
697
698
699 private void saveDirectoryArtifact(
700 CacheResult cacheResult,
701 org.apache.maven.artifact.Artifact projectArtifact,
702 MavenProject project,
703 File originalFile)
704 throws IOException {
705 Path tempZip = Files.createTempFile("maven-cache-", "-" + project.getArtifactId() + ".zip");
706 boolean hasFiles = CacheUtils.zip(originalFile.toPath(), tempZip, "*", cacheConfig.isPreservePermissions());
707 if (hasFiles) {
708
709 projectArtifact.setFile(tempZip.toFile());
710 localCache.saveArtifactFile(cacheResult, projectArtifact);
711 LOGGER.debug("Saved directory artifact as zip: {} -> {}", originalFile, tempZip);
712
713 Files.deleteIfExists(tempZip);
714 } else {
715 LOGGER.info("Skipping empty directory artifact: {}", originalFile);
716 }
717 }
718
719 public void produceDiffReport(CacheResult cacheResult, Build build) {
720 MavenProject project = cacheResult.getContext().getProject();
721 Optional<Build> baselineHolder = remoteCache.findBaselineBuild(project);
722 if (baselineHolder.isPresent()) {
723 Build baseline = baselineHolder.get();
724 String outputDirectory = project.getBuild().getDirectory();
725 Path reportOutputDir = Paths.get(outputDirectory, "incremental-maven");
726 LOGGER.info("Saving cache builds diff to: {}", reportOutputDir);
727 Diff diff = new CacheDiff(build.getDto(), baseline.getDto(), cacheConfig).compare();
728 try {
729 Files.createDirectories(reportOutputDir);
730 final ProjectsInputInfo baselineInputs = baseline.getDto().getProjectsInputInfo();
731 final String checksum = baselineInputs.getChecksum();
732 Files.write(
733 reportOutputDir.resolve("buildinfo-baseline-" + checksum + ".xml"),
734 xmlService.toBytes(baseline.getDto()),
735 TRUNCATE_EXISTING,
736 CREATE);
737 Files.write(
738 reportOutputDir.resolve("buildinfo-" + checksum + ".xml"),
739 xmlService.toBytes(build.getDto()),
740 TRUNCATE_EXISTING,
741 CREATE);
742 Files.write(
743 reportOutputDir.resolve("buildsdiff-" + checksum + ".xml"),
744 xmlService.toBytes(diff),
745 TRUNCATE_EXISTING,
746 CREATE);
747 final Optional<DigestItem> pom =
748 CacheDiff.findPom(build.getDto().getProjectsInputInfo());
749 if (pom.isPresent()) {
750 Files.write(
751 reportOutputDir.resolve("effective-pom-" + checksum + ".xml"),
752 pom.get().getValue().getBytes(StandardCharsets.UTF_8),
753 TRUNCATE_EXISTING,
754 CREATE);
755 }
756 final Optional<DigestItem> baselinePom = CacheDiff.findPom(baselineInputs);
757 if (baselinePom.isPresent()) {
758 Files.write(
759 reportOutputDir.resolve("effective-pom-baseline-" + baselineInputs.getChecksum() + ".xml"),
760 baselinePom.get().getValue().getBytes(StandardCharsets.UTF_8),
761 TRUNCATE_EXISTING,
762 CREATE);
763 }
764 } catch (IOException e) {
765 LOGGER.error("Cannot produce build diff for project", e);
766 }
767 } else {
768 LOGGER.info("Cannot find project in baseline build, skipping diff");
769 }
770 }
771
772 private List<Artifact> artifactDtos(
773 List<org.apache.maven.artifact.Artifact> attachedArtifacts,
774 HashAlgorithm digest,
775 MavenProject project,
776 ProjectCacheState state)
777 throws IOException {
778 List<Artifact> result = new ArrayList<>();
779 for (org.apache.maven.artifact.Artifact attachedArtifact : attachedArtifacts) {
780 if (attachedArtifact.getFile() != null
781 && isOutputArtifact(attachedArtifact.getFile().getName())) {
782 result.add(artifactDto(attachedArtifact, digest, project, state));
783 }
784 }
785 return result;
786 }
787
788 private Artifact artifactDto(
789 org.apache.maven.artifact.Artifact projectArtifact,
790 HashAlgorithm algorithm,
791 MavenProject project,
792 ProjectCacheState state)
793 throws IOException {
794 final Artifact dto = DtoUtils.createDto(projectArtifact);
795 if (projectArtifact.getFile() != null) {
796 final Path file = projectArtifact.getFile().toPath();
797
798
799 if (Files.isRegularFile(file)) {
800 dto.setFileHash(algorithm.hash(file));
801 dto.setFileSize(Files.size(file));
802 } else if (Files.isDirectory(file)) {
803
804 dto.setIsDirectory(true);
805 }
806
807
808
809 Path relativePath = state.attachedResourcesPathsById.get(projectArtifact.getClassifier());
810 if (relativePath == null) {
811
812
813 relativePath = project.getBasedir().toPath().relativize(file.toAbsolutePath());
814 }
815 dto.setFilePath(FilenameUtils.separatorsToUnix(relativePath.toString()));
816 }
817 return dto;
818 }
819
820 private List<CompletedExecution> buildExecutionInfo(
821 List<MojoExecution> mojoExecutions, Map<String, MojoExecutionEvent> executionEvents) {
822 List<CompletedExecution> list = new ArrayList<>();
823 for (MojoExecution mojoExecution : mojoExecutions) {
824 final String executionKey = CacheUtils.mojoExecutionKey(mojoExecution);
825 final MojoExecutionEvent executionEvent =
826 executionEvents != null ? executionEvents.get(executionKey) : null;
827 CompletedExecution executionInfo = new CompletedExecution();
828 executionInfo.setExecutionKey(executionKey);
829 executionInfo.setMojoClassName(mojoExecution.getMojoDescriptor().getImplementation());
830 if (executionEvent != null) {
831 recordMojoProperties(executionInfo, executionEvent);
832 }
833 list.add(executionInfo);
834 }
835 return list;
836 }
837
838 private void recordMojoProperties(CompletedExecution execution, MojoExecutionEvent executionEvent) {
839 final MojoExecution mojoExecution = executionEvent.getExecution();
840
841 final boolean logAll = cacheConfig.isLogAllProperties(mojoExecution);
842 List<TrackedProperty> trackedProperties = cacheConfig.getTrackedProperties(mojoExecution);
843 List<PropertyName> noLogProperties = cacheConfig.getNologProperties(mojoExecution);
844 List<PropertyName> forceLogProperties = cacheConfig.getLoggedProperties(mojoExecution);
845 final Object mojo = executionEvent.getMojo();
846
847 final File baseDir = executionEvent.getProject().getBasedir();
848 final String baseDirPath = FilenameUtils.normalizeNoEndSeparator(baseDir.getAbsolutePath()) + File.separator;
849
850 final List<Parameter> parameters = mojoExecution.getMojoDescriptor().getParameters();
851 for (Parameter parameter : parameters) {
852
853 if (!parameter.isEditable()) {
854 continue;
855 }
856
857 final String propertyName = parameter.getName();
858 final boolean tracked = isTracked(propertyName, trackedProperties);
859 if (!tracked && isExcluded(propertyName, logAll, noLogProperties, forceLogProperties)) {
860 continue;
861 }
862
863 try {
864 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(propertyName, mojo.getClass());
865 if (field != null) {
866 final Object value = ReflectionUtils.getValueIncludingSuperclasses(propertyName, mojo);
867 DtoUtils.addProperty(execution, propertyName, value, baseDirPath, tracked);
868 continue;
869 }
870
871 Method getter = getGetter(propertyName, mojo.getClass());
872 if (getter != null) {
873 Object value = getter.invoke(mojo);
874 DtoUtils.addProperty(execution, propertyName, value, baseDirPath, tracked);
875 continue;
876 }
877
878 if (LOGGER.isWarnEnabled()) {
879 LOGGER.warn(
880 "Cannot find a Mojo parameter '{}' to read for Mojo {}. This parameter should be ignored.",
881 propertyName,
882 mojoExecution);
883 }
884
885 } catch (IllegalAccessException | InvocationTargetException e) {
886 LOGGER.info("Cannot get property {} value from {}: {}", propertyName, mojo, e.getMessage());
887 if (tracked) {
888 throw new IllegalArgumentException("Property configured in cache introspection config " + "for "
889 + mojo + " is not accessible: " + propertyName);
890 }
891 }
892 }
893 }
894
895 private static Method getGetter(String fieldName, Class<?> clazz) {
896 String getterMethodName = "get" + org.codehaus.plexus.util.StringUtils.capitalizeFirstLetter(fieldName);
897 Method[] methods = clazz.getMethods();
898 for (Method method : methods) {
899 if (method.getName().equals(getterMethodName)
900 && !method.getReturnType().equals(Void.TYPE)
901 && method.getParameterCount() == 0) {
902 return method;
903 }
904 }
905 return null;
906 }
907
908 private boolean isExcluded(
909 String propertyName,
910 boolean logAll,
911 List<PropertyName> excludedProperties,
912 List<PropertyName> forceLogProperties) {
913 if (!forceLogProperties.isEmpty()) {
914 for (PropertyName logProperty : forceLogProperties) {
915 if (Strings.CS.equals(propertyName, logProperty.getPropertyName())) {
916 return false;
917 }
918 }
919 return true;
920 }
921
922 if (!excludedProperties.isEmpty()) {
923 for (PropertyName excludedProperty : excludedProperties) {
924 if (Strings.CS.equals(propertyName, excludedProperty.getPropertyName())) {
925 return true;
926 }
927 }
928 return false;
929 }
930
931 return !logAll;
932 }
933
934 private boolean isTracked(String propertyName, List<TrackedProperty> trackedProperties) {
935 for (TrackedProperty trackedProperty : trackedProperties) {
936 if (Strings.CS.equals(propertyName, trackedProperty.getPropertyName())) {
937 return true;
938 }
939 }
940 return false;
941 }
942
943 private boolean isCachedSegmentPropertiesPresent(
944 MavenProject project, Build build, List<MojoExecution> mojoExecutions) {
945 for (MojoExecution mojoExecution : mojoExecutions) {
946
947 final List<TrackedProperty> trackedProperties = cacheConfig.getTrackedProperties(mojoExecution);
948 final CompletedExecution cachedExecution = build.findMojoExecutionInfo(mojoExecution);
949
950 if (cachedExecution == null) {
951 LOGGER.info(
952 "Execution is not cached. Plugin: {}, goal {}, executionId: {}",
953 mojoExecution.getPlugin(),
954 mojoExecution.getGoal(),
955 mojoExecution.getExecutionId());
956 return false;
957 }
958
959 if (!DtoUtils.containsAllProperties(cachedExecution, trackedProperties)) {
960 LOGGER.warn(
961 "Cached build record doesn't contain all tracked properties. Plugin: {}, goal: {},"
962 + " executionId: {}",
963 mojoExecution.getPlugin(),
964 mojoExecution.getGoal(),
965 mojoExecution.getExecutionId());
966 return false;
967 }
968 }
969 return true;
970 }
971
972 @Override
973 public boolean isForcedExecution(MavenProject project, MojoExecution execution) {
974 if (cacheConfig.isForcedExecution(execution)) {
975 return true;
976 }
977
978 if (StringUtils.isNotBlank(cacheConfig.getAlwaysRunPlugins())) {
979 String[] alwaysRunPluginsList = split(cacheConfig.getAlwaysRunPlugins(), ",");
980 for (String pluginAndGoal : alwaysRunPluginsList) {
981 String[] tokens = pluginAndGoal.split(":");
982 String alwaysRunPlugin = tokens[0];
983 String alwaysRunGoal = tokens.length == 1 ? "*" : tokens[1];
984 if (Objects.equals(execution.getPlugin().getArtifactId(), alwaysRunPlugin)
985 && ("*".equals(alwaysRunGoal) || Objects.equals(execution.getGoal(), alwaysRunGoal))) {
986 return true;
987 }
988 }
989 }
990 return false;
991 }
992
993 @Override
994 public void saveCacheReport(MavenSession session) {
995 try {
996 CacheReport cacheReport = new CacheReport();
997 for (CacheResult result : cacheResults.values()) {
998 ProjectReport projectReport = new ProjectReport();
999 CacheContext context = result.getContext();
1000 MavenProject project = context.getProject();
1001 projectReport.setGroupId(project.getGroupId());
1002 projectReport.setArtifactId(project.getArtifactId());
1003 projectReport.setChecksum(context.getInputInfo().getChecksum());
1004 boolean checksumMatched = result.getStatus() != RestoreStatus.EMPTY;
1005 projectReport.setChecksumMatched(checksumMatched);
1006 projectReport.setLifecycleMatched(checksumMatched && result.isSuccess());
1007 projectReport.setSource(String.valueOf(result.getSource()));
1008 if (result.getSource() == CacheSource.REMOTE) {
1009 projectReport.setUrl(remoteCache.getResourceUrl(context, BUILDINFO_XML));
1010 } else if (result.getSource() == CacheSource.BUILD && cacheConfig.isSaveToRemote()) {
1011 projectReport.setSharedToRemote(true);
1012 projectReport.setUrl(remoteCache.getResourceUrl(context, BUILDINFO_XML));
1013 }
1014 cacheReport.addProject(projectReport);
1015 }
1016
1017 String buildId = UUID.randomUUID().toString();
1018 localCache.saveCacheReport(buildId, session, cacheReport);
1019 } catch (Exception e) {
1020 LOGGER.error("Cannot save incremental build aggregated report", e);
1021 }
1022 }
1023
1024 private void populateGitInfo(Build build, MavenSession session) {
1025 if (scm == null) {
1026 synchronized (this) {
1027 if (scm == null) {
1028 try {
1029 scm = CacheUtils.readGitInfo(session);
1030 } catch (IOException e) {
1031 scm = new Scm();
1032 LOGGER.error("Cannot populate git info", e);
1033 }
1034 }
1035 }
1036 }
1037 build.getDto().setScm(scm);
1038 }
1039
1040 private boolean zipAndAttachArtifact(MavenProject project, Path dir, String classifier, final String glob)
1041 throws IOException {
1042 Path temp = Files.createTempFile("maven-incremental-", project.getArtifactId());
1043 temp.toFile().deleteOnExit();
1044 boolean hasFile = CacheUtils.zip(dir, temp, glob, cacheConfig.isPreservePermissions());
1045 if (hasFile) {
1046 projectHelper.attachArtifact(project, "zip", classifier, temp.toFile());
1047 }
1048 return hasFile;
1049 }
1050
1051 private void restoreGeneratedSources(Artifact artifact, Path artifactFilePath, MavenProject project)
1052 throws IOException {
1053 final Path baseDir = project.getBasedir().toPath();
1054 final Path outputDir = baseDir.resolve(FilenameUtils.separatorsToSystem(artifact.getFilePath()));
1055 verifyRestorationInsideProject(project, outputDir);
1056 if (!Files.exists(outputDir)) {
1057 Files.createDirectories(outputDir);
1058 }
1059 CacheUtils.unzip(artifactFilePath, outputDir, cacheConfig.isPreservePermissions());
1060 }
1061
1062
1063 public void attachGeneratedSources(MavenProject project, ProjectCacheState state, long buildStartTime)
1064 throws IOException {
1065 final Path targetDir = Paths.get(project.getBuild().getDirectory());
1066
1067 final Path generatedSourcesDir = targetDir.resolve("generated-sources");
1068 attachDirIfNotEmpty(
1069 generatedSourcesDir,
1070 targetDir,
1071 project,
1072 state,
1073 OutputType.GENERATED_SOURCE,
1074 DEFAULT_FILE_GLOB,
1075 buildStartTime);
1076
1077 final Path generatedTestSourcesDir = targetDir.resolve("generated-test-sources");
1078 attachDirIfNotEmpty(
1079 generatedTestSourcesDir,
1080 targetDir,
1081 project,
1082 state,
1083 OutputType.GENERATED_SOURCE,
1084 DEFAULT_FILE_GLOB,
1085 buildStartTime);
1086
1087 Set<String> sourceRoots = new TreeSet<>();
1088 if (project.getCompileSourceRoots() != null) {
1089 sourceRoots.addAll(project.getCompileSourceRoots());
1090 }
1091 if (project.getTestCompileSourceRoots() != null) {
1092 sourceRoots.addAll(project.getTestCompileSourceRoots());
1093 }
1094
1095 for (String sourceRoot : sourceRoots) {
1096 final Path sourceRootPath = Paths.get(sourceRoot);
1097 if (Files.isDirectory(sourceRootPath)
1098 && sourceRootPath.startsWith(targetDir)
1099 && !(sourceRootPath.startsWith(generatedSourcesDir)
1100 || sourceRootPath.startsWith(generatedTestSourcesDir))) {
1101 attachDirIfNotEmpty(
1102 sourceRootPath,
1103 targetDir,
1104 project,
1105 state,
1106 OutputType.GENERATED_SOURCE,
1107 DEFAULT_FILE_GLOB,
1108 buildStartTime);
1109 }
1110 }
1111 }
1112
1113 private void attachOutputs(MavenProject project, ProjectCacheState state, long buildStartTime) throws IOException {
1114 final List<DirName> attachedDirs = cacheConfig.getAttachedOutputs();
1115 for (DirName dir : attachedDirs) {
1116 final Path targetDir = Paths.get(project.getBuild().getDirectory());
1117 final Path outputDir = targetDir.resolve(dir.getValue());
1118 if (isPathInsideProject(project, outputDir)) {
1119 attachDirIfNotEmpty(
1120 outputDir, targetDir, project, state, OutputType.EXTRA_OUTPUT, dir.getGlob(), buildStartTime);
1121 } else {
1122 LOGGER.warn("Outside project output candidate directory discarded ({})", outputDir.normalize());
1123 }
1124 }
1125 }
1126
1127 private void attachDirIfNotEmpty(
1128 Path candidateSubDir,
1129 Path parentDir,
1130 MavenProject project,
1131 ProjectCacheState state,
1132 final OutputType attachedOutputType,
1133 final String glob,
1134 final long buildStartTime)
1135 throws IOException {
1136 if (Files.isDirectory(candidateSubDir) && hasFiles(candidateSubDir)) {
1137 final Path relativePath = project.getBasedir().toPath().relativize(candidateSubDir);
1138 state.attachedResourceCounter++;
1139 final String classifier = attachedOutputType.getClassifierPrefix() + state.attachedResourceCounter;
1140
1141
1142
1143
1144
1145
1146
1147 boolean success = zipAndAttachArtifact(project, candidateSubDir, classifier, glob);
1148 if (success) {
1149 state.attachedResourcesPathsById.put(classifier, relativePath);
1150 LOGGER.debug("Attached directory: {}", candidateSubDir);
1151 }
1152 }
1153 }
1154
1155 private boolean hasFiles(Path candidateSubDir) throws IOException {
1156 final MutableBoolean hasFiles = new MutableBoolean();
1157 Files.walkFileTree(candidateSubDir, new SimpleFileVisitor<Path>() {
1158
1159 @Override
1160 public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) {
1161 hasFiles.setTrue();
1162 return FileVisitResult.TERMINATE;
1163 }
1164 });
1165 return hasFiles.booleanValue();
1166 }
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239 public void stagePreExistingArtifacts(MavenSession session, MavenProject project) throws IOException {
1240 final ProjectCacheState state = getProjectState(project);
1241 final Path multimoduleRoot = CacheUtils.getMultimoduleRoot(session);
1242 final Path stagingDir = multimoduleRoot.resolve("target").resolve("maven-build-cache-extension");
1243
1244
1245 Files.createDirectories(stagingDir);
1246 state.stagingDirectory = stagingDir;
1247
1248
1249 Set<Path> pathsToProcess = collectCachedArtifactPaths(project);
1250
1251 int movedCount = 0;
1252 for (Path path : pathsToProcess) {
1253
1254 Path relativePath = multimoduleRoot.relativize(path);
1255 Path stagedPath = stagingDir.resolve(relativePath);
1256
1257 if (Files.isDirectory(path)) {
1258
1259 if (Files.exists(stagedPath)) {
1260 deleteDirectory(stagedPath);
1261 LOGGER.debug("Removed existing staged directory: {}", stagedPath);
1262 }
1263
1264 Files.createDirectories(stagedPath.getParent());
1265 Files.move(path, stagedPath);
1266 movedCount++;
1267 LOGGER.debug("Moved directory to staging: {} → {}", relativePath, stagedPath);
1268 } else if (Files.isRegularFile(path)) {
1269
1270 if (Files.exists(stagedPath)) {
1271 Files.delete(stagedPath);
1272 LOGGER.debug("Removed existing staged file: {}", stagedPath);
1273 }
1274
1275 Files.createDirectories(stagedPath.getParent());
1276 Files.move(path, stagedPath);
1277 movedCount++;
1278 LOGGER.debug("Moved file to staging: {} → {}", relativePath, stagedPath);
1279 }
1280 }
1281
1282 if (movedCount > 0) {
1283 LOGGER.info(
1284 "Moved {} pre-existing artifacts to staging directory to prevent caching stale files", movedCount);
1285 }
1286 }
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303 private Set<Path> collectCachedArtifactPaths(MavenProject project) {
1304 Set<Path> paths = new HashSet<>();
1305 final org.apache.maven.artifact.Artifact projectArtifact = project.getArtifact();
1306 final Path targetDir = Paths.get(project.getBuild().getDirectory());
1307
1308
1309 if (projectArtifact.getFile() != null && projectArtifact.getFile().exists()) {
1310 paths.add(projectArtifact.getFile().toPath());
1311 }
1312
1313
1314 if (cacheConfig.isCacheCompile()) {
1315 List<DirName> attachedDirs = cacheConfig.getAttachedOutputs();
1316 for (DirName dir : attachedDirs) {
1317 Path outputDir = targetDir.resolve(dir.getValue());
1318 if (Files.exists(outputDir)) {
1319 paths.add(outputDir);
1320 }
1321 }
1322 }
1323
1324 return paths;
1325 }
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348 public void restoreStagedArtifacts(MavenSession session, MavenProject project) {
1349 final ProjectCacheState state = getProjectState(project);
1350 final Path stagingDir = state.stagingDirectory;
1351
1352 if (stagingDir == null || !Files.exists(stagingDir)) {
1353 return;
1354 }
1355
1356 try {
1357 final Path multimoduleRoot = CacheUtils.getMultimoduleRoot(session);
1358
1359
1360 final List<Path> dirsToDelete = new ArrayList<>();
1361
1362
1363 Files.walkFileTree(stagingDir, new SimpleFileVisitor<Path>() {
1364 @Override
1365 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
1366 if (dir.equals(stagingDir)) {
1367 return FileVisitResult.CONTINUE;
1368 }
1369
1370 Path relativePath = stagingDir.relativize(dir);
1371 Path targetPath = multimoduleRoot.resolve(relativePath);
1372
1373 if (Files.exists(targetPath)) {
1374
1375 dirsToDelete.add(dir);
1376 LOGGER.debug("Fresh directory exists, marking for recursive deletion: {}", relativePath);
1377 return FileVisitResult.SKIP_SUBTREE;
1378 }
1379 return FileVisitResult.CONTINUE;
1380 }
1381
1382 @Override
1383 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1384 Path relativePath = stagingDir.relativize(file);
1385 Path targetPath = multimoduleRoot.resolve(relativePath);
1386
1387 try {
1388
1389 Files.createDirectories(targetPath.getParent());
1390 Files.move(file, targetPath);
1391 LOGGER.debug("Restored unchanged file from staging: {}", relativePath);
1392 } catch (FileAlreadyExistsException e) {
1393
1394 Files.delete(file);
1395 LOGGER.debug("Fresh file exists, deleted stale file: {}", relativePath);
1396 }
1397 return FileVisitResult.CONTINUE;
1398 }
1399
1400 @Override
1401 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
1402 if (exc != null) {
1403 throw exc;
1404 }
1405
1406 if (!dir.equals(stagingDir)) {
1407 try {
1408 Files.delete(dir);
1409 LOGGER.debug("Deleted empty directory: {}", stagingDir.relativize(dir));
1410 } catch (IOException e) {
1411
1412 }
1413 }
1414 return FileVisitResult.CONTINUE;
1415 }
1416 });
1417
1418
1419 for (Path dirToDelete : dirsToDelete) {
1420 LOGGER.debug("Recursively deleting stale directory: {}", stagingDir.relativize(dirToDelete));
1421 deleteDirectory(dirToDelete);
1422 }
1423
1424
1425 try {
1426 Files.delete(stagingDir);
1427 LOGGER.debug("Deleted empty staging directory: {}", stagingDir);
1428 } catch (IOException e) {
1429 LOGGER.debug("Staging directory not empty, preserving for other modules");
1430 }
1431
1432 } catch (IOException e) {
1433 LOGGER.warn("Failed to restore artifacts from staging directory: {}", e.getMessage());
1434 }
1435
1436
1437 state.stagingDirectory = null;
1438
1439
1440 String key = getVersionlessProjectKey(project);
1441 projectStates.remove(key);
1442 }
1443
1444
1445
1446
1447 private void deleteDirectory(Path dir) throws IOException {
1448 if (!Files.exists(dir)) {
1449 return;
1450 }
1451
1452 Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
1453 @Override
1454 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1455 Files.delete(file);
1456 return FileVisitResult.CONTINUE;
1457 }
1458
1459 @Override
1460 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
1461 Files.delete(dir);
1462 return FileVisitResult.CONTINUE;
1463 }
1464 });
1465 }
1466
1467 private boolean isOutputArtifact(String name) {
1468 List<Pattern> excludePatterns = cacheConfig.getExcludePatterns();
1469 for (Pattern pattern : excludePatterns) {
1470 if (pattern.matcher(name).matches()) {
1471 return false;
1472 }
1473 }
1474 return true;
1475 }
1476 }