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.Priority;
22 import javax.inject.Inject;
23 import javax.inject.Named;
24
25 import java.io.File;
26 import java.io.IOException;
27 import java.nio.file.Path;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32
33 import org.apache.commons.lang3.ArrayUtils;
34 import org.apache.commons.lang3.Strings;
35 import org.apache.maven.SessionScoped;
36 import org.apache.maven.buildcache.artifact.ArtifactRestorationReport;
37 import org.apache.maven.buildcache.checksum.MavenProjectInput;
38 import org.apache.maven.buildcache.xml.Build;
39 import org.apache.maven.buildcache.xml.CacheConfig;
40 import org.apache.maven.buildcache.xml.CacheState;
41 import org.apache.maven.buildcache.xml.DtoUtils;
42 import org.apache.maven.buildcache.xml.build.CompletedExecution;
43 import org.apache.maven.buildcache.xml.config.TrackedProperty;
44 import org.apache.maven.execution.MavenSession;
45 import org.apache.maven.execution.MojoExecutionEvent;
46 import org.apache.maven.execution.scope.internal.MojoExecutionScope;
47 import org.apache.maven.lifecycle.LifecycleExecutionException;
48 import org.apache.maven.plugin.MavenPluginManager;
49 import org.apache.maven.plugin.Mojo;
50 import org.apache.maven.plugin.MojoExecution;
51 import org.apache.maven.plugin.MojoExecution.Source;
52 import org.apache.maven.plugin.MojoExecutionException;
53 import org.apache.maven.plugin.MojoExecutionRunner;
54 import org.apache.maven.plugin.MojosExecutionStrategy;
55 import org.apache.maven.plugin.PluginConfigurationException;
56 import org.apache.maven.plugin.PluginContainerException;
57 import org.apache.maven.project.MavenProject;
58 import org.codehaus.plexus.util.ReflectionUtils;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import static org.apache.maven.buildcache.CacheUtils.mojoExecutionKey;
63 import static org.apache.maven.buildcache.checksum.KeyUtils.getVersionlessProjectKey;
64 import static org.apache.maven.buildcache.xml.CacheState.DISABLED;
65 import static org.apache.maven.buildcache.xml.CacheState.INITIALIZED;
66
67
68
69
70 @SessionScoped
71 @Named
72 @Priority(10)
73 @SuppressWarnings("unused")
74 public class BuildCacheMojosExecutionStrategy implements MojosExecutionStrategy {
75
76 private static final Logger LOGGER = LoggerFactory.getLogger(BuildCacheMojosExecutionStrategy.class);
77
78 private final CacheController cacheController;
79 private final CacheConfig cacheConfig;
80 private final MojoParametersListener mojoListener;
81 private final LifecyclePhasesHelper lifecyclePhasesHelper;
82 private final MavenPluginManager mavenPluginManager;
83 private final MojoExecutionScope mojoExecutionScope;
84
85 @Inject
86 public BuildCacheMojosExecutionStrategy(
87 CacheController cacheController,
88 CacheConfig cacheConfig,
89 MojoParametersListener mojoListener,
90 LifecyclePhasesHelper lifecyclePhasesHelper,
91 MavenPluginManager mavenPluginManager,
92 MojoExecutionScope mojoExecutionScope) {
93 this.cacheController = cacheController;
94 this.cacheConfig = cacheConfig;
95 this.mojoListener = mojoListener;
96 this.lifecyclePhasesHelper = lifecyclePhasesHelper;
97 this.mavenPluginManager = mavenPluginManager;
98 this.mojoExecutionScope = mojoExecutionScope;
99 }
100
101 public void execute(
102 List<MojoExecution> mojoExecutions, MavenSession session, MojoExecutionRunner mojoExecutionRunner)
103 throws LifecycleExecutionException {
104
105 try {
106 final MavenProject project = session.getCurrentProject();
107 final Source source = getSource(mojoExecutions);
108
109
110 CacheState cacheState = DISABLED;
111 CacheResult result = CacheResult.empty();
112 boolean skipCache =
113 cacheConfig.isSkipCache() || MavenProjectInput.isSkipCache(project) || isGoalClean(mojoExecutions);
114 boolean cacheIsDisabled = MavenProjectInput.isCacheDisabled(project);
115
116
117
118
119 boolean forkedExecution = lifecyclePhasesHelper.isForkedProject(project);
120 String projectName = getVersionlessProjectKey(project);
121 List<MojoExecution> cleanPhase = null;
122 if (source == Source.LIFECYCLE && !forkedExecution) {
123 if (!cacheIsDisabled) {
124 cacheState = cacheConfig.initialize();
125 if (cacheState == INITIALIZED) {
126
127 mojoListener.setCacheState(cacheState);
128 }
129 LOGGER.info("Cache is {} on project level for {}", cacheState, projectName);
130 } else {
131 LOGGER.info("Cache is explicitly disabled on project level for {}", projectName);
132 }
133 cleanPhase = lifecyclePhasesHelper.getCleanSegment(project, mojoExecutions);
134 for (MojoExecution mojoExecution : cleanPhase) {
135 mojoExecutionRunner.run(mojoExecution);
136 }
137 if (cacheState == INITIALIZED) {
138 result = cacheController.findCachedBuild(session, project, mojoExecutions, skipCache);
139 }
140 } else {
141 LOGGER.info("Cache is disabled on project level for {}", projectName);
142 }
143
144 boolean restorable = result.isSuccess() || result.isPartialSuccess();
145 boolean restored = false;
146
147 if (restorable) {
148 CacheRestorationStatus cacheRestorationStatus =
149 restoreProject(result, mojoExecutions, mojoExecutionRunner, cacheConfig);
150 restored = CacheRestorationStatus.SUCCESS == cacheRestorationStatus;
151 executeExtraCleanPhaseIfNeeded(cacheRestorationStatus, cleanPhase, mojoExecutionRunner);
152 }
153
154 try {
155 if (cacheState == INITIALIZED && !restored && !forkedExecution) {
156
157
158
159
160
161 try {
162 cacheController.stagePreExistingArtifacts(session, project);
163 } catch (IOException e) {
164 LOGGER.debug("Failed to stage pre-existing artifacts: {}", e.getMessage());
165
166 }
167 }
168
169 if (!restored) {
170 for (MojoExecution mojoExecution : mojoExecutions) {
171 if (source == Source.CLI
172 || mojoExecution.getLifecyclePhase() == null
173 || lifecyclePhasesHelper.isLaterPhaseThanClean(mojoExecution.getLifecyclePhase())) {
174 mojoExecutionRunner.run(mojoExecution);
175 }
176 }
177 }
178
179 if (cacheState == INITIALIZED && (!result.isSuccess() || !restored)) {
180 if (cacheConfig.isSkipSave()) {
181 LOGGER.debug("Cache saving is disabled.");
182 } else if (cacheConfig.isMandatoryClean()
183 && lifecyclePhasesHelper
184 .getCleanSegment(project, mojoExecutions)
185 .isEmpty()) {
186 LOGGER.debug("Cache storing is skipped since there was no \"clean\" phase.");
187 } else {
188 final Map<String, MojoExecutionEvent> executionEvents =
189 mojoListener.getProjectExecutions(project);
190 cacheController.save(result, mojoExecutions, executionEvents);
191 }
192 }
193 } finally {
194
195
196
197
198 if (cacheState == INITIALIZED && !restored && !forkedExecution) {
199 cacheController.restoreStagedArtifacts(session, project);
200 }
201 }
202
203 if (cacheConfig.isFailFast() && !result.isSuccess() && !skipCache && !forkedExecution) {
204 throw new LifecycleExecutionException(
205 "Failed to restore project[" + projectName + "] from cache, failing build.", project);
206 }
207 } catch (MojoExecutionException e) {
208 throw new LifecycleExecutionException(e.getMessage(), e);
209 }
210 }
211
212
213
214
215
216
217
218 private boolean isGoalClean(List<MojoExecution> mojoExecutions) {
219 if (mojoExecutions.stream().allMatch(mojoExecution -> "clean".equals(mojoExecution.getLifecyclePhase()))) {
220 LOGGER.info("Build cache is disabled for 'clean' goal.");
221 return true;
222 }
223 return false;
224 }
225
226
227
228
229
230
231
232
233
234
235
236
237
238 private void executeExtraCleanPhaseIfNeeded(
239 final CacheRestorationStatus cacheRestorationStatus,
240 List<MojoExecution> cleanPhase,
241 MojoExecutionRunner mojoExecutionRunner)
242 throws LifecycleExecutionException {
243 if (CacheRestorationStatus.FAILURE_NEEDS_CLEAN == cacheRestorationStatus
244 && cleanPhase != null
245 && !cleanPhase.isEmpty()) {
246 LOGGER.info("Extra clean phase is executed as cache could be partially restored.");
247 for (MojoExecution mojoExecution : cleanPhase) {
248 mojoExecutionRunner.run(mojoExecution);
249 }
250 }
251 }
252
253 private Source getSource(List<MojoExecution> mojoExecutions) {
254 if (mojoExecutions == null || mojoExecutions.isEmpty()) {
255 return null;
256 }
257 for (MojoExecution mojoExecution : mojoExecutions) {
258 if (mojoExecution.getSource() == Source.CLI) {
259 return Source.CLI;
260 }
261 }
262 return Source.LIFECYCLE;
263 }
264
265 private CacheRestorationStatus restoreProject(
266 CacheResult cacheResult,
267 List<MojoExecution> mojoExecutions,
268 MojoExecutionRunner mojoExecutionRunner,
269 CacheConfig cacheConfig)
270 throws LifecycleExecutionException, MojoExecutionException {
271
272 final Build build = cacheResult.getBuildInfo();
273 final MavenProject project = cacheResult.getContext().getProject();
274 final MavenSession session = cacheResult.getContext().getSession();
275 final List<MojoExecution> cachedSegment =
276 lifecyclePhasesHelper.getCachedSegment(project, mojoExecutions, build);
277
278
279 LOGGER.debug("Verify consistency on cached mojos");
280 Set<MojoExecution> forcedExecutionMojos = new HashSet<>();
281 for (MojoExecution cacheCandidate : cachedSegment) {
282 if (cacheController.isForcedExecution(project, cacheCandidate)) {
283 forcedExecutionMojos.add(cacheCandidate);
284 } else {
285 if (!verifyCacheConsistency(
286 cacheCandidate, build, project, session, mojoExecutionRunner, cacheConfig)) {
287 LOGGER.info("A cached mojo is not consistent, continuing with non cached build");
288 return CacheRestorationStatus.FAILURE;
289 }
290 }
291 }
292
293
294 ArtifactRestorationReport restorationReport = cacheController.restoreProjectArtifacts(cacheResult);
295 if (!restorationReport.isSuccess()) {
296 LOGGER.info("Cannot restore project artifacts, continuing with non cached build");
297 return restorationReport.isRestoredFilesInProjectDirectory()
298 ? CacheRestorationStatus.FAILURE_NEEDS_CLEAN
299 : CacheRestorationStatus.FAILURE;
300 }
301
302
303 LOGGER.debug("Execute mandatory mojos in the cache segment");
304 for (MojoExecution cacheCandidate : cachedSegment) {
305 if (forcedExecutionMojos.contains(cacheCandidate)) {
306 LOGGER.info(
307 "Mojo execution is forced by project property: {}",
308 cacheCandidate.getMojoDescriptor().getFullGoalName());
309 mojoExecutionRunner.run(cacheCandidate);
310 } else {
311 LOGGER.info(
312 "Skipping plugin execution (cached): {}",
313 cacheCandidate.getMojoDescriptor().getFullGoalName());
314
315 Mojo mojo = null;
316 mojoExecutionScope.enter();
317 try {
318 mojoExecutionScope.seed(MavenProject.class, project);
319 mojoExecutionScope.seed(MojoExecution.class, cacheCandidate);
320
321 mojo = mavenPluginManager.getConfiguredMojo(Mojo.class, session, cacheCandidate);
322 MojoExecutionEvent mojoExecutionEvent =
323 new MojoExecutionEvent(session, project, cacheCandidate, mojo);
324 mojoListener.beforeMojoExecution(mojoExecutionEvent);
325 } catch (PluginConfigurationException | PluginContainerException e) {
326 throw new RuntimeException(e);
327 } finally {
328 mojoExecutionScope.exit();
329 if (mojo != null) {
330 mavenPluginManager.releaseMojo(mojo, cacheCandidate);
331 }
332 }
333 }
334 }
335
336
337 LOGGER.debug("Execute mojos post cache segment");
338 List<MojoExecution> postCachedSegment =
339 lifecyclePhasesHelper.getPostCachedSegment(project, mojoExecutions, build);
340 for (MojoExecution mojoExecution : postCachedSegment) {
341 mojoExecutionRunner.run(mojoExecution);
342 }
343 return CacheRestorationStatus.SUCCESS;
344 }
345
346 private boolean verifyCacheConsistency(
347 MojoExecution cacheCandidate,
348 Build cachedBuild,
349 MavenProject project,
350 MavenSession session,
351 MojoExecutionRunner mojoExecutionRunner,
352 CacheConfig cacheConfig)
353 throws LifecycleExecutionException {
354 long createdTimestamp = System.currentTimeMillis();
355 boolean consistent = true;
356
357 if (!cacheConfig.getTrackedProperties(cacheCandidate).isEmpty()) {
358 Mojo mojo = null;
359 try {
360 mojo = mavenPluginManager.getConfiguredMojo(Mojo.class, session, cacheCandidate);
361 final CompletedExecution completedExecution = cachedBuild.findMojoExecutionInfo(cacheCandidate);
362 final String fullGoalName = cacheCandidate.getMojoDescriptor().getFullGoalName();
363
364 if (completedExecution != null && !isParamsMatched(project, cacheCandidate, mojo, completedExecution)) {
365 LOGGER.info(
366 "Mojo cached parameters mismatch with actual, forcing full project build. Mojo: {}",
367 fullGoalName);
368 consistent = false;
369 }
370
371 if (consistent) {
372 long elapsed = System.currentTimeMillis() - createdTimestamp;
373
374 LOGGER.debug(
375 "Plugin execution will be skipped ({} : reconciled in {} millis)", elapsed, fullGoalName);
376 }
377
378 LOGGER.debug(
379 "Checked {}, resolved mojo: {}, cached params: {}", fullGoalName, mojo, completedExecution);
380
381 } catch (PluginContainerException | PluginConfigurationException e) {
382 throw new LifecycleExecutionException("Cannot get configured mojo", e);
383 } finally {
384 if (mojo != null) {
385 mavenPluginManager.releaseMojo(mojo, cacheCandidate);
386 }
387 }
388 } else {
389 LOGGER.debug(
390 "Plugin execution will be skipped ({} : cached)",
391 cacheCandidate.getMojoDescriptor().getFullGoalName());
392 }
393
394 return consistent;
395 }
396
397 boolean isParamsMatched(
398 MavenProject project, MojoExecution mojoExecution, Mojo mojo, CompletedExecution completedExecution) {
399 List<TrackedProperty> tracked = cacheConfig.getTrackedProperties(mojoExecution);
400
401 for (TrackedProperty trackedProperty : tracked) {
402 final String propertyName = trackedProperty.getPropertyName();
403
404 String expectedValue = DtoUtils.findPropertyValue(propertyName, completedExecution);
405 if (expectedValue == null) {
406 expectedValue = trackedProperty.getDefaultValue() != null ? trackedProperty.getDefaultValue() : "null";
407 }
408
409 final String currentValue;
410 try {
411 Object value = ReflectionUtils.getValueIncludingSuperclasses(propertyName, mojo);
412
413 if (value instanceof File) {
414 Path baseDirPath = project.getBasedir().toPath();
415 Path path = ((File) value).toPath();
416 currentValue = normalizedPath(path, baseDirPath);
417 } else if (value instanceof Path) {
418 Path baseDirPath = project.getBasedir().toPath();
419 currentValue = normalizedPath(((Path) value), baseDirPath);
420 } else if (value != null && value.getClass().isArray()) {
421 currentValue = ArrayUtils.toString(value);
422 } else {
423 currentValue = String.valueOf(value);
424 }
425 } catch (IllegalAccessException e) {
426 LOGGER.error("Cannot extract plugin property {} from mojo {}", propertyName, mojo, e);
427 return false;
428 }
429
430 if (!Strings.CS.equals(currentValue, expectedValue)) {
431 if (!Strings.CS.equals(currentValue, trackedProperty.getSkipValue())) {
432 LOGGER.info(
433 "Plugin parameter mismatch found. Parameter: {}, expected: {}, actual: {}",
434 propertyName,
435 expectedValue,
436 currentValue);
437 return false;
438 } else {
439 LOGGER.warn(
440 "Cache contains plugin execution with skip flag and might be incomplete. "
441 + "Property: {}, execution {}",
442 propertyName,
443 mojoExecutionKey(mojoExecution));
444 }
445 }
446 }
447 return true;
448 }
449
450
451
452
453
454
455
456
457
458 private static String normalizedPath(Path path, Path baseDirPath) {
459 boolean isProjectSubdir = path.isAbsolute() && path.startsWith(baseDirPath);
460 if (LOGGER.isDebugEnabled()) {
461 LOGGER.debug(
462 "normalizedPath isProjectSubdir {} path '{}' - baseDirPath '{}', path.isAbsolute() {}, path.startsWith(baseDirPath) {}",
463 isProjectSubdir,
464 path,
465 baseDirPath,
466 path.isAbsolute(),
467 path.startsWith(baseDirPath));
468 }
469 Path preparedPath = isProjectSubdir ? baseDirPath.relativize(path) : path;
470 String normalizedPath = preparedPath.normalize().toString();
471 if (LOGGER.isDebugEnabled()) {
472 LOGGER.debug("normalizedPath '{}' - {} return {}", path, baseDirPath, normalizedPath);
473 }
474 return normalizedPath;
475 }
476
477 private enum CacheRestorationStatus {
478 SUCCESS,
479 FAILURE,
480 FAILURE_NEEDS_CLEAN
481 }
482 }