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