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