View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.lifecycle.internal.concurrent;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.xml.stream.XMLStreamException;
24  
25  import java.io.IOException;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Optional;
33  import java.util.Set;
34  import java.util.concurrent.ConcurrentHashMap;
35  import java.util.concurrent.ExecutionException;
36  import java.util.concurrent.Executors;
37  import java.util.concurrent.TimeUnit;
38  import java.util.concurrent.locks.ReadWriteLock;
39  import java.util.concurrent.locks.ReentrantReadWriteLock;
40  import java.util.stream.Collectors;
41  import java.util.stream.Stream;
42  
43  import org.apache.maven.api.Lifecycle;
44  import org.apache.maven.api.services.LifecycleRegistry;
45  import org.apache.maven.api.services.MavenException;
46  import org.apache.maven.api.xml.XmlNode;
47  import org.apache.maven.execution.BuildFailure;
48  import org.apache.maven.execution.BuildSuccess;
49  import org.apache.maven.execution.ExecutionEvent;
50  import org.apache.maven.execution.MavenExecutionRequest;
51  import org.apache.maven.execution.MavenSession;
52  import org.apache.maven.execution.ProjectDependencyGraph;
53  import org.apache.maven.execution.ProjectExecutionEvent;
54  import org.apache.maven.execution.ProjectExecutionListener;
55  import org.apache.maven.internal.MultilineMessageHelper;
56  import org.apache.maven.internal.impl.DefaultLifecycleRegistry;
57  import org.apache.maven.internal.impl.util.PhasingExecutor;
58  import org.apache.maven.internal.transformation.ConsumerPomArtifactTransformer;
59  import org.apache.maven.internal.xml.XmlNodeImpl;
60  import org.apache.maven.lifecycle.LifecycleExecutionException;
61  import org.apache.maven.lifecycle.LifecycleNotFoundException;
62  import org.apache.maven.lifecycle.LifecyclePhaseNotFoundException;
63  import org.apache.maven.lifecycle.MojoExecutionConfigurator;
64  import org.apache.maven.lifecycle.internal.BuildThreadFactory;
65  import org.apache.maven.lifecycle.internal.CompoundProjectExecutionListener;
66  import org.apache.maven.lifecycle.internal.ExecutionEventCatapult;
67  import org.apache.maven.lifecycle.internal.GoalTask;
68  import org.apache.maven.lifecycle.internal.LifecycleTask;
69  import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
70  import org.apache.maven.lifecycle.internal.MojoExecutor;
71  import org.apache.maven.lifecycle.internal.ReactorContext;
72  import org.apache.maven.lifecycle.internal.Task;
73  import org.apache.maven.lifecycle.internal.TaskSegment;
74  import org.apache.maven.model.Plugin;
75  import org.apache.maven.model.PluginExecution;
76  import org.apache.maven.plugin.MavenPluginManager;
77  import org.apache.maven.plugin.MojoExecution;
78  import org.apache.maven.plugin.MojoNotFoundException;
79  import org.apache.maven.plugin.PluginDescriptorParsingException;
80  import org.apache.maven.plugin.descriptor.MojoDescriptor;
81  import org.apache.maven.plugin.descriptor.Parameter;
82  import org.apache.maven.plugin.descriptor.PluginDescriptor;
83  import org.apache.maven.project.MavenProject;
84  import org.codehaus.plexus.classworlds.realm.ClassRealm;
85  import org.eclipse.aether.repository.RemoteRepository;
86  import org.slf4j.Logger;
87  import org.slf4j.LoggerFactory;
88  
89  import static org.apache.maven.api.Lifecycle.AFTER;
90  import static org.apache.maven.api.Lifecycle.AT;
91  import static org.apache.maven.api.Lifecycle.BEFORE;
92  import static org.apache.maven.api.Lifecycle.Phase.PACKAGE;
93  import static org.apache.maven.api.Lifecycle.Phase.READY;
94  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.CREATED;
95  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.EXECUTED;
96  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.FAILED;
97  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.PLAN;
98  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.PLANNING;
99  import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.SCHEDULED;
100 import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.SETUP;
101 import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.TEARDOWN;
102 
103 /**
104  * Builds the full lifecycle in weave-mode (phase by phase as opposed to project-by-project).
105  * <p>
106  * This builder uses a number of threads equal to the minimum of the degree of concurrency (which is the thread count
107  * set with <code>-T</code> on the command-line) and the number of projects to build. As such, building a single project
108  * will always result in a sequential build, regardless of the thread count.
109  * </p>
110  * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
111  *
112  * @since 3.0
113  *         Builds one or more lifecycles for a full module
114  *         NOTE: This class is not part of any public api and can be changed or deleted without prior notice.
115  */
116 @Named
117 public class BuildPlanExecutor {
118 
119     private static final Object GLOBAL = new Object();
120 
121     private final Logger logger = LoggerFactory.getLogger(getClass());
122 
123     private final MojoExecutor mojoExecutor;
124     private final ExecutionEventCatapult eventCatapult;
125     private final ProjectExecutionListener projectExecutionListener;
126     private final ConsumerPomArtifactTransformer consumerPomArtifactTransformer;
127     private final BuildPlanLogger buildPlanLogger;
128     private final Map<String, MojoExecutionConfigurator> mojoExecutionConfigurators;
129     private final MavenPluginManager mavenPluginManager;
130     private final MojoDescriptorCreator mojoDescriptorCreator;
131     private final LifecycleRegistry lifecycles;
132 
133     @Inject
134     @SuppressWarnings("checkstyle:ParameterNumber")
135     public BuildPlanExecutor(
136             @Named("concurrent") MojoExecutor mojoExecutor,
137             ExecutionEventCatapult eventCatapult,
138             List<ProjectExecutionListener> listeners,
139             ConsumerPomArtifactTransformer consumerPomArtifactTransformer,
140             BuildPlanLogger buildPlanLogger,
141             Map<String, MojoExecutionConfigurator> mojoExecutionConfigurators,
142             MavenPluginManager mavenPluginManager,
143             MojoDescriptorCreator mojoDescriptorCreator,
144             LifecycleRegistry lifecycles) {
145         this.mojoExecutor = mojoExecutor;
146         this.eventCatapult = eventCatapult;
147         this.projectExecutionListener = new CompoundProjectExecutionListener(listeners);
148         this.consumerPomArtifactTransformer = consumerPomArtifactTransformer;
149         this.buildPlanLogger = buildPlanLogger;
150         this.mojoExecutionConfigurators = mojoExecutionConfigurators;
151         this.mavenPluginManager = mavenPluginManager;
152         this.mojoDescriptorCreator = mojoDescriptorCreator;
153         this.lifecycles = lifecycles;
154     }
155 
156     public void execute(MavenSession session, ReactorContext reactorContext, List<TaskSegment> taskSegments)
157             throws ExecutionException, InterruptedException {
158         try (BuildContext ctx = new BuildContext(session, reactorContext, taskSegments)) {
159             ctx.execute();
160         }
161     }
162 
163     class BuildContext implements AutoCloseable {
164         final MavenSession session;
165         final ReactorContext reactorContext;
166         final PhasingExecutor executor;
167         final Map<Object, Clock> clocks = new ConcurrentHashMap<>();
168         final ReadWriteLock lock = new ReentrantReadWriteLock();
169         final int threads;
170         BuildPlan plan;
171 
172         BuildContext(MavenSession session, ReactorContext reactorContext, List<TaskSegment> taskSegments) {
173             this.session = session;
174             this.reactorContext = reactorContext;
175             this.threads = Math.min(
176                     session.getRequest().getDegreeOfConcurrency(),
177                     session.getProjects().size());
178             // Propagate the parallel flag to the root session
179             session.setParallel(threads > 1);
180             this.executor = new PhasingExecutor(Executors.newFixedThreadPool(threads, new BuildThreadFactory()));
181 
182             // build initial plan
183             this.plan = buildInitialPlan(taskSegments);
184         }
185 
186         BuildContext() {
187             this.session = null;
188             this.reactorContext = null;
189             this.threads = 1;
190             this.executor = null;
191             this.plan = null;
192         }
193 
194         public BuildPlan buildInitialPlan(List<TaskSegment> taskSegments) {
195             int nThreads = Math.min(
196                     session.getRequest().getDegreeOfConcurrency(),
197                     session.getProjects().size());
198             boolean parallel = nThreads > 1;
199             // Propagate the parallel flag to the root session
200             session.setParallel(parallel);
201 
202             ProjectDependencyGraph dependencyGraph = session.getProjectDependencyGraph();
203             MavenProject rootProject = session.getTopLevelProject();
204 
205             Map<MavenProject, List<MavenProject>> allProjects = new LinkedHashMap<>();
206             dependencyGraph
207                     .getSortedProjects()
208                     .forEach(p -> allProjects.put(p, dependencyGraph.getUpstreamProjects(p, false)));
209 
210             BuildPlan plan = new BuildPlan(allProjects);
211             for (TaskSegment taskSegment : taskSegments) {
212                 Map<MavenProject, List<MavenProject>> projects = taskSegment.isAggregating()
213                         ? Collections.singletonMap(rootProject, allProjects.get(rootProject))
214                         : allProjects;
215 
216                 BuildPlan segment = calculateMojoExecutions(projects, taskSegment.getTasks());
217                 plan.then(segment);
218             }
219 
220             // Create plan, setup and teardown
221             for (MavenProject project : plan.getAllProjects().keySet()) {
222                 BuildStep pplan = new BuildStep(PLAN, project, null);
223                 pplan.status.set(PLANNING); // the plan step always need planning
224                 BuildStep setup = new BuildStep(SETUP, project, null);
225                 BuildStep teardown = new BuildStep(TEARDOWN, project, null);
226                 setup.executeAfter(pplan);
227                 plan.steps(project).forEach(step -> {
228                     if (step.predecessors.isEmpty()) {
229                         step.executeAfter(setup);
230                     } else if (step.successors.isEmpty()) {
231                         teardown.executeAfter(step);
232                     }
233                 });
234                 Stream.of(pplan, setup, teardown).forEach(step -> plan.addStep(project, step.name, step));
235             }
236 
237             return plan;
238         }
239 
240         private void checkUnboundVersions(BuildPlan buildPlan) {
241             String defaulModelId = DefaultLifecycleRegistry.DEFAULT_LIFECYCLE_MODELID;
242             List<String> unversionedPlugins = buildPlan
243                     .allSteps()
244                     .flatMap(step -> step.mojos.values().stream().flatMap(map -> map.values().stream()))
245                     .map(MojoExecution::getPlugin)
246                     .filter(p -> p.getLocation("version") != null
247                             && p.getLocation("version").getSource() != null
248                             && defaulModelId.equals(
249                                     p.getLocation("version").getSource().getModelId()))
250                     .distinct()
251                     .map(Plugin::getArtifactId) // managed by us, groupId is always o.a.m.plugins
252                     .toList();
253             if (!unversionedPlugins.isEmpty()) {
254                 logger.warn("Version not locked for default bindings plugins " + unversionedPlugins
255                         + ", you should define versions in pluginManagement section of your " + "pom.xml or parent");
256             }
257         }
258 
259         private void checkThreadSafety(BuildPlan buildPlan) {
260             if (threads > 1) {
261                 Set<MojoExecution> unsafeExecutions = buildPlan
262                         .allSteps()
263                         .flatMap(step -> step.mojos.values().stream().flatMap(map -> map.values().stream()))
264                         .filter(execution -> !execution.getMojoDescriptor().isV4Api())
265                         .collect(Collectors.toSet());
266                 if (!unsafeExecutions.isEmpty()) {
267                     for (String s : MultilineMessageHelper.format(
268                             """
269                                 Your build is requesting concurrent execution, but this project contains the \
270                                 following plugin(s) that have goals not built with Maven 4 to support concurrent \
271                                 execution. While this /may/ work fine, please look for plugin updates and/or \
272                                 request plugins be made thread-safe. If reporting an issue, report it against the \
273                                 plugin in question, not against Apache Maven.""")) {
274                         logger.warn(s);
275                     }
276                     if (logger.isDebugEnabled()) {
277                         Set<MojoDescriptor> unsafeGoals = unsafeExecutions.stream()
278                                 .map(MojoExecution::getMojoDescriptor)
279                                 .collect(Collectors.toSet());
280                         logger.warn("The following goals are not Maven 4 goals:");
281                         for (MojoDescriptor unsafeGoal : unsafeGoals) {
282                             logger.warn("  " + unsafeGoal.getId());
283                         }
284                     } else {
285                         Set<Plugin> unsafePlugins = unsafeExecutions.stream()
286                                 .map(MojoExecution::getPlugin)
287                                 .collect(Collectors.toSet());
288                         logger.warn("The following plugins are not Maven 4 plugins:");
289                         for (Plugin unsafePlugin : unsafePlugins) {
290                             logger.warn("  " + unsafePlugin.getId());
291                         }
292                         logger.warn("");
293                         logger.warn("Enable verbose output (-X) to see precisely which goals are not marked as"
294                                 + " thread-safe.");
295                     }
296                     logger.warn(MultilineMessageHelper.separatorLine());
297                 }
298             }
299         }
300 
301         void execute() {
302             try (var phase = executor.phase()) {
303                 plan();
304                 executePlan();
305             } catch (Exception e) {
306                 session.getResult().addException(e);
307             }
308         }
309 
310         @Override
311         public void close() {
312             this.executor.close();
313         }
314 
315         private void executePlan() {
316             if (reactorContext.getReactorBuildStatus().isHalted()) {
317                 return;
318             }
319             Clock global = clocks.computeIfAbsent(GLOBAL, p -> new Clock());
320             global.start();
321             lock.readLock().lock();
322             try {
323                 plan.sortedNodes().stream()
324                         .filter(step -> step.status.get() == CREATED)
325                         .filter(step -> step.predecessors.stream().allMatch(s -> s.status.get() == EXECUTED))
326                         .filter(step -> step.status.compareAndSet(CREATED, SCHEDULED))
327                         .forEach(step -> {
328                             boolean nextIsPlanning = step.successors.stream().anyMatch(st -> PLAN.equals(st.name));
329                             executor.execute(() -> {
330                                 try {
331                                     executeStep(step);
332                                     if (nextIsPlanning) {
333                                         lock.writeLock().lock();
334                                         try {
335                                             plan();
336                                         } finally {
337                                             lock.writeLock().unlock();
338                                         }
339                                     }
340                                     executePlan();
341                                 } catch (Exception e) {
342                                     step.status.compareAndSet(SCHEDULED, FAILED);
343                                     global.stop();
344                                     handleBuildError(reactorContext, session, step.project, e, global);
345                                 }
346                             });
347                         });
348             } finally {
349                 lock.readLock().unlock();
350             }
351         }
352 
353         private void executeStep(BuildStep step) throws IOException, LifecycleExecutionException {
354             Clock clock = getClock(step.project);
355             switch (step.name) {
356                 case PLAN:
357                     // Planning steps should be executed out of normal execution
358                     throw new IllegalStateException();
359                 case SETUP:
360                     consumerPomArtifactTransformer.injectTransformedArtifacts(
361                             session.getRepositorySession(), step.project);
362                     projectExecutionListener.beforeProjectExecution(new ProjectExecutionEvent(session, step.project));
363                     eventCatapult.fire(ExecutionEvent.Type.ProjectStarted, session, null);
364                     break;
365                 case TEARDOWN:
366                     projectExecutionListener.afterProjectExecutionSuccess(
367                             new ProjectExecutionEvent(session, step.project, Collections.emptyList()));
368                     reactorContext
369                             .getResult()
370                             .addBuildSummary(new BuildSuccess(step.project, clock.wallTime(), clock.execTime()));
371                     eventCatapult.fire(ExecutionEvent.Type.ProjectSucceeded, session, null);
372                     break;
373                 default:
374                     List<MojoExecution> executions = step.executions().collect(Collectors.toList());
375                     if (!executions.isEmpty()) {
376                         attachToThread(step.project);
377                         session.setCurrentProject(step.project);
378                         clock.start();
379                         executions.forEach(mojoExecution -> {
380                             mojoExecutionConfigurator(mojoExecution).configure(step.project, mojoExecution, true);
381                             finalizeMojoConfiguration(mojoExecution);
382                         });
383                         mojoExecutor.execute(session, executions);
384                         clock.stop();
385                     }
386                     break;
387             }
388             step.status.compareAndSet(SCHEDULED, EXECUTED);
389         }
390 
391         private Clock getClock(Object key) {
392             return clocks.computeIfAbsent(key, p -> new Clock());
393         }
394 
395         private void plan() {
396             lock.writeLock().lock();
397             try {
398                 Set<BuildStep> planSteps = plan.allSteps()
399                         .filter(st -> PLAN.equals(st.name))
400                         .filter(step -> step.predecessors.stream().allMatch(s -> s.status.get() == EXECUTED))
401                         .filter(step -> step.status.compareAndSet(PLANNING, SCHEDULED))
402                         .collect(Collectors.toSet());
403                 for (BuildStep step : planSteps) {
404                     MavenProject project = step.project;
405                     for (Plugin plugin : project.getBuild().getPlugins()) {
406                         for (PluginExecution execution : plugin.getExecutions()) {
407                             for (String goal : execution.getGoals()) {
408                                 MojoDescriptor mojoDescriptor = getMojoDescriptor(project, plugin, goal);
409                                 String phase =
410                                         execution.getPhase() != null ? execution.getPhase() : mojoDescriptor.getPhase();
411                                 String tmpResolvedPhase = plan.aliases().getOrDefault(phase, phase);
412                                 String resolvedPhase = tmpResolvedPhase.startsWith(AT)
413                                         ? tmpResolvedPhase.substring(AT.length())
414                                         : tmpResolvedPhase;
415                                 plan.step(project, resolvedPhase).ifPresent(n -> {
416                                     MojoExecution mojoExecution = new MojoExecution(mojoDescriptor, execution.getId());
417                                     mojoExecution.setLifecyclePhase(phase);
418                                     n.addMojo(mojoExecution, execution.getPriority());
419                                     if (mojoDescriptor.getDependencyCollectionRequired() != null
420                                             || mojoDescriptor.getDependencyResolutionRequired() != null) {
421                                         for (MavenProject p :
422                                                 plan.getAllProjects().get(project)) {
423                                             plan.step(p, AFTER + PACKAGE)
424                                                     .ifPresent(a -> plan.requiredStep(project, resolvedPhase)
425                                                             .executeAfter(a));
426                                         }
427                                     }
428                                 });
429                             }
430                         }
431                     }
432                 }
433 
434                 BuildPlan buildPlan = plan;
435                 for (BuildStep step :
436                         planSteps.stream().flatMap(p -> plan.steps(p.project)).toList()) {
437                     for (MojoExecution execution : step.executions().toList()) {
438                         buildPlan = computeForkPlan(step, execution, buildPlan);
439                     }
440                 }
441 
442                 for (BuildStep step : planSteps) {
443                     MavenProject project = step.project;
444                     buildPlanLogger.writePlan(plan, project);
445                     step.status.compareAndSet(SCHEDULED, EXECUTED);
446                 }
447 
448                 checkThreadSafety(plan);
449                 checkUnboundVersions(plan);
450             } finally {
451                 lock.writeLock().unlock();
452             }
453         }
454 
455         protected BuildPlan computeForkPlan(BuildStep step, MojoExecution execution, BuildPlan buildPlan) {
456             MojoDescriptor mojoDescriptor = execution.getMojoDescriptor();
457             PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor();
458             String forkedGoal = mojoDescriptor.getExecuteGoal();
459             String phase = mojoDescriptor.getExecutePhase();
460             // We have a fork goal
461             if (forkedGoal != null && !forkedGoal.isEmpty()) {
462                 MojoDescriptor forkedMojoDescriptor = pluginDescriptor.getMojo(forkedGoal);
463                 if (forkedMojoDescriptor == null) {
464                     throw new MavenException(new MojoNotFoundException(forkedGoal, pluginDescriptor));
465                 }
466 
467                 List<MavenProject> toFork = new ArrayList<>();
468                 toFork.add(step.project);
469                 if (mojoDescriptor.isAggregator() && step.project.getCollectedProjects() != null) {
470                     toFork.addAll(step.project.getCollectedProjects());
471                 }
472 
473                 BuildPlan plan = new BuildPlan();
474                 for (MavenProject project : toFork) {
475                     BuildStep st = new BuildStep(forkedGoal, project, null);
476                     MojoExecution mojoExecution = new MojoExecution(forkedMojoDescriptor, forkedGoal);
477                     st.addMojo(mojoExecution, 0);
478                     Map<String, BuildStep> n = new HashMap<>();
479                     n.put(forkedGoal, st);
480                     plan.addProject(project, n);
481                 }
482 
483                 for (BuildStep astep : plan.allSteps().toList()) {
484                     for (MojoExecution aexecution : astep.executions().toList()) {
485                         plan = computeForkPlan(astep, aexecution, plan);
486                     }
487                 }
488 
489                 return plan;
490 
491             } else if (phase != null && !phase.isEmpty()) {
492                 String forkedLifecycle = mojoDescriptor.getExecuteLifecycle();
493                 Lifecycle lifecycle;
494                 if (forkedLifecycle != null && !forkedLifecycle.isEmpty()) {
495                     org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle lifecycleOverlay;
496                     try {
497                         lifecycleOverlay = pluginDescriptor.getLifecycleMapping(forkedLifecycle);
498                     } catch (IOException | XMLStreamException e) {
499                         throw new MavenException(new PluginDescriptorParsingException(
500                                 pluginDescriptor.getPlugin(), pluginDescriptor.getSource(), e));
501                     }
502                     if (lifecycleOverlay == null) {
503                         Optional<Lifecycle> lf = lifecycles.lookup(forkedLifecycle);
504                         if (lf.isPresent()) {
505                             lifecycle = lf.get();
506                         } else {
507                             throw new MavenException(new LifecycleNotFoundException(forkedLifecycle));
508                         }
509                     } else {
510                         lifecycle = new PluginLifecycle(lifecycleOverlay, pluginDescriptor);
511                     }
512                 } else {
513                     if (execution.getLifecyclePhase() != null) {
514                         String n = execution.getLifecyclePhase();
515                         String phaseName = n.startsWith(BEFORE)
516                                 ? n.substring(BEFORE.length())
517                                 : n.startsWith(AFTER) ? n.substring(AFTER.length()) : n;
518                         lifecycle = lifecycles.stream()
519                                 .filter(l -> l.allPhases().anyMatch(p -> phaseName.equals(p.name())))
520                                 .findFirst()
521                                 .orElse(null);
522                         if (lifecycle == null) {
523                             throw new IllegalStateException();
524                         }
525                     } else {
526                         lifecycle = lifecycles.require(Lifecycle.DEFAULT);
527                     }
528                 }
529 
530                 String resolvedPhase = getResolvedPhase(lifecycle, phase);
531 
532                 Map<MavenProject, List<MavenProject>> map = Collections.singletonMap(
533                         step.project, plan.getAllProjects().get(step.project));
534                 BuildPlan forkedPlan = calculateLifecycleMappings(map, lifecycle, resolvedPhase);
535                 forkedPlan.then(buildPlan);
536                 return forkedPlan;
537             } else {
538                 return buildPlan;
539             }
540         }
541 
542         private String getResolvedPhase(Lifecycle lifecycle, String phase) {
543             return lifecycle.aliases().stream()
544                     .filter(a -> phase.equals(a.v3Phase()))
545                     .findFirst()
546                     .map(Lifecycle.Alias::v4Phase)
547                     .orElse(phase);
548         }
549 
550         private String getResolvedPhase(String phase) {
551             return lifecycles.stream()
552                     .flatMap(l -> l.aliases().stream())
553                     .filter(a -> phase.equals(a.v3Phase()))
554                     .findFirst()
555                     .map(Lifecycle.Alias::v4Phase)
556                     .orElse(phase);
557         }
558 
559         protected void handleBuildError(
560                 final ReactorContext buildContext,
561                 final MavenSession session,
562                 final MavenProject mavenProject,
563                 Throwable t,
564                 final Clock clock) {
565             // record the error and mark the project as failed
566             buildContext.getResult().addException(t);
567             buildContext
568                     .getResult()
569                     .addBuildSummary(new BuildFailure(mavenProject, clock.execTime(), clock.wallTime(), t));
570 
571             // notify listeners about "soft" project build failures only
572             if (t instanceof Exception && !(t instanceof RuntimeException)) {
573                 eventCatapult.fire(ExecutionEvent.Type.ProjectFailed, session, null, (Exception) t);
574             }
575 
576             // reactor failure modes
577             if (t instanceof RuntimeException || !(t instanceof Exception)) {
578                 // fail fast on RuntimeExceptions, Errors and "other" Throwables
579                 // assume these are system errors and further build is meaningless
580                 buildContext.getReactorBuildStatus().halt();
581             } else if (MavenExecutionRequest.REACTOR_FAIL_NEVER.equals(session.getReactorFailureBehavior())) {
582                 // continue the build
583             } else if (MavenExecutionRequest.REACTOR_FAIL_AT_END.equals(session.getReactorFailureBehavior())) {
584                 // continue the build but ban all projects that depend on the failed one
585                 buildContext.getReactorBuildStatus().blackList(mavenProject);
586             } else if (MavenExecutionRequest.REACTOR_FAIL_FAST.equals(session.getReactorFailureBehavior())) {
587                 buildContext.getReactorBuildStatus().halt();
588             } else {
589                 logger.error("invalid reactor failure behavior " + session.getReactorFailureBehavior());
590                 buildContext.getReactorBuildStatus().halt();
591             }
592         }
593 
594         public BuildPlan calculateMojoExecutions(Map<MavenProject, List<MavenProject>> projects, List<Task> tasks) {
595             BuildPlan buildPlan = new BuildPlan(projects);
596 
597             for (Task task : tasks) {
598                 BuildPlan step;
599 
600                 if (task instanceof GoalTask) {
601                     String pluginGoal = task.getValue();
602 
603                     String executionId = "default-cli";
604                     int executionIdx = pluginGoal.indexOf('@');
605                     if (executionIdx > 0) {
606                         executionId = pluginGoal.substring(executionIdx + 1);
607                     }
608 
609                     step = new BuildPlan();
610                     for (MavenProject project : projects.keySet()) {
611                         BuildStep st = new BuildStep(pluginGoal, project, null);
612                         MojoDescriptor mojoDescriptor = getMojoDescriptor(project, pluginGoal);
613                         MojoExecution mojoExecution =
614                                 new MojoExecution(mojoDescriptor, executionId, MojoExecution.Source.CLI);
615                         st.addMojo(mojoExecution, 0);
616                         Map<String, BuildStep> n = new HashMap<>();
617                         n.put(pluginGoal, st);
618                         step.addProject(project, n);
619                     }
620                 } else if (task instanceof LifecycleTask) {
621                     String lifecyclePhase = task.getValue();
622 
623                     step = calculateLifecycleMappings(projects, lifecyclePhase);
624 
625                 } else {
626                     throw new IllegalStateException("unexpected task " + task);
627                 }
628 
629                 buildPlan.then(step);
630             }
631 
632             return buildPlan;
633         }
634 
635         private MojoDescriptor getMojoDescriptor(MavenProject project, Plugin plugin, String goal) {
636             try {
637                 return mavenPluginManager.getMojoDescriptor(
638                         plugin, goal, project.getRemotePluginRepositories(), session.getRepositorySession());
639             } catch (MavenException e) {
640                 throw e;
641             } catch (Exception e) {
642                 throw new MavenException(e);
643             }
644         }
645 
646         private MojoDescriptor getMojoDescriptor(MavenProject project, String task) {
647             try {
648                 return mojoDescriptorCreator.getMojoDescriptor(task, session, project);
649             } catch (MavenException e) {
650                 throw e;
651             } catch (Exception e) {
652                 throw new MavenException(e);
653             }
654         }
655 
656         public BuildPlan calculateLifecycleMappings(
657                 Map<MavenProject, List<MavenProject>> projects, String lifecyclePhase) {
658 
659             String resolvedPhase = getResolvedPhase(lifecyclePhase);
660             String mainPhase = resolvedPhase.startsWith(BEFORE)
661                     ? resolvedPhase.substring(BEFORE.length())
662                     : resolvedPhase.startsWith(AFTER)
663                             ? resolvedPhase.substring(AFTER.length())
664                             : resolvedPhase.startsWith(AT) ? resolvedPhase.substring(AT.length()) : resolvedPhase;
665 
666             /*
667              * Determine the lifecycle that corresponds to the given phase.
668              */
669             Lifecycle lifecycle = lifecycles.stream()
670                     .filter(l -> l.allPhases().anyMatch(p -> mainPhase.equals(p.name())))
671                     .findFirst()
672                     .orElse(null);
673 
674             if (lifecycle == null) {
675                 throw new MavenException(new LifecyclePhaseNotFoundException(
676                         "Unknown lifecycle phase \"" + lifecyclePhase
677                                 + "\". You must specify a valid lifecycle phase"
678                                 + " or a goal in the format <plugin-prefix>:<goal> or"
679                                 + " <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: "
680                                 + lifecycles.stream()
681                                         .flatMap(l -> l.orderedPhases()
682                                                 .map(List::stream)
683                                                 .orElseGet(() -> l.allPhases().map(Lifecycle.Phase::name)))
684                                         .collect(Collectors.joining(", "))
685                                 + ".",
686                         lifecyclePhase));
687             }
688 
689             return calculateLifecycleMappings(projects, lifecycle, resolvedPhase);
690         }
691 
692         public BuildPlan calculateLifecycleMappings(
693                 Map<MavenProject, List<MavenProject>> projects, Lifecycle lifecycle, String lifecyclePhase) {
694             BuildPlan plan = new BuildPlan(projects);
695 
696             for (MavenProject project : projects.keySet()) {
697                 // For each phase, create and sequence the pre, run and post steps
698                 Map<String, BuildStep> steps = lifecycle
699                         .allPhases()
700                         .flatMap(phase -> {
701                             BuildStep a = new BuildStep(BEFORE + phase.name(), project, phase);
702                             BuildStep b = new BuildStep(phase.name(), project, phase);
703                             BuildStep c = new BuildStep(AFTER + phase.name(), project, phase);
704                             b.executeAfter(a);
705                             c.executeAfter(b);
706                             return Stream.of(a, b, c);
707                         })
708                         .collect(Collectors.toMap(n -> n.name, n -> n));
709                 // for each phase, make sure children phases are execute between pre and post steps
710                 lifecycle.allPhases().forEach(phase -> phase.phases().forEach(child -> {
711                     steps.get(BEFORE + child.name()).executeAfter(steps.get(BEFORE + phase.name()));
712                     steps.get(AFTER + phase.name()).executeAfter(steps.get(AFTER + child.name()));
713                 }));
714                 // for each phase, create links between this project phases
715                 lifecycle.allPhases().forEach(phase -> {
716                     phase.links().stream()
717                             .filter(l -> l.pointer().type() == Lifecycle.Pointer.Type.PROJECT)
718                             .forEach(link -> {
719                                 String n1 = phase.name();
720                                 String n2 = link.pointer().phase();
721                                 if (link.kind() == Lifecycle.Link.Kind.AFTER) {
722                                     steps.get(BEFORE + n1).executeAfter(steps.get(AFTER + n2));
723                                 } else {
724                                     steps.get(BEFORE + n2).executeAfter(steps.get(AFTER + n1));
725                                 }
726                             });
727                 });
728 
729                 // Only keep mojo executions before the end phase
730                 String endPhase = lifecyclePhase.startsWith(BEFORE) || lifecyclePhase.startsWith(AFTER)
731                         ? lifecyclePhase
732                         : lifecyclePhase.startsWith(AT)
733                                 ? lifecyclePhase.substring(AT.length())
734                                 : AFTER + lifecyclePhase;
735                 Set<BuildStep> toKeep = steps.get(endPhase).allPredecessors().collect(Collectors.toSet());
736                 steps.values().stream().filter(n -> !toKeep.contains(n)).forEach(BuildStep::skip);
737 
738                 plan.addProject(project, steps);
739             }
740 
741             // Create inter project dependencies
742             plan.allSteps().filter(step -> step.phase != null).forEach(step -> {
743                 Lifecycle.Phase phase = step.phase;
744                 MavenProject project = step.project;
745                 phase.links().stream()
746                         .filter(l -> l.pointer().type() != Lifecycle.Pointer.Type.PROJECT)
747                         .forEach(link -> {
748                             String n1 = phase.name();
749                             String n2 = link.pointer().phase();
750                             // for each project, if the phase in the build, link after it
751                             getLinkedProjects(projects, project, link).forEach(p -> plan.step(p, AFTER + n2)
752                                     .ifPresent(a -> plan.requiredStep(project, BEFORE + n1)
753                                             .executeAfter(a)));
754                         });
755             });
756 
757             // Keep projects in reactors by GAV
758             Map<String, MavenProject> reactorGavs =
759                     projects.keySet().stream().collect(Collectors.toMap(BuildPlanExecutor::gav, p -> p));
760 
761             // Go through all plugins
762             List<Runnable> toResolve = new ArrayList<>();
763             projects.keySet().forEach(project -> project.getBuild().getPlugins().forEach(plugin -> {
764                 MavenProject pluginProject = reactorGavs.get(gav(plugin));
765                 if (pluginProject != null) {
766                     // In order to plan the project, we need all its plugins...
767                     plan.requiredStep(project, PLAN).executeAfter(plan.requiredStep(pluginProject, READY));
768                 } else {
769                     toResolve.add(() -> resolvePlugin(session, project.getRemotePluginRepositories(), plugin));
770                 }
771             }));
772 
773             // Eagerly resolve all plugins in parallel
774             toResolve.parallelStream().forEach(Runnable::run);
775 
776             // Keep track of phase aliases
777             lifecycle.aliases().forEach(alias -> plan.aliases().put(alias.v3Phase(), alias.v4Phase()));
778 
779             return plan;
780         }
781 
782         private List<MavenProject> getLinkedProjects(
783                 Map<MavenProject, List<MavenProject>> projects, MavenProject project, Lifecycle.Link link) {
784             if (link.pointer().type() == Lifecycle.Pointer.Type.DEPENDENCIES) {
785                 // TODO: String scope = ((Lifecycle.DependenciesPointer) link.pointer()).scope();
786                 return projects.get(project);
787             } else if (link.pointer().type() == Lifecycle.Pointer.Type.CHILDREN) {
788                 return project.getCollectedProjects();
789             } else {
790                 throw new IllegalArgumentException(
791                         "Unsupported pointer type: " + link.pointer().type());
792             }
793         }
794     }
795 
796     private void resolvePlugin(MavenSession session, List<RemoteRepository> repositories, Plugin plugin) {
797         try {
798             mavenPluginManager.getPluginDescriptor(plugin, repositories, session.getRepositorySession());
799         } catch (Exception e) {
800             throw new MavenException(e);
801         }
802     }
803 
804     private static String gav(MavenProject p) {
805         return p.getGroupId() + ":" + p.getArtifactId() + ":" + p.getVersion();
806     }
807 
808     private static String gav(Plugin p) {
809         return p.getGroupId() + ":" + p.getArtifactId() + ":" + p.getVersion();
810     }
811 
812     /**
813      * Post-processes the effective configuration for the specified mojo execution. This step discards all parameters
814      * from the configuration that are not applicable to the mojo and injects the default values for any missing
815      * parameters.
816      *
817      * @param mojoExecution The mojo execution whose configuration should be finalized, must not be {@code null}.
818      */
819     private void finalizeMojoConfiguration(MojoExecution mojoExecution) {
820         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
821 
822         XmlNode executionConfiguration = mojoExecution.getConfiguration() != null
823                 ? mojoExecution.getConfiguration().getDom()
824                 : null;
825         if (executionConfiguration == null) {
826             executionConfiguration = new XmlNodeImpl("configuration");
827         }
828 
829         XmlNode defaultConfiguration = getMojoConfiguration(mojoDescriptor);
830 
831         List<XmlNode> children = new ArrayList<>();
832         if (mojoDescriptor.getParameters() != null) {
833             for (Parameter parameter : mojoDescriptor.getParameters()) {
834                 XmlNode parameterConfiguration = executionConfiguration.getChild(parameter.getName());
835 
836                 if (parameterConfiguration == null) {
837                     parameterConfiguration = executionConfiguration.getChild(parameter.getAlias());
838                 }
839 
840                 XmlNode parameterDefaults = defaultConfiguration.getChild(parameter.getName());
841 
842                 if (parameterConfiguration != null) {
843                     parameterConfiguration = parameterConfiguration.merge(parameterDefaults, Boolean.TRUE);
844                 } else {
845                     parameterConfiguration = parameterDefaults;
846                 }
847 
848                 if (parameterConfiguration != null) {
849                     Map<String, String> attributes = new HashMap<>(parameterConfiguration.getAttributes());
850 
851                     String attributeForImplementation = parameterConfiguration.getAttribute("implementation");
852                     String parameterForImplementation = parameter.getImplementation();
853                     if ((attributeForImplementation == null || attributeForImplementation.isEmpty())
854                             && ((parameterForImplementation != null) && !parameterForImplementation.isEmpty())) {
855                         attributes.put("implementation", parameter.getImplementation());
856                     }
857 
858                     parameterConfiguration = new XmlNodeImpl(
859                             parameter.getName(),
860                             parameterConfiguration.getValue(),
861                             attributes,
862                             parameterConfiguration.getChildren(),
863                             parameterConfiguration.getInputLocation());
864 
865                     children.add(parameterConfiguration);
866                 }
867             }
868         }
869         XmlNode finalConfiguration = new XmlNodeImpl("configuration", null, null, children, null);
870 
871         mojoExecution.setConfiguration(finalConfiguration);
872     }
873 
874     private XmlNode getMojoConfiguration(MojoDescriptor mojoDescriptor) {
875         if (mojoDescriptor.isV4Api()) {
876             return MojoDescriptorCreator.convert(mojoDescriptor.getMojoDescriptorV4());
877         } else {
878             return MojoDescriptorCreator.convert(mojoDescriptor).getDom();
879         }
880     }
881 
882     private MojoExecutionConfigurator mojoExecutionConfigurator(MojoExecution mojoExecution) {
883         String configuratorId = mojoExecution.getMojoDescriptor().getComponentConfigurator();
884         if (configuratorId == null) {
885             configuratorId = "default";
886         }
887 
888         MojoExecutionConfigurator mojoExecutionConfigurator = mojoExecutionConfigurators.get(configuratorId);
889 
890         if (mojoExecutionConfigurator == null) {
891             //
892             // The plugin has a custom component configurator but does not have a custom mojo execution configurator
893             // so fall back to the default mojo execution configurator.
894             //
895             mojoExecutionConfigurator = mojoExecutionConfigurators.get("default");
896         }
897         return mojoExecutionConfigurator;
898     }
899 
900     public static void attachToThread(MavenProject currentProject) {
901         ClassRealm projectRealm = currentProject.getClassRealm();
902         if (projectRealm != null) {
903             Thread.currentThread().setContextClassLoader(projectRealm);
904         }
905     }
906 
907     protected static class Clock {
908         long start;
909         long end;
910         long resumed;
911         long exec;
912 
913         protected void start() {
914             if (start == 0) {
915                 start = System.nanoTime();
916                 resumed = start;
917             } else {
918                 resumed = System.nanoTime();
919             }
920         }
921 
922         protected void stop() {
923             end = System.nanoTime();
924             exec += end - resumed;
925         }
926 
927         protected long wallTime() {
928             return TimeUnit.NANOSECONDS.toMillis(end - start);
929         }
930 
931         protected long execTime() {
932             return TimeUnit.NANOSECONDS.toMillis(exec);
933         }
934     }
935 }