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