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