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