This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new e11b475e8b [MNG-8052] Concurrently lifecycle executor (#1429)
e11b475e8b is described below

commit e11b475e8bae20e8a2b3c0bebcacf4ef354a062e
Author: Guillaume Nodet <gno...@gmail.com>
AuthorDate: Thu Aug 29 19:44:53 2024 +0200

    [MNG-8052] Concurrently lifecycle executor (#1429)
---
 .../main/java/org/apache/maven/api/Lifecycle.java  |   1 +
 maven-core/pom.xml                                 |   8 +
 .../org/apache/maven/execution/BuildFailure.java   |  14 +-
 .../org/apache/maven/execution/BuildSuccess.java   |  13 +-
 .../org/apache/maven/execution/BuildSummary.java   |  36 +-
 .../internal/CompoundProjectExecutionListener.java |   4 +-
 .../maven/lifecycle/internal/MojoExecutor.java     |  91 +-
 .../lifecycle/internal/concurrent/BuildPlan.java   | 166 ++++
 .../internal/concurrent/BuildPlanExecutor.java     | 939 +++++++++++++++++++++
 .../internal/concurrent/BuildPlanLogger.java       | 177 ++++
 .../lifecycle/internal/concurrent/BuildStep.java   | 133 +++
 .../concurrent/ConcurrentLifecycleStarter.java     | 191 +++++
 .../internal/concurrent/ConcurrentLogOutput.java   |  81 ++
 .../internal/concurrent/MojoExecutor.java          |  59 ++
 .../internal/concurrent/PhasingExecutor.java       |  54 ++
 .../internal/concurrent/PluginLifecycle.java       |  94 +++
 .../internal/concurrent/BuildPlanCreatorTest.java  | 144 ++++
 .../internal/concurrent/PhasingExecutorTest.java}  |  37 +-
 18 files changed, 2180 insertions(+), 62 deletions(-)

diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/Lifecycle.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/Lifecycle.java
index 40d2715e33..c7612a1d84 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Lifecycle.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Lifecycle.java
@@ -56,6 +56,7 @@ public interface Lifecycle extends ExtensibleEnum {
     // ======================
     String BEFORE = "before:";
     String AFTER = "after:";
+    String AT = "at:";
 
     /**
      * Name or identifier of this lifecycle.
diff --git a/maven-core/pom.xml b/maven-core/pom.xml
index f79a0d40df..39eca04145 100644
--- a/maven-core/pom.xml
+++ b/maven-core/pom.xml
@@ -91,6 +91,14 @@ under the License.
       <groupId>org.apache.maven</groupId>
       <artifactId>maven-api-impl</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-jline</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-slf4j-provider</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.apache.maven.resolver</groupId>
       <artifactId>maven-resolver-api</artifactId>
diff --git 
a/maven-core/src/main/java/org/apache/maven/execution/BuildFailure.java 
b/maven-core/src/main/java/org/apache/maven/execution/BuildFailure.java
index cd7155076d..2c43f1e943 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/BuildFailure.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/BuildFailure.java
@@ -39,7 +39,19 @@ public class BuildFailure extends BuildSummary {
      * @param cause The cause of the build failure, may be {@code null}.
      */
     public BuildFailure(MavenProject project, long time, Throwable cause) {
-        super(project, time);
+        this(project, time, time, cause);
+    }
+
+    /**
+     * Creates a new build summary for the specified project.
+     *
+     * @param project The project being summarized, must not be {@code null}.
+     * @param execTime The exec time of the project in milliseconds.
+     * @param wallTime The wall time of the project in milliseconds.
+     * @param cause The cause of the build failure, may be {@code null}.
+     */
+    public BuildFailure(MavenProject project, long execTime, long wallTime, 
Throwable cause) {
+        super(project, execTime, wallTime);
         this.cause = cause;
     }
 
diff --git 
a/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java 
b/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java
index 0f0310d3cd..a2a4546b23 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java
@@ -33,6 +33,17 @@ public class BuildSuccess extends BuildSummary {
      * @param time The build time of the project in milliseconds.
      */
     public BuildSuccess(MavenProject project, long time) {
-        super(project, time);
+        super(project, time, time);
+    }
+
+    /**
+     * Creates a new build summary for the specified project.
+     *
+     * @param project The project being summarized, must not be {@code null}.
+     * @param wallTime The wall time of the project in milliseconds.
+     * @param execTime The exec time of the project in milliseconds.
+     */
+    public BuildSuccess(MavenProject project, long wallTime, long execTime) {
+        super(project, wallTime, execTime);
     }
 }
diff --git 
a/maven-core/src/main/java/org/apache/maven/execution/BuildSummary.java 
b/maven-core/src/main/java/org/apache/maven/execution/BuildSummary.java
index 657afb31eb..efc0da1d8b 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/BuildSummary.java
+++ b/maven-core/src/main/java/org/apache/maven/execution/BuildSummary.java
@@ -36,7 +36,12 @@ public abstract class BuildSummary {
     /**
      * The build time of the project in milliseconds.
      */
-    private final long time;
+    private final long wallTime;
+
+    /**
+     * The total amount of time spent for to run mojos in milliseconds.
+     */
+    private final long execTime;
 
     /**
      * Creates a new build summary for the specified project.
@@ -45,9 +50,21 @@ public abstract class BuildSummary {
      * @param time The build time of the project in milliseconds.
      */
     protected BuildSummary(MavenProject project, long time) {
+        this(project, time, time);
+    }
+
+    /**
+     * Creates a new build summary for the specified project.
+     *
+     * @param project The project being summarized, must not be {@code null}.
+     * @param execTime The exec time of the project in milliseconds.
+     * @param wallTime The wall time of the project in milliseconds.
+     */
+    protected BuildSummary(MavenProject project, long execTime, long wallTime) 
{
         this.project = Objects.requireNonNull(project, "project cannot be 
null");
         // TODO Validate for < 0?
-        this.time = time;
+        this.execTime = execTime;
+        this.wallTime = wallTime;
     }
 
     /**
@@ -60,11 +77,20 @@ public abstract class BuildSummary {
     }
 
     /**
-     * Gets the build time of the project in milliseconds.
+     * Gets the wall time of the project in milliseconds.
      *
-     * @return The build time of the project in milliseconds.
+     * @return The wall time of the project in milliseconds.
      */
     public long getTime() {
-        return time;
+        return execTime;
+    }
+
+    /**
+     * Gets the exec time of the project in milliseconds.
+     *
+     * @return The exec time of the project in milliseconds.
+     */
+    public long getWallTime() {
+        return wallTime;
     }
 }
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/CompoundProjectExecutionListener.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/CompoundProjectExecutionListener.java
index 34cee12b95..33eb878aab 100644
--- 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/CompoundProjectExecutionListener.java
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/CompoundProjectExecutionListener.java
@@ -24,10 +24,10 @@ import org.apache.maven.execution.ProjectExecutionEvent;
 import org.apache.maven.execution.ProjectExecutionListener;
 import org.apache.maven.lifecycle.LifecycleExecutionException;
 
-class CompoundProjectExecutionListener implements ProjectExecutionListener {
+public class CompoundProjectExecutionListener implements 
ProjectExecutionListener {
     private final Collection<ProjectExecutionListener> listeners;
 
-    CompoundProjectExecutionListener(Collection<ProjectExecutionListener> 
listeners) {
+    public 
CompoundProjectExecutionListener(Collection<ProjectExecutionListener> 
listeners) {
         this.listeners = listeners; // NB this is live injected collection
     }
 
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
index 0c3779ae7a..7a479d4d2f 100644
--- 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
@@ -214,6 +214,13 @@ public class MojoExecutor {
         doExecute(session, mojoExecution, dependencyContext);
     }
 
+    protected static class NoLock implements NoExceptionCloseable {
+        public NoLock() {}
+
+        @Override
+        public void close() {}
+    }
+
     /**
      * Aggregating mojo executions (possibly) modify all MavenProjects, 
including those that are currently in use
      * by concurrently running mojo executions. To prevent race conditions, an 
aggregating execution will block
@@ -222,54 +229,45 @@ public class MojoExecutor {
      * TODO: ideally, the builder should take care of the ordering in a 
smarter way
      * TODO: and concurrency issues fixed with MNG-7157
      */
-    private class ProjectLock implements AutoCloseable {
+    protected class ProjectLock implements NoExceptionCloseable {
         final Lock acquiredAggregatorLock;
         final OwnerReentrantLock acquiredProjectLock;
 
         ProjectLock(MavenSession session, MojoDescriptor mojoDescriptor) {
             mojos.put(Thread.currentThread(), mojoDescriptor);
-            if (session.getRequest().getDegreeOfConcurrency() > 1) {
-                boolean aggregator = mojoDescriptor.isAggregator();
-                acquiredAggregatorLock = aggregator ? 
aggregatorLock.writeLock() : aggregatorLock.readLock();
-                acquiredProjectLock = getProjectLock(session);
-                if (!acquiredAggregatorLock.tryLock()) {
-                    Thread owner = aggregatorLock.getOwner();
-                    MojoDescriptor ownerMojo = owner != null ? 
mojos.get(owner) : null;
-                    String str = ownerMojo != null ? " The " + 
ownerMojo.getId() : "An";
-                    String msg = str + " aggregator mojo is already being 
executed "
-                            + "in this parallel build, those kind of mojos 
require exclusive access to "
-                            + "reactor to prevent race conditions. This mojo 
execution will be blocked "
-                            + "until the aggregator mojo is done.";
-                    warn(msg);
-                    acquiredAggregatorLock.lock();
-                }
-                if (!acquiredProjectLock.tryLock()) {
-                    Thread owner = acquiredProjectLock.getOwner();
-                    MojoDescriptor ownerMojo = owner != null ? 
mojos.get(owner) : null;
-                    String str = ownerMojo != null ? " The " + 
ownerMojo.getId() : "A";
-                    String msg = str + " mojo is already being executed "
-                            + "on the project " + 
session.getCurrentProject().getGroupId()
-                            + ":" + 
session.getCurrentProject().getArtifactId() + ". "
-                            + "This mojo execution will be blocked "
-                            + "until the mojo is done.";
-                    warn(msg);
-                    acquiredProjectLock.lock();
-                }
-            } else {
-                acquiredAggregatorLock = null;
-                acquiredProjectLock = null;
+            boolean aggregator = mojoDescriptor.isAggregator();
+            acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : 
aggregatorLock.readLock();
+            acquiredProjectLock = getProjectLock(session);
+            if (!acquiredAggregatorLock.tryLock()) {
+                Thread owner = aggregatorLock.getOwner();
+                MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : 
null;
+                String str = ownerMojo != null ? " The " + ownerMojo.getId() : 
"An";
+                String msg = str + " aggregator mojo is already being executed 
"
+                        + "in this parallel build, those kind of mojos require 
exclusive access to "
+                        + "reactor to prevent race conditions. This mojo 
execution will be blocked "
+                        + "until the aggregator mojo is done.";
+                warn(msg);
+                acquiredAggregatorLock.lock();
+            }
+            if (!acquiredProjectLock.tryLock()) {
+                Thread owner = acquiredProjectLock.getOwner();
+                MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : 
null;
+                String str = ownerMojo != null ? " The " + ownerMojo.getId() : 
"A";
+                String msg = str + " mojo is already being executed "
+                        + "on the project " + 
session.getCurrentProject().getGroupId()
+                        + ":" + session.getCurrentProject().getArtifactId() + 
". "
+                        + "This mojo execution will be blocked "
+                        + "until the mojo is done.";
+                warn(msg);
+                acquiredProjectLock.lock();
             }
         }
 
         @Override
         public void close() {
             // release the lock in the reverse order of the acquisition
-            if (acquiredProjectLock != null) {
-                acquiredProjectLock.unlock();
-            }
-            if (acquiredAggregatorLock != null) {
-                acquiredAggregatorLock.unlock();
-            }
+            acquiredProjectLock.unlock();
+            acquiredAggregatorLock.unlock();
             mojos.remove(Thread.currentThread());
         }
 
@@ -308,7 +306,7 @@ public class MojoExecutor {
 
         ensureDependenciesAreResolved(mojoDescriptor, session, 
dependencyContext);
 
-        try (ProjectLock lock = new ProjectLock(session, mojoDescriptor)) {
+        try (NoExceptionCloseable lock = getProjectLock(session, 
mojoDescriptor)) {
             doExecute2(session, mojoExecution);
         } finally {
             for (MavenProject forkedProject : forkedProjects) {
@@ -317,6 +315,23 @@ public class MojoExecutor {
         }
     }
 
+    protected interface NoExceptionCloseable extends AutoCloseable {
+        @Override
+        void close();
+    }
+
+    protected NoExceptionCloseable getProjectLock(MavenSession session, 
MojoDescriptor mojoDescriptor) {
+        if (useProjectLock(session)) {
+            return new ProjectLock(session, mojoDescriptor);
+        } else {
+            return new NoLock();
+        }
+    }
+
+    protected boolean useProjectLock(MavenSession session) {
+        return session.getRequest().getDegreeOfConcurrency() > 1;
+    }
+
     private void doExecute2(MavenSession session, MojoExecution mojoExecution) 
throws LifecycleExecutionException {
         eventCatapult.fire(ExecutionEvent.Type.MojoStarted, session, 
mojoExecution);
         try {
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java
new file mode 100644
index 0000000000..222a1ecd77
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.project.MavenProject;
+
+public class BuildPlan {
+
+    private final Map<MavenProject, Map<String, BuildStep>> plan = new 
LinkedHashMap<>();
+    private final Map<MavenProject, List<MavenProject>> projects;
+    private final Map<String, String> aliases = new HashMap<>();
+    private volatile Set<String> duplicateIds;
+    private volatile List<BuildStep> sortedNodes;
+
+    BuildPlan() {
+        this.projects = null;
+    }
+
+    public BuildPlan(Map<MavenProject, List<MavenProject>> projects) {
+        this.projects = projects;
+    }
+
+    public Map<MavenProject, List<MavenProject>> getAllProjects() {
+        return projects;
+    }
+
+    public Map<String, String> aliases() {
+        return aliases;
+    }
+
+    public Stream<MavenProject> projects() {
+        return plan.keySet().stream();
+    }
+
+    public void addProject(MavenProject project, Map<String, BuildStep> steps) 
{
+        plan.put(project, steps);
+    }
+
+    public void addStep(MavenProject project, String name, BuildStep step) {
+        plan.get(project).put(name, step);
+    }
+
+    public Stream<BuildStep> allSteps() {
+        return plan.values().stream().flatMap(m -> m.values().stream());
+    }
+
+    public Stream<BuildStep> steps(MavenProject project) {
+        return Optional.ofNullable(plan.get(project))
+                .map(m -> m.values().stream())
+                .orElse(Stream.empty());
+    }
+
+    public Optional<BuildStep> step(MavenProject project, String name) {
+        return Optional.ofNullable(plan.get(project)).map(m -> m.get(name));
+    }
+
+    public BuildStep requiredStep(MavenProject project, String name) {
+        return step(project, name).get();
+    }
+
+    // add a follow-up plan to this one
+    public void then(BuildPlan step) {
+        step.plan.forEach((k, v) -> plan.merge(k, v, this::merge));
+        aliases.putAll(step.aliases);
+    }
+
+    private Map<String, BuildStep> merge(Map<String, BuildStep> org, 
Map<String, BuildStep> add) {
+        // all new phases should be added after the existing ones
+        List<BuildStep> lasts =
+                org.values().stream().filter(b -> 
b.successors.isEmpty()).collect(Collectors.toList());
+        List<BuildStep> firsts =
+                add.values().stream().filter(b -> 
b.predecessors.isEmpty()).collect(Collectors.toList());
+        firsts.stream()
+                .filter(addNode -> !org.containsKey(addNode.name))
+                .forEach(addNode -> lasts.forEach(orgNode -> 
addNode.executeAfter(orgNode)));
+        add.forEach((name, node) -> org.merge(name, node, this::merge));
+        return org;
+    }
+
+    private BuildStep merge(BuildStep node1, BuildStep node2) {
+        node1.predecessors.addAll(node2.predecessors);
+        node1.successors.addAll(node2.successors);
+        node2.mojos.forEach((k, v) -> node1.mojos.merge(k, v, 
this::mergeMojos));
+        return node1;
+    }
+
+    private Map<String, MojoExecution> mergeMojos(Map<String, MojoExecution> 
l1, Map<String, MojoExecution> l2) {
+        l2.forEach(l1::putIfAbsent);
+        return l1;
+    }
+
+    // gather artifactIds which are not unique so that the respective thread 
names can be extended with the groupId
+    public Set<String> duplicateIds() {
+        if (duplicateIds == null) {
+            synchronized (this) {
+                if (duplicateIds == null) {
+                    duplicateIds = projects()
+                            .map(MavenProject::getArtifactId)
+                            
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
+                            .entrySet()
+                            .stream()
+                            .filter(p -> p.getValue() > 1)
+                            .map(Map.Entry::getKey)
+                            .collect(Collectors.toSet());
+                }
+            }
+        }
+        return duplicateIds;
+    }
+
+    public List<BuildStep> sortedNodes() {
+        if (sortedNodes == null) {
+            synchronized (this) {
+                if (sortedNodes == null) {
+                    List<BuildStep> sortedNodes = new ArrayList<>();
+                    Set<BuildStep> visited = new HashSet<>();
+                    // Visit each unvisited node
+                    allSteps().forEach(node -> visitNode(node, visited, 
sortedNodes));
+                    // Reverse the sorted nodes to get the correct order
+                    Collections.reverse(sortedNodes);
+                    this.sortedNodes = sortedNodes;
+                }
+            }
+        }
+        return sortedNodes;
+    }
+
+    // Helper method to visit a node
+    private static void visitNode(BuildStep node, Set<BuildStep> visited, 
List<BuildStep> sortedNodes) {
+        if (visited.add(node)) {
+            // For each successor of the current node, visit unvisited 
successors
+            node.successors.forEach(successor -> visitNode(successor, visited, 
sortedNodes));
+            sortedNodes.add(node);
+        }
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java
new file mode 100644
index 0000000000..6f77b92781
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java
@@ -0,0 +1,939 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.xml.stream.XMLStreamException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.api.Lifecycle;
+import org.apache.maven.api.services.LifecycleRegistry;
+import org.apache.maven.api.services.MavenException;
+import org.apache.maven.api.xml.XmlNode;
+import org.apache.maven.execution.BuildFailure;
+import org.apache.maven.execution.BuildSuccess;
+import org.apache.maven.execution.ExecutionEvent;
+import org.apache.maven.execution.MavenExecutionRequest;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.execution.ProjectDependencyGraph;
+import org.apache.maven.execution.ProjectExecutionEvent;
+import org.apache.maven.execution.ProjectExecutionListener;
+import org.apache.maven.internal.MultilineMessageHelper;
+import org.apache.maven.internal.transformation.ConsumerPomArtifactTransformer;
+import org.apache.maven.internal.xml.XmlNodeImpl;
+import org.apache.maven.lifecycle.LifecycleExecutionException;
+import org.apache.maven.lifecycle.LifecycleNotFoundException;
+import org.apache.maven.lifecycle.LifecyclePhaseNotFoundException;
+import org.apache.maven.lifecycle.MojoExecutionConfigurator;
+import org.apache.maven.lifecycle.internal.BuildThreadFactory;
+import org.apache.maven.lifecycle.internal.CompoundProjectExecutionListener;
+import org.apache.maven.lifecycle.internal.DefaultLifecyclePluginAnalyzer;
+import org.apache.maven.lifecycle.internal.ExecutionEventCatapult;
+import org.apache.maven.lifecycle.internal.GoalTask;
+import org.apache.maven.lifecycle.internal.LifecycleTask;
+import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
+import org.apache.maven.lifecycle.internal.MojoExecutor;
+import org.apache.maven.lifecycle.internal.ReactorContext;
+import org.apache.maven.lifecycle.internal.Task;
+import org.apache.maven.lifecycle.internal.TaskSegment;
+import org.apache.maven.model.Plugin;
+import org.apache.maven.model.PluginExecution;
+import org.apache.maven.plugin.MavenPluginManager;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.MojoNotFoundException;
+import org.apache.maven.plugin.PluginDescriptorParsingException;
+import org.apache.maven.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.plugin.descriptor.Parameter;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.maven.api.Lifecycle.AFTER;
+import static org.apache.maven.api.Lifecycle.AT;
+import static org.apache.maven.api.Lifecycle.BEFORE;
+import static org.apache.maven.api.Lifecycle.Phase.PACKAGE;
+import static org.apache.maven.api.Lifecycle.Phase.READY;
+import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.CREATED;
+import static 
org.apache.maven.lifecycle.internal.concurrent.BuildStep.EXECUTED;
+import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.FAILED;
+import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.PLAN;
+import static 
org.apache.maven.lifecycle.internal.concurrent.BuildStep.PLANNING;
+import static 
org.apache.maven.lifecycle.internal.concurrent.BuildStep.SCHEDULED;
+import static org.apache.maven.lifecycle.internal.concurrent.BuildStep.SETUP;
+import static 
org.apache.maven.lifecycle.internal.concurrent.BuildStep.TEARDOWN;
+
+/**
+ * Builds the full lifecycle in weave-mode (phase by phase as opposed to 
project-by-project).
+ * <p>
+ * This builder uses a number of threads equal to the minimum of the degree of 
concurrency (which is the thread count
+ * set with <code>-T</code> on the command-line) and the number of projects to 
build. As such, building a single project
+ * will always result in a sequential build, regardless of the thread count.
+ * </p>
+ * <strong>NOTE:</strong> This class is not part of any public api and can be 
changed or deleted without prior notice.
+ *
+ * @since 3.0
+ *         Builds one or more lifecycles for a full module
+ *         NOTE: This class is not part of any public api and can be changed 
or deleted without prior notice.
+ */
+@Named
+public class BuildPlanExecutor {
+
+    private static final Object GLOBAL = new Object();
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private final MojoExecutor mojoExecutor;
+    private final ExecutionEventCatapult eventCatapult;
+    private final ProjectExecutionListener projectExecutionListener;
+    private final ConsumerPomArtifactTransformer 
consumerPomArtifactTransformer;
+    private final BuildPlanLogger buildPlanLogger;
+    private final Map<String, MojoExecutionConfigurator> 
mojoExecutionConfigurators;
+    private final MavenPluginManager mavenPluginManager;
+    private final MojoDescriptorCreator mojoDescriptorCreator;
+    private final LifecycleRegistry lifecycles;
+
+    @Inject
+    @SuppressWarnings("checkstyle:ParameterNumber")
+    public BuildPlanExecutor(
+            @Named("concurrent") MojoExecutor mojoExecutor,
+            ExecutionEventCatapult eventCatapult,
+            List<ProjectExecutionListener> listeners,
+            ConsumerPomArtifactTransformer consumerPomArtifactTransformer,
+            BuildPlanLogger buildPlanLogger,
+            Map<String, MojoExecutionConfigurator> mojoExecutionConfigurators,
+            MavenPluginManager mavenPluginManager,
+            MojoDescriptorCreator mojoDescriptorCreator,
+            LifecycleRegistry lifecycles) {
+        this.mojoExecutor = mojoExecutor;
+        this.eventCatapult = eventCatapult;
+        this.projectExecutionListener = new 
CompoundProjectExecutionListener(listeners);
+        this.consumerPomArtifactTransformer = consumerPomArtifactTransformer;
+        this.buildPlanLogger = buildPlanLogger;
+        this.mojoExecutionConfigurators = mojoExecutionConfigurators;
+        this.mavenPluginManager = mavenPluginManager;
+        this.mojoDescriptorCreator = mojoDescriptorCreator;
+        this.lifecycles = lifecycles;
+    }
+
+    public void execute(MavenSession session, ReactorContext reactorContext, 
List<TaskSegment> taskSegments)
+            throws ExecutionException, InterruptedException {
+        try (BuildContext ctx = new BuildContext(session, reactorContext, 
taskSegments)) {
+            ctx.execute();
+        }
+    }
+
+    class BuildContext implements AutoCloseable {
+        final MavenSession session;
+        final ReactorContext reactorContext;
+        final PhasingExecutor executor;
+        final ConcurrentLogOutput appender;
+        final Map<Object, Clock> clocks = new ConcurrentHashMap<>();
+        final ReadWriteLock lock = new ReentrantReadWriteLock();
+        final int threads;
+        BuildPlan plan;
+
+        BuildContext(MavenSession session, ReactorContext reactorContext, 
List<TaskSegment> taskSegments) {
+            this.session = session;
+            this.reactorContext = reactorContext;
+            this.threads = Math.min(
+                    session.getRequest().getDegreeOfConcurrency(),
+                    session.getProjects().size());
+            // Propagate the parallel flag to the root session
+            session.setParallel(threads > 1);
+            this.executor = new 
PhasingExecutor(Executors.newFixedThreadPool(threads, new 
BuildThreadFactory()));
+            this.appender = new ConcurrentLogOutput();
+
+            // build initial plan
+            this.plan = buildInitialPlan(taskSegments);
+        }
+
+        BuildContext() {
+            this.session = null;
+            this.reactorContext = null;
+            this.threads = 1;
+            this.executor = null;
+            this.appender = null;
+            this.plan = null;
+        }
+
+        public BuildPlan buildInitialPlan(List<TaskSegment> taskSegments) {
+            int nThreads = Math.min(
+                    session.getRequest().getDegreeOfConcurrency(),
+                    session.getProjects().size());
+            boolean parallel = nThreads > 1;
+            // Propagate the parallel flag to the root session
+            session.setParallel(parallel);
+
+            ProjectDependencyGraph dependencyGraph = 
session.getProjectDependencyGraph();
+            MavenProject rootProject = session.getTopLevelProject();
+
+            Map<MavenProject, List<MavenProject>> allProjects = new 
LinkedHashMap<>();
+            dependencyGraph
+                    .getSortedProjects()
+                    .forEach(p -> allProjects.put(p, 
dependencyGraph.getUpstreamProjects(p, false)));
+
+            BuildPlan plan = new BuildPlan(allProjects);
+            for (TaskSegment taskSegment : taskSegments) {
+                Map<MavenProject, List<MavenProject>> projects = 
taskSegment.isAggregating()
+                        ? Collections.singletonMap(rootProject, 
allProjects.get(rootProject))
+                        : allProjects;
+
+                BuildPlan segment = calculateMojoExecutions(projects, 
taskSegment.getTasks());
+                plan.then(segment);
+            }
+
+            // Create plan, setup and teardown
+            for (MavenProject project : plan.getAllProjects().keySet()) {
+                BuildStep pplan = new BuildStep(PLAN, project, null);
+                pplan.status.set(PLANNING); // the plan step always need 
planning
+                BuildStep setup = new BuildStep(SETUP, project, null);
+                BuildStep teardown = new BuildStep(TEARDOWN, project, null);
+                setup.executeAfter(pplan);
+                plan.steps(project).forEach(step -> {
+                    if (step.predecessors.isEmpty()) {
+                        step.executeAfter(setup);
+                    } else if (step.successors.isEmpty()) {
+                        teardown.executeAfter(step);
+                    }
+                });
+                Stream.of(pplan, setup, teardown).forEach(step -> 
plan.addStep(project, step.name, step));
+            }
+
+            return plan;
+        }
+
+        private void checkUnboundVersions(BuildPlan buildPlan) {
+            String defaulModelId = 
DefaultLifecyclePluginAnalyzer.DEFAULTLIFECYCLEBINDINGS_MODELID;
+            List<String> unversionedPlugins = buildPlan
+                    .allSteps()
+                    .flatMap(step -> step.mojos.values().stream().flatMap(map 
-> map.values().stream()))
+                    .map(MojoExecution::getPlugin)
+                    .filter(p -> p.getLocation("version") != null
+                            && p.getLocation("version").getSource() != null
+                            && defaulModelId.equals(
+                                    
p.getLocation("version").getSource().getModelId()))
+                    .distinct()
+                    .map(Plugin::getArtifactId) // managed by us, groupId is 
always o.a.m.plugins
+                    .toList();
+            if (!unversionedPlugins.isEmpty()) {
+                logger.warn("Version not locked for default bindings plugins " 
+ unversionedPlugins
+                        + ", you should define versions in pluginManagement 
section of your " + "pom.xml or parent");
+            }
+        }
+
+        private void checkThreadSafety(BuildPlan buildPlan) {
+            if (threads > 1) {
+                Set<MojoExecution> unsafeExecutions = buildPlan
+                        .allSteps()
+                        .flatMap(step -> 
step.mojos.values().stream().flatMap(map -> map.values().stream()))
+                        .filter(execution -> 
!execution.getMojoDescriptor().isV4Api())
+                        .collect(Collectors.toSet());
+                if (!unsafeExecutions.isEmpty()) {
+                    for (String s : MultilineMessageHelper.format(
+                            """
+                                Your build is requesting concurrent execution, 
but this project contains the \
+                                following plugin(s) that have goals not built 
with Maven 4 to support concurrent \
+                                execution. While this /may/ work fine, please 
look for plugin updates and/or \
+                                request plugins be made thread-safe. If 
reporting an issue, report it against the \
+                                plugin in question, not against Apache 
Maven.""")) {
+                        logger.warn(s);
+                    }
+                    if (logger.isDebugEnabled()) {
+                        Set<MojoDescriptor> unsafeGoals = 
unsafeExecutions.stream()
+                                .map(MojoExecution::getMojoDescriptor)
+                                .collect(Collectors.toSet());
+                        logger.warn("The following goals are not Maven 4 
goals:");
+                        for (MojoDescriptor unsafeGoal : unsafeGoals) {
+                            logger.warn("  " + unsafeGoal.getId());
+                        }
+                    } else {
+                        Set<Plugin> unsafePlugins = unsafeExecutions.stream()
+                                .map(MojoExecution::getPlugin)
+                                .collect(Collectors.toSet());
+                        logger.warn("The following plugins are not Maven 4 
plugins:");
+                        for (Plugin unsafePlugin : unsafePlugins) {
+                            logger.warn("  " + unsafePlugin.getId());
+                        }
+                        logger.warn("");
+                        logger.warn("Enable verbose output (-X) to see 
precisely which goals are not marked as"
+                                + " thread-safe.");
+                    }
+                    logger.warn(MultilineMessageHelper.separatorLine());
+                }
+            }
+        }
+
+        void execute() {
+            try {
+                plan();
+                executePlan();
+                executor.await();
+            } catch (Exception e) {
+                session.getResult().addException(e);
+            }
+        }
+
+        @Override
+        public void close() {
+            this.appender.close();
+            this.executor.close();
+        }
+
+        private void executePlan() {
+            if (reactorContext.getReactorBuildStatus().isHalted()) {
+                return;
+            }
+            Clock global = clocks.computeIfAbsent(GLOBAL, p -> new Clock());
+            global.start();
+            lock.readLock().lock();
+            try {
+                plan.sortedNodes().stream()
+                        .filter(step -> step.status.get() == CREATED)
+                        .filter(step -> step.predecessors.stream().allMatch(s 
-> s.status.get() == EXECUTED))
+                        .filter(step -> step.status.compareAndSet(CREATED, 
SCHEDULED))
+                        .forEach(step -> {
+                            boolean nextIsPlanning = 
step.successors.stream().anyMatch(st -> PLAN.equals(st.name));
+                            executor.execute(() -> {
+                                try (AutoCloseable ctx = 
appender.build(step.project)) {
+                                    executeStep(step);
+                                    if (nextIsPlanning) {
+                                        lock.writeLock().lock();
+                                        try {
+                                            plan();
+                                        } finally {
+                                            lock.writeLock().unlock();
+                                        }
+                                    }
+                                    executePlan();
+                                } catch (Exception e) {
+                                    step.status.compareAndSet(SCHEDULED, 
FAILED);
+                                    global.stop();
+                                    handleBuildError(reactorContext, session, 
step.project, e, global);
+                                }
+                            });
+                        });
+            } finally {
+                lock.readLock().unlock();
+            }
+        }
+
+        private void executeStep(BuildStep step) throws IOException, 
LifecycleExecutionException {
+            Clock clock = getClock(step.project);
+            switch (step.name) {
+                case PLAN:
+                    // Planning steps should be executed out of normal 
execution
+                    throw new IllegalStateException();
+                case SETUP:
+                    consumerPomArtifactTransformer.injectTransformedArtifacts(
+                            session.getRepositorySession(), step.project);
+                    projectExecutionListener.beforeProjectExecution(new 
ProjectExecutionEvent(session, step.project));
+                    eventCatapult.fire(ExecutionEvent.Type.ProjectStarted, 
session, null);
+                    break;
+                case TEARDOWN:
+                    projectExecutionListener.afterProjectExecutionSuccess(
+                            new ProjectExecutionEvent(session, step.project, 
Collections.emptyList()));
+                    reactorContext
+                            .getResult()
+                            .addBuildSummary(new BuildSuccess(step.project, 
clock.wallTime(), clock.execTime()));
+                    eventCatapult.fire(ExecutionEvent.Type.ProjectSucceeded, 
session, null);
+                    break;
+                default:
+                    List<MojoExecution> executions = 
step.executions().collect(Collectors.toList());
+                    if (!executions.isEmpty()) {
+                        attachToThread(step.project);
+                        session.setCurrentProject(step.project);
+                        clock.start();
+                        executions.forEach(mojoExecution -> {
+                            
mojoExecutionConfigurator(mojoExecution).configure(step.project, mojoExecution, 
true);
+                            finalizeMojoConfiguration(mojoExecution);
+                        });
+                        mojoExecutor.execute(session, executions);
+                        clock.stop();
+                    }
+                    break;
+            }
+            step.status.compareAndSet(SCHEDULED, EXECUTED);
+        }
+
+        private Clock getClock(Object key) {
+            return clocks.computeIfAbsent(key, p -> new Clock());
+        }
+
+        private void plan() {
+            lock.writeLock().lock();
+            try {
+                Set<BuildStep> planSteps = plan.allSteps()
+                        .filter(st -> PLAN.equals(st.name))
+                        .filter(step -> step.predecessors.stream().allMatch(s 
-> s.status.get() == EXECUTED))
+                        .filter(step -> step.status.compareAndSet(PLANNING, 
SCHEDULED))
+                        .collect(Collectors.toSet());
+                for (BuildStep step : planSteps) {
+                    MavenProject project = step.project;
+                    for (Plugin plugin : project.getBuild().getPlugins()) {
+                        for (PluginExecution execution : 
plugin.getExecutions()) {
+                            for (String goal : execution.getGoals()) {
+                                MojoDescriptor mojoDescriptor = 
getMojoDescriptor(project, plugin, goal);
+                                String phase =
+                                        execution.getPhase() != null ? 
execution.getPhase() : mojoDescriptor.getPhase();
+                                String tmpResolvedPhase = 
plan.aliases().getOrDefault(phase, phase);
+                                String resolvedPhase = 
tmpResolvedPhase.startsWith(AT)
+                                        ? 
tmpResolvedPhase.substring(AT.length())
+                                        : tmpResolvedPhase;
+                                plan.step(project, resolvedPhase).ifPresent(n 
-> {
+                                    MojoExecution mojoExecution = new 
MojoExecution(mojoDescriptor, execution.getId());
+                                    mojoExecution.setLifecyclePhase(phase);
+                                    n.addMojo(mojoExecution, 
execution.getPriority());
+                                    if 
(mojoDescriptor.getDependencyCollectionRequired() != null
+                                            || 
mojoDescriptor.getDependencyResolutionRequired() != null) {
+                                        for (MavenProject p :
+                                                
plan.getAllProjects().get(project)) {
+                                            plan.step(p, AFTER + PACKAGE)
+                                                    .ifPresent(a -> 
plan.requiredStep(project, resolvedPhase)
+                                                            .executeAfter(a));
+                                        }
+                                    }
+                                });
+                            }
+                        }
+                    }
+                }
+
+                BuildPlan buildPlan = plan;
+                for (BuildStep step :
+                        planSteps.stream().flatMap(p -> 
plan.steps(p.project)).toList()) {
+                    for (MojoExecution execution : step.executions().toList()) 
{
+                        buildPlan = computeForkPlan(step, execution, 
buildPlan);
+                    }
+                }
+
+                for (BuildStep step : planSteps) {
+                    MavenProject project = step.project;
+                    buildPlanLogger.writePlan(plan, project);
+                    step.status.compareAndSet(SCHEDULED, EXECUTED);
+                }
+
+                checkThreadSafety(plan);
+                checkUnboundVersions(plan);
+            } finally {
+                lock.writeLock().unlock();
+            }
+        }
+
+        protected BuildPlan computeForkPlan(BuildStep step, MojoExecution 
execution, BuildPlan buildPlan) {
+            MojoDescriptor mojoDescriptor = execution.getMojoDescriptor();
+            PluginDescriptor pluginDescriptor = 
mojoDescriptor.getPluginDescriptor();
+            String forkedGoal = mojoDescriptor.getExecuteGoal();
+            String phase = mojoDescriptor.getExecutePhase();
+            // We have a fork goal
+            if (forkedGoal != null && !forkedGoal.isEmpty()) {
+                MojoDescriptor forkedMojoDescriptor = 
pluginDescriptor.getMojo(forkedGoal);
+                if (forkedMojoDescriptor == null) {
+                    throw new MavenException(new 
MojoNotFoundException(forkedGoal, pluginDescriptor));
+                }
+
+                List<MavenProject> toFork = new ArrayList<>();
+                toFork.add(step.project);
+                if (mojoDescriptor.isAggregator() && 
step.project.getCollectedProjects() != null) {
+                    toFork.addAll(step.project.getCollectedProjects());
+                }
+
+                BuildPlan plan = new BuildPlan();
+                for (MavenProject project : toFork) {
+                    BuildStep st = new BuildStep(forkedGoal, project, null);
+                    MojoExecution mojoExecution = new 
MojoExecution(forkedMojoDescriptor, forkedGoal);
+                    st.addMojo(mojoExecution, 0);
+                    Map<String, BuildStep> n = new HashMap<>();
+                    n.put(forkedGoal, st);
+                    plan.addProject(project, n);
+                }
+
+                for (BuildStep astep : plan.allSteps().toList()) {
+                    for (MojoExecution aexecution : 
astep.executions().toList()) {
+                        plan = computeForkPlan(astep, aexecution, plan);
+                    }
+                }
+
+                return plan;
+
+            } else if (phase != null && !phase.isEmpty()) {
+                String forkedLifecycle = mojoDescriptor.getExecuteLifecycle();
+                Lifecycle lifecycle;
+                if (forkedLifecycle != null && !forkedLifecycle.isEmpty()) {
+                    org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle 
lifecycleOverlay;
+                    try {
+                        lifecycleOverlay = 
pluginDescriptor.getLifecycleMapping(forkedLifecycle);
+                    } catch (IOException | XMLStreamException e) {
+                        throw new MavenException(new 
PluginDescriptorParsingException(
+                                pluginDescriptor.getPlugin(), 
pluginDescriptor.getSource(), e));
+                    }
+                    if (lifecycleOverlay == null) {
+                        Optional<Lifecycle> lf = 
lifecycles.lookup(forkedLifecycle);
+                        if (lf.isPresent()) {
+                            lifecycle = lf.get();
+                        } else {
+                            throw new MavenException(new 
LifecycleNotFoundException(forkedLifecycle));
+                        }
+                    } else {
+                        lifecycle = new PluginLifecycle(lifecycleOverlay, 
pluginDescriptor);
+                    }
+                } else {
+                    if (execution.getLifecyclePhase() != null) {
+                        String n = execution.getLifecyclePhase();
+                        String phaseName = n.startsWith(BEFORE)
+                                ? n.substring(BEFORE.length())
+                                : n.startsWith(AFTER) ? 
n.substring(AFTER.length()) : n;
+                        lifecycle = lifecycles.stream()
+                                .filter(l -> l.allPhases().anyMatch(p -> 
phaseName.equals(p.name())))
+                                .findFirst()
+                                .orElse(null);
+                        if (lifecycle == null) {
+                            throw new IllegalStateException();
+                        }
+                    } else {
+                        lifecycle = lifecycles.require(Lifecycle.DEFAULT);
+                    }
+                }
+
+                String resolvedPhase = getResolvedPhase(lifecycle, phase);
+
+                Map<MavenProject, List<MavenProject>> map = 
Collections.singletonMap(
+                        step.project, plan.getAllProjects().get(step.project));
+                BuildPlan forkedPlan = calculateLifecycleMappings(map, 
lifecycle, resolvedPhase);
+                forkedPlan.then(buildPlan);
+                return forkedPlan;
+            } else {
+                return buildPlan;
+            }
+        }
+
+        private String getResolvedPhase(Lifecycle lifecycle, String phase) {
+            return lifecycle.aliases().stream()
+                    .filter(a -> phase.equals(a.v3Phase()))
+                    .findFirst()
+                    .map(Lifecycle.Alias::v4Phase)
+                    .orElse(phase);
+        }
+
+        private String getResolvedPhase(String phase) {
+            return lifecycles.stream()
+                    .flatMap(l -> l.aliases().stream())
+                    .filter(a -> phase.equals(a.v3Phase()))
+                    .findFirst()
+                    .map(Lifecycle.Alias::v4Phase)
+                    .orElse(phase);
+        }
+
+        protected void handleBuildError(
+                final ReactorContext buildContext,
+                final MavenSession session,
+                final MavenProject mavenProject,
+                Throwable t,
+                final Clock clock) {
+            // record the error and mark the project as failed
+            buildContext.getResult().addException(t);
+            buildContext
+                    .getResult()
+                    .addBuildSummary(new BuildFailure(mavenProject, 
clock.execTime(), clock.wallTime(), t));
+
+            // notify listeners about "soft" project build failures only
+            if (t instanceof Exception && !(t instanceof RuntimeException)) {
+                eventCatapult.fire(ExecutionEvent.Type.ProjectFailed, session, 
null, (Exception) t);
+            }
+
+            // reactor failure modes
+            if (t instanceof RuntimeException || !(t instanceof Exception)) {
+                // fail fast on RuntimeExceptions, Errors and "other" 
Throwables
+                // assume these are system errors and further build is 
meaningless
+                buildContext.getReactorBuildStatus().halt();
+            } else if 
(MavenExecutionRequest.REACTOR_FAIL_NEVER.equals(session.getReactorFailureBehavior()))
 {
+                // continue the build
+            } else if 
(MavenExecutionRequest.REACTOR_FAIL_AT_END.equals(session.getReactorFailureBehavior()))
 {
+                // continue the build but ban all projects that depend on the 
failed one
+                buildContext.getReactorBuildStatus().blackList(mavenProject);
+            } else if 
(MavenExecutionRequest.REACTOR_FAIL_FAST.equals(session.getReactorFailureBehavior()))
 {
+                buildContext.getReactorBuildStatus().halt();
+            } else {
+                logger.error("invalid reactor failure behavior " + 
session.getReactorFailureBehavior());
+                buildContext.getReactorBuildStatus().halt();
+            }
+        }
+
+        public BuildPlan calculateMojoExecutions(Map<MavenProject, 
List<MavenProject>> projects, List<Task> tasks) {
+            BuildPlan buildPlan = new BuildPlan(projects);
+
+            for (Task task : tasks) {
+                BuildPlan step;
+
+                if (task instanceof GoalTask) {
+                    String pluginGoal = task.getValue();
+
+                    String executionId = "default-cli";
+                    int executionIdx = pluginGoal.indexOf('@');
+                    if (executionIdx > 0) {
+                        executionId = pluginGoal.substring(executionIdx + 1);
+                    }
+
+                    step = new BuildPlan();
+                    for (MavenProject project : projects.keySet()) {
+                        BuildStep st = new BuildStep(pluginGoal, project, 
null);
+                        MojoDescriptor mojoDescriptor = 
getMojoDescriptor(project, pluginGoal);
+                        MojoExecution mojoExecution =
+                                new MojoExecution(mojoDescriptor, executionId, 
MojoExecution.Source.CLI);
+                        st.addMojo(mojoExecution, 0);
+                        Map<String, BuildStep> n = new HashMap<>();
+                        n.put(pluginGoal, st);
+                        step.addProject(project, n);
+                    }
+                } else if (task instanceof LifecycleTask) {
+                    String lifecyclePhase = task.getValue();
+
+                    step = calculateLifecycleMappings(projects, 
lifecyclePhase);
+
+                } else {
+                    throw new IllegalStateException("unexpected task " + task);
+                }
+
+                buildPlan.then(step);
+            }
+
+            return buildPlan;
+        }
+
+        private MojoDescriptor getMojoDescriptor(MavenProject project, Plugin 
plugin, String goal) {
+            try {
+                return mavenPluginManager.getMojoDescriptor(
+                        plugin, goal, project.getRemotePluginRepositories(), 
session.getRepositorySession());
+            } catch (MavenException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new MavenException(e);
+            }
+        }
+
+        private MojoDescriptor getMojoDescriptor(MavenProject project, String 
task) {
+            try {
+                return mojoDescriptorCreator.getMojoDescriptor(task, session, 
project);
+            } catch (MavenException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new MavenException(e);
+            }
+        }
+
+        public BuildPlan calculateLifecycleMappings(
+                Map<MavenProject, List<MavenProject>> projects, String 
lifecyclePhase) {
+
+            String resolvedPhase = getResolvedPhase(lifecyclePhase);
+            String mainPhase = resolvedPhase.startsWith(BEFORE)
+                    ? resolvedPhase.substring(BEFORE.length())
+                    : resolvedPhase.startsWith(AFTER)
+                            ? resolvedPhase.substring(AFTER.length())
+                            : resolvedPhase.startsWith(AT) ? 
resolvedPhase.substring(AT.length()) : resolvedPhase;
+
+            /*
+             * Determine the lifecycle that corresponds to the given phase.
+             */
+            Lifecycle lifecycle = lifecycles.stream()
+                    .filter(l -> l.allPhases().anyMatch(p -> 
mainPhase.equals(p.name())))
+                    .findFirst()
+                    .orElse(null);
+
+            if (lifecycle == null) {
+                throw new MavenException(new LifecyclePhaseNotFoundException(
+                        "Unknown lifecycle phase \"" + lifecyclePhase
+                                + "\". You must specify a valid lifecycle 
phase"
+                                + " or a goal in the format 
<plugin-prefix>:<goal> or"
+                                + " 
<plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available 
lifecycle phases are: "
+                                + lifecycles.stream()
+                                        .flatMap(l -> l.orderedPhases()
+                                                .map(List::stream)
+                                                .orElseGet(() -> 
l.allPhases().map(Lifecycle.Phase::name)))
+                                        .collect(Collectors.joining(", "))
+                                + ".",
+                        lifecyclePhase));
+            }
+
+            return calculateLifecycleMappings(projects, lifecycle, 
resolvedPhase);
+        }
+
+        public BuildPlan calculateLifecycleMappings(
+                Map<MavenProject, List<MavenProject>> projects, Lifecycle 
lifecycle, String lifecyclePhase) {
+            BuildPlan plan = new BuildPlan(projects);
+
+            for (MavenProject project : projects.keySet()) {
+                // For each phase, create and sequence the pre, run and post 
steps
+                Map<String, BuildStep> steps = lifecycle
+                        .allPhases()
+                        .flatMap(phase -> {
+                            BuildStep a = new BuildStep(BEFORE + phase.name(), 
project, phase);
+                            BuildStep b = new BuildStep(phase.name(), project, 
phase);
+                            BuildStep c = new BuildStep(AFTER + phase.name(), 
project, phase);
+                            b.executeAfter(a);
+                            c.executeAfter(b);
+                            return Stream.of(a, b, c);
+                        })
+                        .collect(Collectors.toMap(n -> n.name, n -> n));
+                // for each phase, make sure children phases are execute 
between pre and post steps
+                lifecycle.allPhases().forEach(phase -> 
phase.phases().forEach(child -> {
+                    steps.get(BEFORE + 
child.name()).executeAfter(steps.get(BEFORE + phase.name()));
+                    steps.get(AFTER + 
phase.name()).executeAfter(steps.get(AFTER + child.name()));
+                }));
+                // for each phase, create links between this project phases
+                lifecycle.allPhases().forEach(phase -> {
+                    phase.links().stream()
+                            .filter(l -> l.pointer().type() == 
Lifecycle.Pointer.Type.PROJECT)
+                            .forEach(link -> {
+                                String n1 = phase.name();
+                                String n2 = link.pointer().phase();
+                                if (link.kind() == Lifecycle.Link.Kind.AFTER) {
+                                    steps.get(BEFORE + 
n1).executeAfter(steps.get(AFTER + n2));
+                                } else {
+                                    steps.get(BEFORE + 
n2).executeAfter(steps.get(AFTER + n1));
+                                }
+                            });
+                });
+
+                // Only keep mojo executions before the end phase
+                String endPhase = lifecyclePhase.startsWith(BEFORE) || 
lifecyclePhase.startsWith(AFTER)
+                        ? lifecyclePhase
+                        : lifecyclePhase.startsWith(AT)
+                                ? lifecyclePhase.substring(AT.length())
+                                : AFTER + lifecyclePhase;
+                Set<BuildStep> toKeep = 
steps.get(endPhase).allPredecessors().collect(Collectors.toSet());
+                steps.values().stream().filter(n -> 
!toKeep.contains(n)).forEach(BuildStep::skip);
+
+                plan.addProject(project, steps);
+            }
+
+            // Create inter project dependencies
+            plan.allSteps().filter(step -> step.phase != null).forEach(step -> 
{
+                Lifecycle.Phase phase = step.phase;
+                MavenProject project = step.project;
+                phase.links().stream()
+                        .filter(l -> l.pointer().type() != 
Lifecycle.Pointer.Type.PROJECT)
+                        .forEach(link -> {
+                            String n1 = phase.name();
+                            String n2 = link.pointer().phase();
+                            // for each project, if the phase in the build, 
link after it
+                            getLinkedProjects(projects, project, 
link).forEach(p -> plan.step(p, AFTER + n2)
+                                    .ifPresent(a -> plan.requiredStep(project, 
BEFORE + n1)
+                                            .executeAfter(a)));
+                        });
+            });
+
+            // Keep projects in reactors by GAV
+            Map<String, MavenProject> reactorGavs =
+                    
projects.keySet().stream().collect(Collectors.toMap(BuildPlanExecutor::gav, p 
-> p));
+
+            // Go through all plugins
+            List<Runnable> toResolve = new ArrayList<>();
+            projects.keySet().forEach(project -> 
project.getBuild().getPlugins().forEach(plugin -> {
+                MavenProject pluginProject = reactorGavs.get(gav(plugin));
+                if (pluginProject != null) {
+                    // In order to plan the project, we need all its plugins...
+                    plan.requiredStep(project, 
PLAN).executeAfter(plan.requiredStep(pluginProject, READY));
+                } else {
+                    toResolve.add(() -> resolvePlugin(session, 
project.getRemotePluginRepositories(), plugin));
+                }
+            }));
+
+            // Eagerly resolve all plugins in parallel
+            toResolve.parallelStream().forEach(Runnable::run);
+
+            // Keep track of phase aliases
+            lifecycle.aliases().forEach(alias -> 
plan.aliases().put(alias.v3Phase(), alias.v4Phase()));
+
+            return plan;
+        }
+
+        private List<MavenProject> getLinkedProjects(
+                Map<MavenProject, List<MavenProject>> projects, MavenProject 
project, Lifecycle.Link link) {
+            if (link.pointer().type() == Lifecycle.Pointer.Type.DEPENDENCIES) {
+                // TODO: String scope = ((Lifecycle.DependenciesPointer) 
link.pointer()).scope();
+                return projects.get(project);
+            } else if (link.pointer().type() == 
Lifecycle.Pointer.Type.CHILDREN) {
+                return project.getCollectedProjects();
+            } else {
+                throw new IllegalArgumentException(
+                        "Unsupported pointer type: " + link.pointer().type());
+            }
+        }
+    }
+
+    private void resolvePlugin(MavenSession session, List<RemoteRepository> 
repositories, Plugin plugin) {
+        try {
+            mavenPluginManager.getPluginDescriptor(plugin, repositories, 
session.getRepositorySession());
+        } catch (Exception e) {
+            throw new MavenException(e);
+        }
+    }
+
+    private static String gav(MavenProject p) {
+        return p.getGroupId() + ":" + p.getArtifactId() + ":" + p.getVersion();
+    }
+
+    private static String gav(Plugin p) {
+        return p.getGroupId() + ":" + p.getArtifactId() + ":" + p.getVersion();
+    }
+
+    /**
+     * Post-processes the effective configuration for the specified mojo 
execution. This step discards all parameters
+     * from the configuration that are not applicable to the mojo and injects 
the default values for any missing
+     * parameters.
+     *
+     * @param mojoExecution The mojo execution whose configuration should be 
finalized, must not be {@code null}.
+     */
+    private void finalizeMojoConfiguration(MojoExecution mojoExecution) {
+        MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
+
+        XmlNode executionConfiguration = mojoExecution.getConfiguration() != 
null
+                ? mojoExecution.getConfiguration().getDom()
+                : null;
+        if (executionConfiguration == null) {
+            executionConfiguration = new XmlNodeImpl("configuration");
+        }
+
+        XmlNode defaultConfiguration = getMojoConfiguration(mojoDescriptor);
+
+        List<XmlNode> children = new ArrayList<>();
+        if (mojoDescriptor.getParameters() != null) {
+            for (Parameter parameter : mojoDescriptor.getParameters()) {
+                XmlNode parameterConfiguration = 
executionConfiguration.getChild(parameter.getName());
+
+                if (parameterConfiguration == null) {
+                    parameterConfiguration = 
executionConfiguration.getChild(parameter.getAlias());
+                }
+
+                XmlNode parameterDefaults = 
defaultConfiguration.getChild(parameter.getName());
+
+                if (parameterConfiguration != null) {
+                    parameterConfiguration = 
parameterConfiguration.merge(parameterDefaults, Boolean.TRUE);
+                } else {
+                    parameterConfiguration = parameterDefaults;
+                }
+
+                if (parameterConfiguration != null) {
+                    Map<String, String> attributes = new 
HashMap<>(parameterConfiguration.getAttributes());
+
+                    String attributeForImplementation = 
parameterConfiguration.getAttribute("implementation");
+                    String parameterForImplementation = 
parameter.getImplementation();
+                    if ((attributeForImplementation == null || 
attributeForImplementation.isEmpty())
+                            && ((parameterForImplementation != null) && 
!parameterForImplementation.isEmpty())) {
+                        attributes.put("implementation", 
parameter.getImplementation());
+                    }
+
+                    parameterConfiguration = new XmlNodeImpl(
+                            parameter.getName(),
+                            parameterConfiguration.getValue(),
+                            attributes,
+                            parameterConfiguration.getChildren(),
+                            parameterConfiguration.getInputLocation());
+
+                    children.add(parameterConfiguration);
+                }
+            }
+        }
+        XmlNode finalConfiguration = new XmlNodeImpl("configuration", null, 
null, children, null);
+
+        mojoExecution.setConfiguration(finalConfiguration);
+    }
+
+    private XmlNode getMojoConfiguration(MojoDescriptor mojoDescriptor) {
+        if (mojoDescriptor.isV4Api()) {
+            return 
MojoDescriptorCreator.convert(mojoDescriptor.getMojoDescriptorV4());
+        } else {
+            return MojoDescriptorCreator.convert(mojoDescriptor).getDom();
+        }
+    }
+
+    private MojoExecutionConfigurator mojoExecutionConfigurator(MojoExecution 
mojoExecution) {
+        String configuratorId = 
mojoExecution.getMojoDescriptor().getComponentConfigurator();
+        if (configuratorId == null) {
+            configuratorId = "default";
+        }
+
+        MojoExecutionConfigurator mojoExecutionConfigurator = 
mojoExecutionConfigurators.get(configuratorId);
+
+        if (mojoExecutionConfigurator == null) {
+            //
+            // The plugin has a custom component configurator but does not 
have a custom mojo execution configurator
+            // so fall back to the default mojo execution configurator.
+            //
+            mojoExecutionConfigurator = 
mojoExecutionConfigurators.get("default");
+        }
+        return mojoExecutionConfigurator;
+    }
+
+    public static void attachToThread(MavenProject currentProject) {
+        ClassRealm projectRealm = currentProject.getClassRealm();
+        if (projectRealm != null) {
+            Thread.currentThread().setContextClassLoader(projectRealm);
+        }
+    }
+
+    protected static class Clock {
+        long start;
+        long end;
+        long resumed;
+        long exec;
+
+        protected void start() {
+            if (start == 0) {
+                start = System.nanoTime();
+                resumed = start;
+            } else {
+                resumed = System.nanoTime();
+            }
+        }
+
+        protected void stop() {
+            end = System.nanoTime();
+            exec += end - resumed;
+        }
+
+        protected long wallTime() {
+            return TimeUnit.NANOSECONDS.toMillis(end - start);
+        }
+
+        protected long execTime() {
+            return TimeUnit.NANOSECONDS.toMillis(exec);
+        }
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java
new file mode 100644
index 0000000000..85bbe00e64
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import javax.inject.Named;
+
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.project.MavenProject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * <p>
+ * Logs debug output from the various lifecycle phases.
+ * </p>
+ * <strong>NOTE:</strong> This class is not part of any public api and can be 
changed or deleted without prior notice.
+ *
+ * @since 3.0
+ */
+@Named
+public class BuildPlanLogger {
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    public void writePlan(BuildPlan plan) {
+        if (logger.isDebugEnabled()) {
+            writePlan(logger::debug, plan);
+        }
+    }
+
+    public void writePlan(BuildPlan plan, MavenProject project) {
+        if (logger.isDebugEnabled()) {
+            writePlan(logger::debug, plan, project);
+        }
+    }
+
+    public void writePlan(Consumer<String> writer, BuildPlan plan) {
+        plan.projects().forEach(project -> writePlan(writer, plan, project));
+    }
+
+    public void writePlan(Consumer<String> writer, BuildPlan plan, 
MavenProject project) {
+        writer.accept("=== PROJECT BUILD PLAN 
================================================");
+        writer.accept("Project:                     " + getKey(project));
+        writer.accept("Repositories (dependencies): " + 
project.getRemoteProjectRepositories());
+        writer.accept("Repositories (plugins):      " + 
project.getRemotePluginRepositories());
+
+        Optional<BuildStep> planStep = plan.step(project, BuildStep.PLAN);
+        if (planStep.isPresent() && planStep.get().status.get() == 
BuildStep.PLANNING) {
+            writer.accept("Build plan will be lazily computed");
+        } else {
+            plan.steps(project)
+                    .filter(step ->
+                            step.phase != null && 
step.executions().findAny().isPresent())
+                    
.sorted(Comparator.comparingInt(plan.sortedNodes()::indexOf))
+                    .forEach(step -> {
+                        
writer.accept("\t-----------------------------------------------------------------------");
+                        writer.accept("\tPhase:         " + step.name);
+                        if (!step.predecessors.isEmpty()) {
+                            writer.accept("\tPredecessors:  "
+                                    + nonEmptyPredecessors(step)
+                                            .map(n -> phase(project, n, 
plan.duplicateIds()))
+                                            .collect(Collectors.joining(", 
")));
+                        }
+                        /*
+                        if (!node.successors.isEmpty()) {
+                            writer.accept("\tSuccessors:    "
+                                    + node.successors.stream()
+                                            .map(n -> phase(currentProject, n, 
duplicateIds))
+                                            .collect(Collectors.joining(", 
")));
+                        }
+                        */
+                        step.mojos.values().stream()
+                                .flatMap(m -> m.values().stream())
+                                .forEach(mojo -> mojo(writer, mojo));
+                    });
+        }
+
+        
writer.accept("=======================================================================");
+    }
+
+    protected Stream<BuildStep> nonEmptyPredecessors(BuildStep step) {
+        HashSet<BuildStep> preds = new HashSet<>();
+        nonEmptyPredecessors(step, preds, new HashSet<>());
+        return preds.stream();
+    }
+
+    private void nonEmptyPredecessors(BuildStep step, Set<BuildStep> preds, 
Set<BuildStep> visited) {
+        if (visited.add(step)) {
+            step.predecessors.forEach(ch -> {
+                if (ch.executions().findAny().isPresent()) {
+                    preds.add(ch);
+                } else {
+                    nonEmptyPredecessors(ch, preds, visited);
+                }
+            });
+        }
+    }
+
+    protected String phase(MavenProject currentProject, BuildStep step, 
Set<String> duplicateIds) {
+        if (step.project == currentProject) {
+            return step.name;
+        } else {
+            String artifactId = step.project.getArtifactId();
+            if (duplicateIds.contains(artifactId)) {
+                return step.name + "(" + step.project.getGroupId() + ":" + 
artifactId + ")";
+            } else {
+                return step.name + "(:" + artifactId + ")";
+            }
+        }
+    }
+
+    protected void mojo(Consumer<String> writer, MojoExecution mojoExecution) {
+        String mojoExecId =
+                mojoExecution.getGroupId() + ':' + 
mojoExecution.getArtifactId() + ':' + mojoExecution.getVersion()
+                        + ':' + mojoExecution.getGoal() + " (" + 
mojoExecution.getExecutionId() + ')';
+
+        Map<String, List<MojoExecution>> forkedExecutions = 
mojoExecution.getForkedExecutions();
+        if (!forkedExecutions.isEmpty()) {
+            for (Map.Entry<String, List<MojoExecution>> fork : 
forkedExecutions.entrySet()) {
+                writer.accept("\t--- init fork of " + fork.getKey() + " for " 
+ mojoExecId + " ---");
+
+                for (MojoExecution forkedExecution : fork.getValue()) {
+                    mojo(writer, forkedExecution);
+                }
+
+                writer.accept("\t--- exit fork of " + fork.getKey() + " for " 
+ mojoExecId + " ---");
+            }
+        }
+
+        
writer.accept("\t\t-----------------------------------------------------------------------");
+        if (mojoExecution.getMojoDescriptor().isAggregator()) {
+            writer.accept("\t\tAggregator goal:        " + mojoExecId);
+        } else {
+            writer.accept("\t\tGoal:                   " + mojoExecId);
+        }
+        if (mojoExecution.getConfiguration() != null) {
+            writer.accept("\t\tConfiguration:          " + 
mojoExecution.getConfiguration());
+        }
+        if 
(mojoExecution.getMojoDescriptor().getDependencyCollectionRequired() != null) {
+            writer.accept("\t\tDependencies (collect): "
+                    + 
mojoExecution.getMojoDescriptor().getDependencyCollectionRequired());
+        }
+        if 
(mojoExecution.getMojoDescriptor().getDependencyResolutionRequired() != null) {
+            writer.accept("\t\tDependencies (resolve): "
+                    + 
mojoExecution.getMojoDescriptor().getDependencyResolutionRequired());
+        }
+    }
+
+    protected String getKey(MavenProject project) {
+        return project.getGroupId() + ':' + project.getArtifactId() + ':' + 
project.getVersion();
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java
new file mode 100644
index 0000000000..e0b7c9598a
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import org.apache.maven.api.Lifecycle;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.project.MavenProject;
+
+public class BuildStep {
+    public static final int CREATED = 0;
+    public static final int PLANNING = 1;
+    public static final int SCHEDULED = 2;
+    public static final int EXECUTED = 3;
+    public static final int FAILED = 4;
+
+    public static final String PLAN = "$plan$";
+    public static final String SETUP = "$setup$";
+    public static final String TEARDOWN = "$teardown$";
+
+    final MavenProject project;
+    final String name;
+    final Lifecycle.Phase phase;
+    final Map<Integer, Map<String, MojoExecution>> mojos = new TreeMap<>();
+    final Collection<BuildStep> predecessors = new HashSet<>();
+    final Collection<BuildStep> successors = new HashSet<>();
+    final AtomicInteger status = new AtomicInteger();
+    final AtomicBoolean skip = new AtomicBoolean();
+
+    public BuildStep(String name, MavenProject project, Lifecycle.Phase phase) 
{
+        this.name = name;
+        this.project = project;
+        this.phase = phase;
+    }
+
+    public Stream<BuildStep> allPredecessors() {
+        return preds(new HashSet<>()).stream();
+    }
+
+    private Set<BuildStep> preds(Set<BuildStep> preds) {
+        if (preds.add(this)) {
+            this.predecessors.forEach(n -> n.preds(preds));
+        }
+        return preds;
+    }
+
+    public boolean isSuccessorOf(BuildStep step) {
+        return isSuccessorOf(new HashSet<>(), step);
+    }
+
+    private boolean isSuccessorOf(Set<BuildStep> visited, BuildStep step) {
+        if (this == step) {
+            return true;
+        } else if (visited.add(this)) {
+            return this.predecessors.stream().anyMatch(n -> 
n.isSuccessorOf(visited, step));
+        } else {
+            return false;
+        }
+    }
+
+    public void skip() {
+        skip.set(true);
+        mojos.clear();
+    }
+
+    public void addMojo(MojoExecution mojo, int priority) {
+        if (!skip.get()) {
+            mojos.computeIfAbsent(priority, k -> new LinkedHashMap<>())
+                    .put(mojo.getGoal() + ":" + mojo.getExecutionId(), mojo);
+        }
+    }
+
+    public void executeAfter(BuildStep stepToExecuteBefore) {
+        if (!isSuccessorOf(stepToExecuteBefore)) {
+            predecessors.add(stepToExecuteBefore);
+            stepToExecuteBefore.successors.add(this);
+        }
+    }
+
+    public Stream<MojoExecution> executions() {
+        return mojos.values().stream().flatMap(m -> m.values().stream());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        BuildStep that = (BuildStep) o;
+        return Objects.equals(project, that.project) && Objects.equals(name, 
that.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(project, name);
+    }
+
+    @Override
+    public String toString() {
+        return "BuildStep[" + "project="
+                + project.getGroupId() + ":" + project.getArtifactId() + ", 
phase="
+                + name + ']';
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLifecycleStarter.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLifecycleStarter.java
new file mode 100644
index 0000000000..e4422ffc1c
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLifecycleStarter.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.execution.ExecutionEvent;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.lifecycle.DefaultLifecycles;
+import org.apache.maven.lifecycle.MissingProjectException;
+import org.apache.maven.lifecycle.NoGoalSpecifiedException;
+import org.apache.maven.lifecycle.internal.ExecutionEventCatapult;
+import org.apache.maven.lifecycle.internal.GoalTask;
+import org.apache.maven.lifecycle.internal.LifecyclePluginResolver;
+import org.apache.maven.lifecycle.internal.LifecycleStarter;
+import org.apache.maven.lifecycle.internal.LifecycleTask;
+import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
+import org.apache.maven.lifecycle.internal.ReactorBuildStatus;
+import org.apache.maven.lifecycle.internal.ReactorContext;
+import org.apache.maven.lifecycle.internal.TaskSegment;
+import org.apache.maven.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.project.MavenProject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Starts the build life cycle
+ */
+@Named("concurrent")
+@Singleton
+public class ConcurrentLifecycleStarter implements LifecycleStarter {
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private final ExecutionEventCatapult eventCatapult;
+    private final DefaultLifecycles defaultLifeCycles;
+    private final BuildPlanExecutor executor;
+    private final LifecyclePluginResolver lifecyclePluginResolver;
+    private final MojoDescriptorCreator mojoDescriptorCreator;
+
+    @Inject
+    public ConcurrentLifecycleStarter(
+            ExecutionEventCatapult eventCatapult,
+            DefaultLifecycles defaultLifeCycles,
+            BuildPlanExecutor executor,
+            LifecyclePluginResolver lifecyclePluginResolver,
+            MojoDescriptorCreator mojoDescriptorCreator) {
+        this.eventCatapult = eventCatapult;
+        this.defaultLifeCycles = defaultLifeCycles;
+        this.executor = executor;
+        this.lifecyclePluginResolver = lifecyclePluginResolver;
+        this.mojoDescriptorCreator = mojoDescriptorCreator;
+    }
+
+    public void execute(MavenSession session) {
+        eventCatapult.fire(ExecutionEvent.Type.SessionStarted, session, null);
+
+        try {
+            if (requiresProject(session) && projectIsNotPresent(session)) {
+                throw new MissingProjectException("The goal you specified 
requires a project to execute"
+                        + " but there is no POM in this directory (" + 
session.getTopDirectory() + ")."
+                        + " Please verify you invoked Maven from the correct 
directory.");
+            }
+
+            List<TaskSegment> taskSegments = calculateTaskSegments(session);
+            if (taskSegments.isEmpty()) {
+                throw new NoGoalSpecifiedException("No goals have been 
specified for this build."
+                        + " You must specify a valid lifecycle phase or a goal 
in the format <plugin-prefix>:<goal> or"
+                        + " 
<plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>."
+                        + " Available lifecycle phases are: " + 
defaultLifeCycles.getLifecyclePhaseList() + ".");
+            }
+
+            int degreeOfConcurrency = 
session.getRequest().getDegreeOfConcurrency();
+            if (degreeOfConcurrency > 1) {
+                logger.info("");
+                logger.info(String.format(
+                        "Using the %s implementation with a thread count of 
%d",
+                        executor.getClass().getSimpleName(), 
degreeOfConcurrency));
+            }
+
+            ClassLoader oldContextClassLoader = 
Thread.currentThread().getContextClassLoader();
+            ReactorBuildStatus reactorBuildStatus = new 
ReactorBuildStatus(session.getProjectDependencyGraph());
+            ReactorContext reactorContext =
+                    new ReactorContext(session.getResult(), 
oldContextClassLoader, reactorBuildStatus);
+            executor.execute(session, reactorContext, taskSegments);
+
+        } catch (Exception e) {
+            session.getResult().addException(e);
+        } finally {
+            eventCatapult.fire(ExecutionEvent.Type.SessionEnded, session, 
null);
+        }
+    }
+
+    public List<TaskSegment> calculateTaskSegments(MavenSession session) 
throws Exception {
+
+        MavenProject rootProject = session.getTopLevelProject();
+
+        List<String> tasks = requireNonNull(session.getGoals()); // session 
never returns null, but empty list
+
+        if (tasks.isEmpty()
+                && (rootProject.getDefaultGoal() != null
+                        && !rootProject.getDefaultGoal().isEmpty())) {
+            tasks = Stream.of(rootProject.getDefaultGoal().split("\\s+"))
+                    .filter(g -> !g.isEmpty())
+                    .collect(Collectors.toList());
+        }
+
+        return calculateTaskSegments(session, tasks);
+    }
+
+    public List<TaskSegment> calculateTaskSegments(MavenSession session, 
List<String> tasks) throws Exception {
+        List<TaskSegment> taskSegments = new ArrayList<>(tasks.size());
+
+        TaskSegment currentSegment = null;
+
+        for (String task : tasks) {
+            if (isGoalSpecification(task)) {
+                // "pluginPrefix[:version]:goal" or 
"groupId:artifactId[:version]:goal"
+
+                
lifecyclePluginResolver.resolveMissingPluginVersions(session.getTopLevelProject(),
 session);
+
+                MojoDescriptor mojoDescriptor =
+                        mojoDescriptorCreator.getMojoDescriptor(task, session, 
session.getTopLevelProject());
+
+                boolean aggregating = mojoDescriptor.isAggregator() || 
!mojoDescriptor.isProjectRequired();
+
+                if (currentSegment == null || currentSegment.isAggregating() 
!= aggregating) {
+                    currentSegment = new TaskSegment(aggregating);
+                    taskSegments.add(currentSegment);
+                }
+
+                currentSegment.getTasks().add(new GoalTask(task));
+            } else {
+                // lifecycle phase
+
+                if (currentSegment == null || currentSegment.isAggregating()) {
+                    currentSegment = new TaskSegment(false);
+                    taskSegments.add(currentSegment);
+                }
+
+                currentSegment.getTasks().add(new LifecycleTask(task));
+            }
+        }
+
+        return taskSegments;
+    }
+
+    private boolean projectIsNotPresent(MavenSession session) {
+        return !session.getRequest().isProjectPresent();
+    }
+
+    private boolean requiresProject(MavenSession session) {
+        List<String> goals = session.getGoals();
+        if (goals != null) {
+            for (String goal : goals) {
+                if (!isGoalSpecification(goal)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isGoalSpecification(String task) {
+        return task.indexOf(':') >= 0;
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLogOutput.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLogOutput.java
new file mode 100644
index 0000000000..5c5342ba8e
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLogOutput.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.slf4j.MavenSimpleLogger;
+
+/**
+ * Forwards log messages to the client.
+ */
+public class ConcurrentLogOutput implements AutoCloseable {
+
+    private static final ThreadLocal<ProjectExecutionContext> CONTEXT = new 
InheritableThreadLocal<>();
+
+    public ConcurrentLogOutput() {
+        MavenSimpleLogger.setLogSink(this::accept);
+    }
+
+    protected void accept(String message) {
+        ProjectExecutionContext context = CONTEXT.get();
+        if (context != null) {
+            context.accept(message);
+        } else {
+            System.out.println(message);
+        }
+    }
+
+    @Override
+    public void close() {
+        MavenSimpleLogger.setLogSink(null);
+    }
+
+    public AutoCloseable build(MavenProject project) {
+        return new ProjectExecutionContext(project);
+    }
+
+    private static class ProjectExecutionContext implements AutoCloseable {
+        final MavenProject project;
+        final List<String> messages = new CopyOnWriteArrayList<>();
+        boolean closed;
+
+        ProjectExecutionContext(MavenProject project) {
+            this.project = project;
+            CONTEXT.set(this);
+        }
+
+        void accept(String message) {
+            if (!closed) {
+                this.messages.add(message);
+            } else {
+                System.out.println(message);
+            }
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+            CONTEXT.set(null);
+            this.messages.forEach(System.out::println);
+        }
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/MojoExecutor.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/MojoExecutor.java
new file mode 100644
index 0000000000..273035b82c
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/MojoExecutor.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.apache.maven.api.services.MessageBuilderFactory;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.lifecycle.internal.ExecutionEventCatapult;
+import org.apache.maven.lifecycle.internal.LifecycleDependencyResolver;
+import org.apache.maven.plugin.BuildPluginManager;
+import org.apache.maven.plugin.MavenPluginManager;
+import org.apache.maven.plugin.MojosExecutionStrategy;
+
+@Named("concurrent")
+@Singleton
+public class MojoExecutor extends 
org.apache.maven.lifecycle.internal.MojoExecutor {
+
+    @Inject
+    public MojoExecutor(
+            BuildPluginManager pluginManager,
+            MavenPluginManager mavenPluginManager,
+            LifecycleDependencyResolver lifeCycleDependencyResolver,
+            ExecutionEventCatapult eventCatapult,
+            Provider<MojosExecutionStrategy> mojosExecutionStrategy,
+            MessageBuilderFactory messageBuilderFactory) {
+        super(
+                pluginManager,
+                mavenPluginManager,
+                lifeCycleDependencyResolver,
+                eventCatapult,
+                mojosExecutionStrategy,
+                messageBuilderFactory);
+    }
+
+    @Override
+    protected boolean useProjectLock(MavenSession session) {
+        return false;
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutor.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutor.java
new file mode 100644
index 0000000000..c27f5ce5b3
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutor.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+
+public class PhasingExecutor implements Executor, AutoCloseable {
+    private final ExecutorService executor;
+    private final Phaser phaser = new Phaser();
+
+    public PhasingExecutor(ExecutorService executor) {
+        this.executor = executor;
+        this.phaser.register();
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        phaser.register();
+        executor.submit(() -> {
+            try {
+                command.run();
+            } finally {
+                phaser.arriveAndDeregister();
+            }
+        });
+    }
+
+    public void await() {
+        phaser.arriveAndAwaitAdvance();
+    }
+
+    @Override
+    public void close() {
+        executor.shutdownNow();
+    }
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PluginLifecycle.java
 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PluginLifecycle.java
new file mode 100644
index 0000000000..81a319b7ae
--- /dev/null
+++ 
b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PluginLifecycle.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.maven.api.Lifecycle;
+import org.apache.maven.api.model.Plugin;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
+
+class PluginLifecycle implements Lifecycle {
+    private final org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle 
lifecycleOverlay;
+    private final PluginDescriptor pluginDescriptor;
+
+    PluginLifecycle(
+            org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle 
lifecycleOverlay,
+            PluginDescriptor pluginDescriptor) {
+        this.lifecycleOverlay = lifecycleOverlay;
+        this.pluginDescriptor = pluginDescriptor;
+    }
+
+    @Override
+    public String id() {
+        return lifecycleOverlay.getId();
+    }
+
+    @Override
+    public Collection<Phase> phases() {
+        return lifecycleOverlay.getPhases().stream()
+                .map(phase -> new Phase() {
+                    @Override
+                    public String name() {
+                        return phase.getId();
+                    }
+
+                    @Override
+                    public List<Plugin> plugins() {
+                        return Collections.singletonList(Plugin.newBuilder()
+                                .groupId(pluginDescriptor.getGroupId())
+                                .artifactId(pluginDescriptor.getArtifactId())
+                                .version(pluginDescriptor.getVersion())
+                                .configuration(phase.getConfiguration())
+                                .executions(phase.getExecutions().stream()
+                                        .map(exec -> 
org.apache.maven.api.model.PluginExecution.newBuilder()
+                                                .goals(exec.getGoals())
+                                                
.configuration(exec.getConfiguration())
+                                                .build())
+                                        .collect(Collectors.toList()))
+                                .build());
+                    }
+
+                    @Override
+                    public Collection<Link> links() {
+                        return Collections.emptyList();
+                    }
+
+                    @Override
+                    public List<Phase> phases() {
+                        return Collections.emptyList();
+                    }
+
+                    @Override
+                    public Stream<Phase> allPhases() {
+                        return Stream.concat(Stream.of(this), 
phases().stream().flatMap(Phase::allPhases));
+                    }
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Collection<Alias> aliases() {
+        return Collections.emptyList();
+    }
+}
diff --git 
a/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java
 
b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java
new file mode 100644
index 0000000000..0e3fd5dd0c
--- /dev/null
+++ 
b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.concurrent;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.maven.internal.impl.DefaultLifecycleRegistry;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.project.MavenProject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class BuildPlanCreatorTest {
+
+    @Test
+    void testMulti() {
+        MavenProject project = new MavenProject();
+        Map<MavenProject, List<MavenProject>> projects = 
Collections.singletonMap(project, Collections.emptyList());
+
+        BuildPlan plan = calculateLifecycleMappings(projects, "package");
+
+        new BuildPlanLogger().writePlan(System.out::println, plan);
+    }
+
+    @Test
+    void testCondense() {
+        MavenProject p1 = new MavenProject();
+        p1.setArtifactId("p1");
+        MavenProject p2 = new MavenProject();
+        p2.setArtifactId("p2");
+        Map<MavenProject, List<MavenProject>> projects = new HashMap<>();
+        projects.put(p1, Collections.emptyList());
+        projects.put(p2, Collections.singletonList(p1));
+
+        BuildPlan plan = calculateLifecycleMappings(projects, "verify");
+        plan.then(calculateLifecycleMappings(projects, "install"));
+
+        Stream.of(p1, p2).forEach(project -> {
+            plan.requiredStep(project, "after:resources").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "after:test-resources").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "compile").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "test-compile").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "test").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "package").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "install").addMojo(new 
MojoExecution(null), 0);
+        });
+
+        new BuildPlanLogger() {
+            @Override
+            protected void mojo(Consumer<String> writer, MojoExecution 
mojoExecution) {}
+        }.writePlan(System.out::println, plan);
+
+        plan.allSteps().forEach(phase -> {
+            phase.predecessors.forEach(
+                    pred -> assertTrue(plan.step(pred.project, 
pred.name).isPresent(), "Phase not present: " + pred));
+        });
+    }
+
+    @Test
+    void testAlias() {
+        MavenProject p1 = new MavenProject();
+        p1.setArtifactId("p1");
+        Map<MavenProject, List<MavenProject>> projects = 
Collections.singletonMap(p1, Collections.emptyList());
+
+        BuildPlan plan = calculateLifecycleMappings(projects, 
"generate-resources");
+    }
+
+    private BuildPlan calculateLifecycleMappings(Map<MavenProject, 
List<MavenProject>> projects, String phase) {
+        DefaultLifecycleRegistry lifecycles = new 
DefaultLifecycleRegistry(Collections.emptyList());
+        BuildPlanExecutor builder = new BuildPlanExecutor(null, null, null, 
null, null, null, null, null, lifecycles);
+        BuildPlanExecutor.BuildContext context = builder.new BuildContext();
+        return context.calculateLifecycleMappings(projects, phase);
+    }
+
+    /*
+    @Test
+    void testPlugins() {
+        DefaultLifecycleRegistry lifecycles =
+                new DefaultLifecycleRegistry(Collections.emptyList(), 
Collections.emptyMap());
+        BuildPlanCreator builder = new BuildPlanCreator(null, null, null, 
null, null, lifecycles);
+        MavenProject p1 = new MavenProject();
+        p1.setGroupId("g");
+        p1.setArtifactId("p1");
+        p1.getBuild().getPlugins().add(new 
Plugin(org.apache.maven.api.model.Plugin.newBuilder()
+                .groupId("g").artifactId("p2")
+                .
+                .build()))
+        MavenProject p2 = new MavenProject();
+        p2.setGroupId("g");
+        p2.setArtifactId("p2");
+
+        Map<MavenProject, List<MavenProject>> projects = new HashMap<>();
+        projects.put(p1, Collections.emptyList());
+        projects.put(p2, Collections.singletonList(p1));
+        Lifecycle lifecycle = lifecycles.require("default");
+        BuildPlan plan = builder.calculateLifecycleMappings(null, projects, 
lifecycle, "verify");
+        plan.then(builder.calculateLifecycleMappings(null, projects, 
lifecycle, "install"));
+
+        Stream.of(p1, p2).forEach(project -> {
+            plan.requiredStep(project, "post:resources").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "post:test-resources").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "compile").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "test-compile").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "test").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "package").addMojo(new 
MojoExecution(null), 0);
+            plan.requiredStep(project, "install").addMojo(new 
MojoExecution(null), 0);
+        });
+
+        plan.condense();
+
+        new BuildPlanLogger() {
+            @Override
+            protected void mojo(Consumer<String> writer, MojoExecution 
mojoExecution) {}
+        }.writePlan(System.out::println, plan);
+
+        plan.allSteps().forEach(phase -> {
+            phase.predecessors.forEach(
+                    pred -> assertTrue(plan.step(pred.project, 
pred.name).isPresent(), "Phase not present: " + pred));
+        });
+    }
+     */
+}
diff --git 
a/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java 
b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java
similarity index 53%
copy from maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java
copy to 
maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java
index 0f0310d3cd..5c038cd072 100644
--- a/maven-core/src/main/java/org/apache/maven/execution/BuildSuccess.java
+++ 
b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java
@@ -16,23 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.execution;
+package org.apache.maven.lifecycle.internal.concurrent;
 
-import org.apache.maven.project.MavenProject;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
-/**
- * Summarizes the result of a successful project build in the reactor.
- *
- */
-public class BuildSuccess extends BuildSummary {
+import org.junit.jupiter.api.Test;
+
+public class PhasingExecutorTest {
+
+    @Test
+    void testPhaser() {
+        PhasingExecutor p = new 
PhasingExecutor(Executors.newFixedThreadPool(4));
+        p.execute(() -> waitSomeTime(p, 2));
+        p.await();
+    }
 
-    /**
-     * Creates a new build summary for the specified project.
-     *
-     * @param project The project being summarized, must not be {@code null}.
-     * @param time The build time of the project in milliseconds.
-     */
-    public BuildSuccess(MavenProject project, long time) {
-        super(project, time);
+    private void waitSomeTime(Executor executor, int nb) {
+        try {
+            Thread.sleep(10);
+            if (nb > 0) {
+                executor.execute(() -> waitSomeTime(executor, nb - 1));
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
     }
 }

Reply via email to