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