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
25 import java.io.File;
26 import java.io.FileNotFoundException;
27 import java.io.IOException;
28 import java.lang.reflect.Field;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 import java.nio.charset.StandardCharsets;
32 import java.nio.file.FileVisitResult;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.nio.file.Paths;
36 import java.nio.file.SimpleFileVisitor;
37 import java.nio.file.attribute.BasicFileAttributes;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Objects;
43 import java.util.Optional;
44 import java.util.Set;
45 import java.util.TreeSet;
46 import java.util.UUID;
47 import java.util.concurrent.ConcurrentHashMap;
48 import java.util.concurrent.ConcurrentMap;
49 import java.util.concurrent.ExecutionException;
50 import java.util.concurrent.Future;
51 import java.util.concurrent.FutureTask;
52 import java.util.regex.Pattern;
53
54 import org.apache.commons.io.FileUtils;
55 import org.apache.commons.io.FilenameUtils;
56 import org.apache.commons.lang3.StringUtils;
57 import org.apache.commons.lang3.mutable.MutableBoolean;
58 import org.apache.maven.SessionScoped;
59 import org.apache.maven.artifact.InvalidArtifactRTException;
60 import org.apache.maven.artifact.handler.ArtifactHandler;
61 import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
62 import org.apache.maven.buildcache.artifact.ArtifactRestorationReport;
63 import org.apache.maven.buildcache.artifact.RestoredArtifact;
64 import org.apache.maven.buildcache.checksum.MavenProjectInput;
65 import org.apache.maven.buildcache.hash.HashAlgorithm;
66 import org.apache.maven.buildcache.hash.HashFactory;
67 import org.apache.maven.buildcache.xml.Build;
68 import org.apache.maven.buildcache.xml.CacheConfig;
69 import org.apache.maven.buildcache.xml.CacheSource;
70 import org.apache.maven.buildcache.xml.DtoUtils;
71 import org.apache.maven.buildcache.xml.XmlService;
72 import org.apache.maven.buildcache.xml.build.Artifact;
73 import org.apache.maven.buildcache.xml.build.CompletedExecution;
74 import org.apache.maven.buildcache.xml.build.DigestItem;
75 import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
76 import org.apache.maven.buildcache.xml.build.Scm;
77 import org.apache.maven.buildcache.xml.config.PropertyName;
78 import org.apache.maven.buildcache.xml.config.TrackedProperty;
79 import org.apache.maven.buildcache.xml.diff.Diff;
80 import org.apache.maven.buildcache.xml.report.CacheReport;
81 import org.apache.maven.buildcache.xml.report.ProjectReport;
82 import org.apache.maven.execution.MavenSession;
83 import org.apache.maven.execution.MojoExecutionEvent;
84 import org.apache.maven.plugin.MojoExecution;
85 import org.apache.maven.plugin.descriptor.Parameter;
86 import org.apache.maven.project.MavenProject;
87 import org.apache.maven.project.MavenProjectHelper;
88 import org.apache.maven.repository.RepositorySystem;
89 import org.codehaus.plexus.util.ReflectionUtils;
90 import org.slf4j.Logger;
91 import org.slf4j.LoggerFactory;
92
93 import static java.nio.file.StandardOpenOption.CREATE;
94 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
95 import static org.apache.commons.lang3.StringUtils.isNotBlank;
96 import static org.apache.commons.lang3.StringUtils.replace;
97 import static org.apache.commons.lang3.StringUtils.split;
98 import static org.apache.maven.buildcache.CacheResult.empty;
99 import static org.apache.maven.buildcache.CacheResult.failure;
100 import static org.apache.maven.buildcache.CacheResult.partialSuccess;
101 import static org.apache.maven.buildcache.CacheResult.rebuilded;
102 import static org.apache.maven.buildcache.CacheResult.success;
103 import static org.apache.maven.buildcache.RemoteCacheRepository.BUILDINFO_XML;
104 import static org.apache.maven.buildcache.checksum.KeyUtils.getVersionlessProjectKey;
105 import static org.apache.maven.buildcache.checksum.MavenProjectInput.CACHE_IMPLEMENTATION_VERSION;
106
107
108
109
110 @SessionScoped
111 @Named
112 @SuppressWarnings("unused")
113 public class CacheControllerImpl implements CacheController {
114
115 public static final String FILE_SEPARATOR_SUBST = "_";
116
117
118
119 private static final String BUILD_PREFIX = "build" + FILE_SEPARATOR_SUBST;
120
121 private static final Logger LOGGER = LoggerFactory.getLogger(CacheControllerImpl.class);
122
123 private final MavenProjectHelper projectHelper;
124 private final ArtifactHandlerManager artifactHandlerManager;
125 private final XmlService xmlService;
126 private final CacheConfig cacheConfig;
127 private final LocalCacheRepository localCache;
128 private final RemoteCacheRepository remoteCache;
129 private final ConcurrentMap<String, CacheResult> cacheResults = new ConcurrentHashMap<>();
130 private final LifecyclePhasesHelper lifecyclePhasesHelper;
131 private volatile Map<String, MavenProject> projectIndex;
132 private final ProjectInputCalculator projectInputCalculator;
133 private final RestoredArtifactHandler restoreArtifactHandler;
134 private volatile Scm scm;
135
136 @Inject
137 public CacheControllerImpl(
138 MavenProjectHelper projectHelper,
139 RepositorySystem repoSystem,
140 ArtifactHandlerManager artifactHandlerManager,
141 XmlService xmlService,
142 LocalCacheRepository localCache,
143 RemoteCacheRepository remoteCache,
144 CacheConfig cacheConfig,
145 ProjectInputCalculator projectInputCalculator,
146 RestoredArtifactHandler restoreArtifactHandler,
147 LifecyclePhasesHelper lifecyclePhasesHelper,
148 MavenSession session) {
149 this.projectHelper = projectHelper;
150 this.localCache = localCache;
151 this.remoteCache = remoteCache;
152 this.cacheConfig = cacheConfig;
153 this.artifactHandlerManager = artifactHandlerManager;
154 this.xmlService = xmlService;
155 this.lifecyclePhasesHelper = lifecyclePhasesHelper;
156 this.projectInputCalculator = projectInputCalculator;
157 this.restoreArtifactHandler = restoreArtifactHandler;
158 }
159
160 @Override
161 @Nonnull
162 public CacheResult findCachedBuild(
163 MavenSession session, MavenProject project, List<MojoExecution> mojoExecutions, boolean skipCache) {
164 final String highestPhase = lifecyclePhasesHelper.resolveHighestLifecyclePhase(project, mojoExecutions);
165
166 if (!lifecyclePhasesHelper.isLaterPhaseThanClean(highestPhase)) {
167 return empty();
168 }
169
170 String projectName = getVersionlessProjectKey(project);
171
172 ProjectsInputInfo inputInfo = projectInputCalculator.calculateInput(project);
173
174 final CacheContext context = new CacheContext(project, inputInfo, session);
175
176 CacheResult result = empty(context);
177 if (!skipCache) {
178
179 LOGGER.info("Attempting to restore project {} from build cache", projectName);
180
181
182 if (cacheConfig.isRemoteCacheEnabled()) {
183 result = findCachedBuild(mojoExecutions, context);
184 if (!result.isSuccess() && result.getContext() != null) {
185 LOGGER.info("Remote cache is incomplete or missing, trying local build for {}", projectName);
186 }
187 }
188
189 if (!result.isSuccess() && result.getContext() != null) {
190 CacheResult localBuild = findLocalBuild(mojoExecutions, context);
191 if (localBuild.isSuccess() || (localBuild.isPartialSuccess() && !result.isPartialSuccess())) {
192 result = localBuild;
193 } else {
194 LOGGER.info(
195 "Local build was not found by checksum {} for {}", inputInfo.getChecksum(), projectName);
196 }
197 }
198 } else {
199 LOGGER.info(
200 "Project {} is marked as requiring force rebuild, will skip lookup in build cache", projectName);
201 }
202 cacheResults.put(getVersionlessProjectKey(project), result);
203
204 return result;
205 }
206
207 private CacheResult findCachedBuild(List<MojoExecution> mojoExecutions, CacheContext context) {
208 Optional<Build> cachedBuild = Optional.empty();
209 try {
210 cachedBuild = localCache.findBuild(context);
211 if (cachedBuild.isPresent()) {
212 return analyzeResult(context, mojoExecutions, cachedBuild.get());
213 }
214 } catch (Exception e) {
215 LOGGER.error("Cannot read cached remote build", e);
216 }
217 return cachedBuild.map(build -> failure(build, context)).orElseGet(() -> empty(context));
218 }
219
220 private CacheResult findLocalBuild(List<MojoExecution> mojoExecutions, CacheContext context) {
221 Optional<Build> localBuild = Optional.empty();
222 try {
223 localBuild = localCache.findLocalBuild(context);
224 if (localBuild.isPresent()) {
225 return analyzeResult(context, mojoExecutions, localBuild.get());
226 }
227 } catch (Exception e) {
228 LOGGER.error("Cannot read local build", e);
229 }
230 return localBuild.map(build -> failure(build, context)).orElseGet(() -> empty(context));
231 }
232
233 private CacheResult analyzeResult(CacheContext context, List<MojoExecution> mojoExecutions, Build build) {
234 try {
235 final ProjectsInputInfo inputInfo = context.getInputInfo();
236 String projectName = getVersionlessProjectKey(context.getProject());
237
238 LOGGER.info(
239 "Found cached build, restoring {} from cache by checksum {}", projectName, inputInfo.getChecksum());
240 LOGGER.debug("Cached build details: {}", build);
241
242 final String cacheImplementationVersion = build.getCacheImplementationVersion();
243 if (!CACHE_IMPLEMENTATION_VERSION.equals(cacheImplementationVersion)) {
244 LOGGER.warn(
245 "Maven and cached build implementations mismatch, caching might not work correctly. "
246 + "Implementation version: " + CACHE_IMPLEMENTATION_VERSION + ", cached build: {}",
247 build.getCacheImplementationVersion());
248 }
249
250 List<MojoExecution> cachedSegment =
251 lifecyclePhasesHelper.getCachedSegment(context.getProject(), mojoExecutions, build);
252 List<MojoExecution> missingMojos = build.getMissingExecutions(cachedSegment);
253 if (!missingMojos.isEmpty()) {
254 LOGGER.warn(
255 "Cached build doesn't contains all requested plugin executions "
256 + "(missing: {}), cannot restore",
257 missingMojos);
258 return failure(build, context);
259 }
260
261 if (!isCachedSegmentPropertiesPresent(context.getProject(), build, cachedSegment)) {
262 LOGGER.info("Cached build violates cache rules, cannot restore");
263 return failure(build, context);
264 }
265
266 final String highestRequestPhase =
267 lifecyclePhasesHelper.resolveHighestLifecyclePhase(context.getProject(), mojoExecutions);
268
269 if (lifecyclePhasesHelper.isLaterPhaseThanBuild(highestRequestPhase, build)
270 && !canIgnoreMissingSegment(context.getProject(), build, mojoExecutions)) {
271 LOGGER.info(
272 "Project {} restored partially. Highest cached goal: {}, requested: {}",
273 projectName,
274 build.getHighestCompletedGoal(),
275 highestRequestPhase);
276 return partialSuccess(build, context);
277 }
278
279 return success(build, context);
280
281 } catch (Exception e) {
282 LOGGER.error("Failed to restore project", e);
283 localCache.clearCache(context);
284 return failure(build, context);
285 }
286 }
287
288 private boolean canIgnoreMissingSegment(MavenProject project, Build info, List<MojoExecution> mojoExecutions) {
289 final List<MojoExecution> postCachedSegment =
290 lifecyclePhasesHelper.getPostCachedSegment(project, mojoExecutions, info);
291
292 for (MojoExecution mojoExecution : postCachedSegment) {
293 if (!cacheConfig.canIgnore(mojoExecution)) {
294 return false;
295 }
296 }
297 return true;
298 }
299
300 @Override
301 public ArtifactRestorationReport restoreProjectArtifacts(CacheResult cacheResult) {
302
303 LOGGER.debug("Restore project artifacts");
304 final Build build = cacheResult.getBuildInfo();
305 final CacheContext context = cacheResult.getContext();
306 final MavenProject project = context.getProject();
307 ArtifactRestorationReport restorationReport = new ArtifactRestorationReport();
308
309 try {
310 RestoredArtifact restoredProjectArtifact = null;
311 List<RestoredArtifact> restoredAttachedArtifacts = new ArrayList<>();
312
313 if (build.getArtifact() != null && isNotBlank(build.getArtifact().getFileName())) {
314 final Artifact artifactInfo = build.getArtifact();
315 String originalVersion = artifactInfo.getVersion();
316 artifactInfo.setVersion(project.getVersion());
317
318 final Future<File> downloadTask =
319 createDownloadTask(cacheResult, context, project, artifactInfo, originalVersion);
320 restoredProjectArtifact = restoredArtifact(
321 project.getArtifact(), artifactInfo.getType(), artifactInfo.getClassifier(), downloadTask);
322 }
323
324 for (Artifact attachedArtifactInfo : build.getAttachedArtifacts()) {
325 String originalVersion = attachedArtifactInfo.getVersion();
326 attachedArtifactInfo.setVersion(project.getVersion());
327 if (isNotBlank(attachedArtifactInfo.getFileName())) {
328 if (StringUtils.startsWith(attachedArtifactInfo.getClassifier(), BUILD_PREFIX)) {
329
330
331
332 if (cacheConfig.isRestoreGeneratedSources()
333 && MavenProjectInput.isRestoreGeneratedSources(project)) {
334
335 restorationReport.setRestoredFilesInProjectDirectory(true);
336
337 final Path attachedArtifactFile =
338 localCache.getArtifactFile(context, cacheResult.getSource(), attachedArtifactInfo);
339 restoreGeneratedSources(attachedArtifactInfo, attachedArtifactFile, project);
340 }
341 } else {
342 Future<File> downloadTask = createDownloadTask(
343 cacheResult, context, project, attachedArtifactInfo, originalVersion);
344 final RestoredArtifact restoredAttachedArtifact = restoredArtifact(
345 restoredProjectArtifact == null ? project.getArtifact() : restoredProjectArtifact,
346 attachedArtifactInfo.getType(),
347 attachedArtifactInfo.getClassifier(),
348 downloadTask);
349 restoredAttachedArtifacts.add(restoredAttachedArtifact);
350 }
351 }
352 }
353
354
355 if (restoredProjectArtifact != null) {
356 project.setArtifact(restoredProjectArtifact);
357 }
358 restoredAttachedArtifacts.forEach(project::addAttachedArtifact);
359 restorationReport.setSuccess(true);
360 } catch (Exception e) {
361 LOGGER.debug("Cannot restore cache, continuing with normal build.", e);
362 }
363 return restorationReport;
364 }
365
366
367
368
369
370 private RestoredArtifact restoredArtifact(
371 org.apache.maven.artifact.Artifact parent,
372 String artifactType,
373 String artifactClassifier,
374 Future<File> artifactFile) {
375 ArtifactHandler handler = null;
376
377 if (artifactType != null) {
378 handler = artifactHandlerManager.getArtifactHandler(artifactType);
379 }
380
381 if (handler == null) {
382 handler = artifactHandlerManager.getArtifactHandler("jar");
383 }
384
385
386 RestoredArtifact artifact =
387 new RestoredArtifact(parent, artifactFile, artifactType, artifactClassifier, handler);
388 artifact.setResolved(true);
389
390 return artifact;
391 }
392
393 private Future<File> createDownloadTask(
394 CacheResult cacheResult,
395 CacheContext context,
396 MavenProject project,
397 Artifact artifact,
398 String originalVersion) {
399 final FutureTask<File> downloadTask = new FutureTask<>(() -> {
400 LOGGER.debug("Downloading artifact {}", artifact.getArtifactId());
401 final Path artifactFile = localCache.getArtifactFile(context, cacheResult.getSource(), artifact);
402 if (!Files.exists(artifactFile)) {
403 throw new FileNotFoundException("Missing file for cached build, cannot restore. File: " + artifactFile);
404 }
405 LOGGER.debug("Downloaded artifact " + artifact.getArtifactId() + " to: " + artifactFile);
406 return restoreArtifactHandler
407 .adjustArchiveArtifactVersion(project, originalVersion, artifactFile)
408 .toFile();
409 });
410 if (!cacheConfig.isLazyRestore()) {
411 downloadTask.run();
412 try {
413 downloadTask.get();
414 } catch (InterruptedException e) {
415 Thread.currentThread().interrupt();
416 throw new InvalidArtifactRTException(
417 artifact.getGroupId(),
418 artifact.getArtifactId(),
419 artifact.getVersion(),
420 artifact.getType(),
421 RestoredArtifact.MSG_INTERRUPTED_WHILE_RETRIEVING_ARTIFACT_FILE,
422 e);
423 } catch (ExecutionException e) {
424 throw new InvalidArtifactRTException(
425 artifact.getGroupId(),
426 artifact.getArtifactId(),
427 artifact.getVersion(),
428 artifact.getType(),
429 RestoredArtifact.MSG_ERROR_RETRIEVING_ARTIFACT_FILE,
430 e.getCause());
431 }
432 }
433 return downloadTask;
434 }
435
436 @Override
437 public void save(
438 CacheResult cacheResult,
439 List<MojoExecution> mojoExecutions,
440 Map<String, MojoExecutionEvent> executionEvents) {
441 CacheContext context = cacheResult.getContext();
442
443 if (context == null || context.getInputInfo() == null) {
444 LOGGER.info("Cannot save project in cache, skipping");
445 return;
446 }
447
448 final MavenProject project = context.getProject();
449 final MavenSession session = context.getSession();
450 try {
451 final HashFactory hashFactory = cacheConfig.getHashFactory();
452 final org.apache.maven.artifact.Artifact projectArtifact = project.getArtifact();
453 final List<org.apache.maven.artifact.Artifact> attachedArtifacts;
454 final List<Artifact> attachedArtifactDtos;
455 final Artifact projectArtifactDto;
456 if (project.hasLifecyclePhase("package")) {
457 final HashAlgorithm algorithm = hashFactory.createAlgorithm();
458 attachGeneratedSources(project);
459 attachOutputs(project);
460 attachedArtifacts = project.getAttachedArtifacts() != null
461 ? project.getAttachedArtifacts()
462 : Collections.emptyList();
463 attachedArtifactDtos = artifactDtos(attachedArtifacts, algorithm);
464 projectArtifactDto = artifactDto(project.getArtifact(), algorithm);
465 } else {
466 attachedArtifacts = Collections.emptyList();
467 attachedArtifactDtos = new ArrayList<>();
468 projectArtifactDto = null;
469 }
470
471 List<CompletedExecution> completedExecution = buildExecutionInfo(mojoExecutions, executionEvents);
472
473 final Build build = new Build(
474 session.getGoals(),
475 projectArtifactDto,
476 attachedArtifactDtos,
477 context.getInputInfo(),
478 completedExecution,
479 hashFactory.getAlgorithm());
480 populateGitInfo(build, session);
481 build.getDto().set_final(cacheConfig.isSaveToRemoteFinal());
482 cacheResults.put(getVersionlessProjectKey(project), rebuilded(cacheResult, build));
483
484 localCache.beforeSave(context);
485
486
487 if (project.hasLifecyclePhase("package")) {
488 localCache.saveBuildInfo(cacheResult, build);
489 if (projectArtifact.getFile() != null) {
490 localCache.saveArtifactFile(cacheResult, projectArtifact);
491 }
492 for (org.apache.maven.artifact.Artifact attachedArtifact : attachedArtifacts) {
493 if (attachedArtifact.getFile() != null) {
494 boolean storeArtifact =
495 isOutputArtifact(attachedArtifact.getFile().getName());
496 if (storeArtifact) {
497 localCache.saveArtifactFile(cacheResult, attachedArtifact);
498 } else {
499 LOGGER.debug(
500 "Skipping attached project artifact '{}' = "
501 + " it is marked for exclusion from caching",
502 attachedArtifact.getFile().getName());
503 }
504 }
505 }
506 } else {
507 localCache.saveBuildInfo(cacheResult, build);
508 }
509
510 if (cacheConfig.isBaselineDiffEnabled()) {
511 produceDiffReport(cacheResult, build);
512 }
513
514 } catch (Exception e) {
515 LOGGER.error("Failed to save project, cleaning cache. Project: {}", project, e);
516 try {
517 localCache.clearCache(context);
518 } catch (Exception ex) {
519 LOGGER.error("Failed to clean cache due to unexpected error:", ex);
520 }
521 }
522 }
523
524 public void produceDiffReport(CacheResult cacheResult, Build build) {
525 MavenProject project = cacheResult.getContext().getProject();
526 Optional<Build> baselineHolder = remoteCache.findBaselineBuild(project);
527 if (baselineHolder.isPresent()) {
528 Build baseline = baselineHolder.get();
529 String outputDirectory = project.getBuild().getDirectory();
530 Path reportOutputDir = Paths.get(outputDirectory, "incremental-maven");
531 LOGGER.info("Saving cache builds diff to: {}", reportOutputDir);
532 Diff diff = new CacheDiff(build.getDto(), baseline.getDto(), cacheConfig).compare();
533 try {
534 Files.createDirectories(reportOutputDir);
535 final ProjectsInputInfo baselineInputs = baseline.getDto().getProjectsInputInfo();
536 final String checksum = baselineInputs.getChecksum();
537 Files.write(
538 reportOutputDir.resolve("buildinfo-baseline-" + checksum + ".xml"),
539 xmlService.toBytes(baseline.getDto()),
540 TRUNCATE_EXISTING,
541 CREATE);
542 Files.write(
543 reportOutputDir.resolve("buildinfo-" + checksum + ".xml"),
544 xmlService.toBytes(build.getDto()),
545 TRUNCATE_EXISTING,
546 CREATE);
547 Files.write(
548 reportOutputDir.resolve("buildsdiff-" + checksum + ".xml"),
549 xmlService.toBytes(diff),
550 TRUNCATE_EXISTING,
551 CREATE);
552 final Optional<DigestItem> pom =
553 CacheDiff.findPom(build.getDto().getProjectsInputInfo());
554 if (pom.isPresent()) {
555 Files.write(
556 reportOutputDir.resolve("effective-pom-" + checksum + ".xml"),
557 pom.get().getValue().getBytes(StandardCharsets.UTF_8),
558 TRUNCATE_EXISTING,
559 CREATE);
560 }
561 final Optional<DigestItem> baselinePom = CacheDiff.findPom(baselineInputs);
562 if (baselinePom.isPresent()) {
563 Files.write(
564 reportOutputDir.resolve("effective-pom-baseline-" + baselineInputs.getChecksum() + ".xml"),
565 baselinePom.get().getValue().getBytes(StandardCharsets.UTF_8),
566 TRUNCATE_EXISTING,
567 CREATE);
568 }
569 } catch (IOException e) {
570 LOGGER.error("Cannot produce build diff for project", e);
571 }
572 } else {
573 LOGGER.info("Cannot find project in baseline build, skipping diff");
574 }
575 }
576
577 private List<Artifact> artifactDtos(
578 List<org.apache.maven.artifact.Artifact> attachedArtifacts, HashAlgorithm digest) throws IOException {
579 List<Artifact> result = new ArrayList<>();
580 for (org.apache.maven.artifact.Artifact attachedArtifact : attachedArtifacts) {
581 if (attachedArtifact.getFile() != null
582 && isOutputArtifact(attachedArtifact.getFile().getName())) {
583 result.add(artifactDto(attachedArtifact, digest));
584 }
585 }
586 return result;
587 }
588
589 private Artifact artifactDto(org.apache.maven.artifact.Artifact projectArtifact, HashAlgorithm algorithm)
590 throws IOException {
591 final Artifact dto = DtoUtils.createDto(projectArtifact);
592 if (projectArtifact.getFile() != null && projectArtifact.getFile().isFile()) {
593 final Path file = projectArtifact.getFile().toPath();
594 dto.setFileHash(algorithm.hash(file));
595 dto.setFileSize(Files.size(file));
596 }
597 return dto;
598 }
599
600 private List<CompletedExecution> buildExecutionInfo(
601 List<MojoExecution> mojoExecutions, Map<String, MojoExecutionEvent> executionEvents) {
602 List<CompletedExecution> list = new ArrayList<>();
603 for (MojoExecution mojoExecution : mojoExecutions) {
604 final String executionKey = CacheUtils.mojoExecutionKey(mojoExecution);
605 final MojoExecutionEvent executionEvent =
606 executionEvents != null ? executionEvents.get(executionKey) : null;
607 CompletedExecution executionInfo = new CompletedExecution();
608 executionInfo.setExecutionKey(executionKey);
609 executionInfo.setMojoClassName(mojoExecution.getMojoDescriptor().getImplementation());
610 if (executionEvent != null) {
611 recordMojoProperties(executionInfo, executionEvent);
612 }
613 list.add(executionInfo);
614 }
615 return list;
616 }
617
618 private void recordMojoProperties(CompletedExecution execution, MojoExecutionEvent executionEvent) {
619 final MojoExecution mojoExecution = executionEvent.getExecution();
620
621 final boolean logAll = cacheConfig.isLogAllProperties(mojoExecution);
622 List<TrackedProperty> trackedProperties = cacheConfig.getTrackedProperties(mojoExecution);
623 List<PropertyName> noLogProperties = cacheConfig.getNologProperties(mojoExecution);
624 List<PropertyName> forceLogProperties = cacheConfig.getLoggedProperties(mojoExecution);
625 final Object mojo = executionEvent.getMojo();
626
627 final File baseDir = executionEvent.getProject().getBasedir();
628 final String baseDirPath = FilenameUtils.normalizeNoEndSeparator(baseDir.getAbsolutePath()) + File.separator;
629
630 final List<Parameter> parameters = mojoExecution.getMojoDescriptor().getParameters();
631 for (Parameter parameter : parameters) {
632
633 if (!parameter.isEditable()) {
634 continue;
635 }
636
637 final String propertyName = parameter.getName();
638 final boolean tracked = isTracked(propertyName, trackedProperties);
639 if (!tracked && isExcluded(propertyName, logAll, noLogProperties, forceLogProperties)) {
640 continue;
641 }
642
643 try {
644 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(propertyName, mojo.getClass());
645 if (field != null) {
646 final Object value = ReflectionUtils.getValueIncludingSuperclasses(propertyName, mojo);
647 DtoUtils.addProperty(execution, propertyName, value, baseDirPath, tracked);
648 continue;
649 }
650
651 Method getter = getGetter(propertyName, mojo.getClass());
652 if (getter != null) {
653 Object value = getter.invoke(mojo);
654 DtoUtils.addProperty(execution, propertyName, value, baseDirPath, tracked);
655 continue;
656 }
657
658 if (LOGGER.isWarnEnabled()) {
659 LOGGER.warn(
660 "Cannot find a Mojo parameter '{}' to read for Mojo {}. This parameter should be ignored.",
661 propertyName,
662 mojoExecution);
663 }
664
665 } catch (IllegalAccessException | InvocationTargetException e) {
666 LOGGER.info("Cannot get property {} value from {}: {}", propertyName, mojo, e.getMessage());
667 if (tracked) {
668 throw new IllegalArgumentException("Property configured in cache introspection config " + "for "
669 + mojo + " is not accessible: " + propertyName);
670 }
671 }
672 }
673 }
674
675 private static Method getGetter(String fieldName, Class<?> clazz) {
676 String getterMethodName = "get" + org.codehaus.plexus.util.StringUtils.capitalizeFirstLetter(fieldName);
677 Method[] methods = clazz.getMethods();
678 for (Method method : methods) {
679 if (method.getName().equals(getterMethodName)
680 && !method.getReturnType().equals(Void.TYPE)
681 && method.getParameterCount() == 0) {
682 return method;
683 }
684 }
685 return null;
686 }
687
688 private boolean isExcluded(
689 String propertyName,
690 boolean logAll,
691 List<PropertyName> excludedProperties,
692 List<PropertyName> forceLogProperties) {
693 if (!forceLogProperties.isEmpty()) {
694 for (PropertyName logProperty : forceLogProperties) {
695 if (StringUtils.equals(propertyName, logProperty.getPropertyName())) {
696 return false;
697 }
698 }
699 return true;
700 }
701
702 if (!excludedProperties.isEmpty()) {
703 for (PropertyName excludedProperty : excludedProperties) {
704 if (StringUtils.equals(propertyName, excludedProperty.getPropertyName())) {
705 return true;
706 }
707 }
708 return false;
709 }
710
711 return !logAll;
712 }
713
714 private boolean isTracked(String propertyName, List<TrackedProperty> trackedProperties) {
715 for (TrackedProperty trackedProperty : trackedProperties) {
716 if (StringUtils.equals(propertyName, trackedProperty.getPropertyName())) {
717 return true;
718 }
719 }
720 return false;
721 }
722
723 private boolean isCachedSegmentPropertiesPresent(
724 MavenProject project, Build build, List<MojoExecution> mojoExecutions) {
725 for (MojoExecution mojoExecution : mojoExecutions) {
726
727 final List<TrackedProperty> trackedProperties = cacheConfig.getTrackedProperties(mojoExecution);
728 final CompletedExecution cachedExecution = build.findMojoExecutionInfo(mojoExecution);
729
730 if (cachedExecution == null) {
731 LOGGER.info(
732 "Execution is not cached. Plugin: {}, goal {}, executionId: {}",
733 mojoExecution.getPlugin(),
734 mojoExecution.getGoal(),
735 mojoExecution.getExecutionId());
736 return false;
737 }
738
739 if (!DtoUtils.containsAllProperties(cachedExecution, trackedProperties)) {
740 LOGGER.warn(
741 "Cached build record doesn't contain all tracked properties. Plugin: {}, goal: {},"
742 + " executionId: {}",
743 mojoExecution.getPlugin(),
744 mojoExecution.getGoal(),
745 mojoExecution.getExecutionId());
746 return false;
747 }
748 }
749 return true;
750 }
751
752 @Override
753 public boolean isForcedExecution(MavenProject project, MojoExecution execution) {
754 if (cacheConfig.isForcedExecution(execution)) {
755 return true;
756 }
757
758 if (StringUtils.isNotBlank(cacheConfig.getAlwaysRunPlugins())) {
759 String[] alwaysRunPluginsList = split(cacheConfig.getAlwaysRunPlugins(), ",");
760 for (String pluginAndGoal : alwaysRunPluginsList) {
761 String[] tokens = pluginAndGoal.split(":");
762 String alwaysRunPlugin = tokens[0];
763 String alwaysRunGoal = tokens.length == 1 ? "*" : tokens[1];
764 if (Objects.equals(execution.getPlugin().getArtifactId(), alwaysRunPlugin)
765 && ("*".equals(alwaysRunGoal) || Objects.equals(execution.getGoal(), alwaysRunGoal))) {
766 return true;
767 }
768 }
769 }
770 return false;
771 }
772
773 @Override
774 public void saveCacheReport(MavenSession session) {
775 try {
776 CacheReport cacheReport = new CacheReport();
777 for (CacheResult result : cacheResults.values()) {
778 ProjectReport projectReport = new ProjectReport();
779 CacheContext context = result.getContext();
780 MavenProject project = context.getProject();
781 projectReport.setGroupId(project.getGroupId());
782 projectReport.setArtifactId(project.getArtifactId());
783 projectReport.setChecksum(context.getInputInfo().getChecksum());
784 boolean checksumMatched = result.getStatus() != RestoreStatus.EMPTY;
785 projectReport.setChecksumMatched(checksumMatched);
786 projectReport.setLifecycleMatched(checksumMatched && result.isSuccess());
787 projectReport.setSource(String.valueOf(result.getSource()));
788 if (result.getSource() == CacheSource.REMOTE) {
789 projectReport.setUrl(remoteCache.getResourceUrl(context, BUILDINFO_XML));
790 } else if (result.getSource() == CacheSource.BUILD && cacheConfig.isSaveToRemote()) {
791 projectReport.setSharedToRemote(true);
792 projectReport.setUrl(remoteCache.getResourceUrl(context, BUILDINFO_XML));
793 }
794 cacheReport.addProject(projectReport);
795 }
796
797 String buildId = UUID.randomUUID().toString();
798 localCache.saveCacheReport(buildId, session, cacheReport);
799 } catch (Exception e) {
800 LOGGER.error("Cannot save incremental build aggregated report", e);
801 }
802 }
803
804 private void populateGitInfo(Build build, MavenSession session) {
805 if (scm == null) {
806 synchronized (this) {
807 if (scm == null) {
808 try {
809 scm = CacheUtils.readGitInfo(session);
810 } catch (IOException e) {
811 scm = new Scm();
812 LOGGER.error("Cannot populate git info", e);
813 }
814 }
815 }
816 }
817 build.getDto().setScm(scm);
818 }
819
820 private void zipAndAttachArtifact(MavenProject project, Path dir, String classifier) throws IOException {
821 Path temp = Files.createTempFile("maven-incremental", project.getArtifactId());
822 temp.toFile().deleteOnExit();
823 CacheUtils.zip(dir, temp);
824 projectHelper.attachArtifact(project, "zip", classifier, temp.toFile());
825 }
826
827 private String pathToClassifier(Path relative) {
828 final int nameCount = relative.getNameCount();
829 List<String> segments = new ArrayList<>(nameCount + 1);
830 for (int i = 0; i < nameCount; i++) {
831 segments.add(relative.getName(i).toFile().getName());
832 }
833
834 return BUILD_PREFIX + StringUtils.join(segments.iterator(), FILE_SEPARATOR_SUBST);
835 }
836
837 private Path classifierToPath(Path outputDir, String classifier) {
838 classifier = StringUtils.removeStart(classifier, BUILD_PREFIX);
839 final String relPath = replace(classifier, FILE_SEPARATOR_SUBST, File.separator);
840 return outputDir.resolve(relPath);
841 }
842
843 private void restoreGeneratedSources(Artifact artifact, Path artifactFilePath, MavenProject project)
844 throws IOException {
845 final Path targetDir = Paths.get(project.getBuild().getDirectory());
846 final Path outputDir = classifierToPath(targetDir, artifact.getClassifier());
847 if (Files.exists(outputDir)) {
848 FileUtils.cleanDirectory(outputDir.toFile());
849 } else {
850 Files.createDirectories(outputDir);
851 }
852 CacheUtils.unzip(artifactFilePath, outputDir);
853 }
854
855
856 public void attachGeneratedSources(MavenProject project) throws IOException {
857 final Path targetDir = Paths.get(project.getBuild().getDirectory());
858
859 final Path generatedSourcesDir = targetDir.resolve("generated-sources");
860 attachDirIfNotEmpty(generatedSourcesDir, targetDir, project);
861
862 final Path generatedTestSourcesDir = targetDir.resolve("generated-test-sources");
863 attachDirIfNotEmpty(generatedTestSourcesDir, targetDir, project);
864
865 Set<String> sourceRoots = new TreeSet<>();
866 if (project.getCompileSourceRoots() != null) {
867 sourceRoots.addAll(project.getCompileSourceRoots());
868 }
869 if (project.getTestCompileSourceRoots() != null) {
870 sourceRoots.addAll(project.getTestCompileSourceRoots());
871 }
872
873 for (String sourceRoot : sourceRoots) {
874 final Path sourceRootPath = Paths.get(sourceRoot);
875 if (Files.isDirectory(sourceRootPath)
876 && sourceRootPath.startsWith(targetDir)
877 && !(sourceRootPath.startsWith(generatedSourcesDir)
878 || sourceRootPath.startsWith(generatedTestSourcesDir))) {
879 attachDirIfNotEmpty(sourceRootPath, targetDir, project);
880 }
881 }
882 }
883
884 private void attachOutputs(MavenProject project) throws IOException {
885 final List<String> attachedDirs = cacheConfig.getAttachedOutputs();
886 for (String dir : attachedDirs) {
887 final Path targetDir = Paths.get(project.getBuild().getDirectory());
888 final Path outputDir = targetDir.resolve(dir);
889 attachDirIfNotEmpty(outputDir, targetDir, project);
890 }
891 }
892
893 private void attachDirIfNotEmpty(Path candidateSubDir, Path parentDir, MavenProject project) throws IOException {
894 if (Files.isDirectory(candidateSubDir) && hasFiles(candidateSubDir)) {
895 final Path relativePath = parentDir.relativize(candidateSubDir);
896 final String classifier = pathToClassifier(relativePath);
897 zipAndAttachArtifact(project, candidateSubDir, classifier);
898 LOGGER.debug("Attached directory: {}", candidateSubDir);
899 }
900 }
901
902 private boolean hasFiles(Path candidateSubDir) throws IOException {
903 final MutableBoolean hasFiles = new MutableBoolean();
904 Files.walkFileTree(candidateSubDir, new SimpleFileVisitor<Path>() {
905
906 @Override
907 public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) {
908 hasFiles.setTrue();
909 return FileVisitResult.TERMINATE;
910 }
911 });
912 return hasFiles.booleanValue();
913 }
914
915 private boolean isOutputArtifact(String name) {
916 List<Pattern> excludePatterns = cacheConfig.getExcludePatterns();
917 for (Pattern pattern : excludePatterns) {
918 if (pattern.matcher(name).matches()) {
919 return false;
920 }
921 }
922 return true;
923 }
924 }