From 48fde02ddc55962ac78bda332a551fff08df56b2 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 29 Feb 2024 08:00:54 +0100 Subject: [PATCH] [MNG-8052] Concurrently lifecycle executor --- maven-core/pom.xml | 8 + .../apache/maven/execution/BuildFailure.java | 14 +- .../apache/maven/execution/BuildSuccess.java | 13 +- .../apache/maven/execution/BuildSummary.java | 36 +- .../CompoundProjectExecutionListener.java | 4 +- .../lifecycle/internal/MojoExecutor.java | 91 +- .../internal/concurrent/BuildPlan.java | 166 ++++ .../concurrent/BuildPlanExecutor.java | 939 ++++++++++++++++++ .../internal/concurrent/BuildPlanLogger.java | 173 ++++ .../internal/concurrent/BuildStep.java | 133 +++ .../ConcurrentLifecycleStarter.java | 191 ++++ .../concurrent/ConcurrentLogOutput.java | 81 ++ .../internal/concurrent/MojoExecutor.java | 59 ++ .../internal/concurrent/PhasingExecutor.java | 54 + .../internal/concurrent/PluginLifecycle.java | 94 ++ .../concurrent/BuildPlanCreatorTest.java | 144 +++ .../concurrent/PhasingExecutorTest.java | 45 + .../java/org/apache/maven/cli/MavenCli.java | 4 +- 18 files changed, 2200 insertions(+), 49 deletions(-) create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLifecycleStarter.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/ConcurrentLogOutput.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/MojoExecutor.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutor.java create mode 100644 maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/PluginLifecycle.java create mode 100644 maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanCreatorTest.java create mode 100644 maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java diff --git a/maven-core/pom.xml b/maven-core/pom.xml index f79a0d40df13..39eca0414588 100644 --- a/maven-core/pom.xml +++ b/maven-core/pom.xml @@ -91,6 +91,14 @@ under the License. org.apache.maven maven-api-impl + + org.apache.maven + maven-jline + + + org.apache.maven + maven-slf4j-provider + org.apache.maven.resolver maven-resolver-api 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 cd7155076d5c..2c43f1e943ec 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 0f0310d3cdec..a2a4546b23bf 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 657afb31ebe6..efc0da1d8be2 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 MavenProject getProject() { } /** - * 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 34cee12b956e..33eb878aabf9 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.ProjectExecutionListener; import org.apache.maven.lifecycle.LifecycleExecutionException; -class CompoundProjectExecutionListener implements ProjectExecutionListener { +public class CompoundProjectExecutionListener implements ProjectExecutionListener { private final Collection listeners; - CompoundProjectExecutionListener(Collection listeners) { + public CompoundProjectExecutionListener(Collection 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 5f442555bc88..86a9ce856eb2 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 @@ -213,6 +213,13 @@ private void execute(MavenSession session, MojoExecution mojoExecution, Dependen 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 @@ -221,54 +228,45 @@ private void execute(MavenSession session, MojoExecution mojoExecution, Dependen * 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()); } @@ -307,7 +305,7 @@ private void doExecute(MavenSession session, MojoExecution mojoExecution, Depend 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) { @@ -316,6 +314,23 @@ private void doExecute(MavenSession session, MojoExecution mojoExecution, Depend } } + 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 000000000000..222a1ecd77e4 --- /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> plan = new LinkedHashMap<>(); + private final Map> projects; + private final Map aliases = new HashMap<>(); + private volatile Set duplicateIds; + private volatile List sortedNodes; + + BuildPlan() { + this.projects = null; + } + + public BuildPlan(Map> projects) { + this.projects = projects; + } + + public Map> getAllProjects() { + return projects; + } + + public Map aliases() { + return aliases; + } + + public Stream projects() { + return plan.keySet().stream(); + } + + public void addProject(MavenProject project, Map steps) { + plan.put(project, steps); + } + + public void addStep(MavenProject project, String name, BuildStep step) { + plan.get(project).put(name, step); + } + + public Stream allSteps() { + return plan.values().stream().flatMap(m -> m.values().stream()); + } + + public Stream steps(MavenProject project) { + return Optional.ofNullable(plan.get(project)) + .map(m -> m.values().stream()) + .orElse(Stream.empty()); + } + + public Optional 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 merge(Map org, Map add) { + // all new phases should be added after the existing ones + List lasts = + org.values().stream().filter(b -> b.successors.isEmpty()).collect(Collectors.toList()); + List 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 mergeMojos(Map l1, Map 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 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 sortedNodes() { + if (sortedNodes == null) { + synchronized (this) { + if (sortedNodes == null) { + List sortedNodes = new ArrayList<>(); + Set 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 visited, List 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 000000000000..6f77b927815b --- /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). + *

+ * This builder uses a number of threads equal to the minimum of the degree of concurrency (which is the thread count + * set with -T 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. + *

+ * NOTE: 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 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 listeners, + ConsumerPomArtifactTransformer consumerPomArtifactTransformer, + BuildPlanLogger buildPlanLogger, + Map 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 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 clocks = new ConcurrentHashMap<>(); + final ReadWriteLock lock = new ReentrantReadWriteLock(); + final int threads; + BuildPlan plan; + + BuildContext(MavenSession session, ReactorContext reactorContext, List 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 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> allProjects = new LinkedHashMap<>(); + dependencyGraph + .getSortedProjects() + .forEach(p -> allProjects.put(p, dependencyGraph.getUpstreamProjects(p, false))); + + BuildPlan plan = new BuildPlan(allProjects); + for (TaskSegment taskSegment : taskSegments) { + Map> 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 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 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 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 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 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 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 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 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 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> 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> projects, List 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 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> 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 : or" + + " :[:]:. 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> 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 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 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 reactorGavs = + projects.keySet().stream().collect(Collectors.toMap(BuildPlanExecutor::gav, p -> p)); + + // Go through all plugins + List 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 getLinkedProjects( + Map> 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 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 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 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 000000000000..78d3f1b76bf2 --- /dev/null +++ b/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanLogger.java @@ -0,0 +1,173 @@ +/* + * 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; + +/** + *

+ * Logs debug output from the various lifecycle phases. + *

+ * NOTE: 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) { + writePlan(logger::info, plan); + } + + public void writePlan(BuildPlan plan, MavenProject project) { + writePlan(logger::info, plan, project); + } + + public void writePlan(Consumer writer, BuildPlan plan) { + plan.projects().forEach(project -> writePlan(writer, plan, project)); + } + + public void writePlan(Consumer 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 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 nonEmptyPredecessors(BuildStep step) { + HashSet preds = new HashSet<>(); + nonEmptyPredecessors(step, preds, new HashSet<>()); + return preds.stream(); + } + + private void nonEmptyPredecessors(BuildStep step, Set preds, Set 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 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 writer, MojoExecution mojoExecution) { + String mojoExecId = + mojoExecution.getGroupId() + ':' + mojoExecution.getArtifactId() + ':' + mojoExecution.getVersion() + + ':' + mojoExecution.getGoal() + " (" + mojoExecution.getExecutionId() + ')'; + + Map> forkedExecutions = mojoExecution.getForkedExecutions(); + if (!forkedExecutions.isEmpty()) { + for (Map.Entry> 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 000000000000..e0b7c9598a0c --- /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> mojos = new TreeMap<>(); + final Collection predecessors = new HashSet<>(); + final Collection 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 allPredecessors() { + return preds(new HashSet<>()).stream(); + } + + private Set preds(Set 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 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 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 000000000000..e4422ffc1cd1 --- /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 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 : or" + + " :[:]:." + + " 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 calculateTaskSegments(MavenSession session) throws Exception { + + MavenProject rootProject = session.getTopLevelProject(); + + List 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 calculateTaskSegments(MavenSession session, List tasks) throws Exception { + List 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 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 000000000000..5c5342ba8ef8 --- /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 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 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 000000000000..273035b82c13 --- /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, + 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 000000000000..c27f5ce5b3f5 --- /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 000000000000..81a319b7aea6 --- /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 phases() { + return lifecycleOverlay.getPhases().stream() + .map(phase -> new Phase() { + @Override + public String name() { + return phase.getId(); + } + + @Override + public List 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 links() { + return Collections.emptyList(); + } + + @Override + public List phases() { + return Collections.emptyList(); + } + + @Override + public Stream allPhases() { + return Stream.concat(Stream.of(this), phases().stream().flatMap(Phase::allPhases)); + } + }) + .collect(Collectors.toList()); + } + + @Override + public Collection 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 000000000000..0e3fd5dd0ce8 --- /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> 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> 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 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> projects = Collections.singletonMap(p1, Collections.emptyList()); + + BuildPlan plan = calculateLifecycleMappings(projects, "generate-resources"); + } + + private BuildPlan calculateLifecycleMappings(Map> 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> 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 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/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java new file mode 100644 index 000000000000..5c038cd072e2 --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/lifecycle/internal/concurrent/PhasingExecutorTest.java @@ -0,0 +1,45 @@ +/* + * 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.Executors; + +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(); + } + + 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); + } + } +} diff --git a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java index 02974d2d9076..97e53f3996e7 100644 --- a/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java +++ b/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java @@ -1337,7 +1337,7 @@ private MavenExecutionRequest populateRequest(CliRequest cliRequest, MavenExecut // parameters but this is sufficient for now. Ultimately we want components like Builders to provide a way to // extend the command line to accept its own configuration parameters. // - final String threadConfiguration = commandLine.getOptionValue(CLIManager.THREADS); + final String threadConfiguration = commandLine.getOptionValue(CLIManager.THREADS, "1C"); if (threadConfiguration != null) { int degreeOfConcurrency = calculateDegreeOfConcurrency(threadConfiguration); @@ -1350,7 +1350,7 @@ private MavenExecutionRequest populateRequest(CliRequest cliRequest, MavenExecut // // Allow the builder to be overridden by the user if requested. The builders are now pluggable. // - request.setBuilderId(commandLine.getOptionValue(CLIManager.BUILDER, request.getBuilderId())); + request.setBuilderId(commandLine.getOptionValue(CLIManager.BUILDER, "concurrent")); return request; }