diff --git a/daemon/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java b/daemon/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
new file mode 100644
index 000000000..68b048a4b
--- /dev/null
+++ b/daemon/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2019 the original author or authors.
+ *
+ * Licensed 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+/*
+ * 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.
+ */
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
+import org.apache.maven.artifact.resolver.filter.CumulativeScopeArtifactFilter;
+import org.apache.maven.execution.ExecutionEvent;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.lifecycle.LifecycleExecutionException;
+import org.apache.maven.lifecycle.MissingProjectException;
+import org.apache.maven.plugin.BuildPluginManager;
+import org.apache.maven.plugin.MavenPluginManager;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.PluginConfigurationException;
+import org.apache.maven.plugin.PluginIncompatibleException;
+import org.apache.maven.plugin.PluginManagerException;
+import org.apache.maven.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.annotations.Requirement;
+import org.codehaus.plexus.util.StringUtils;
+import org.eclipse.aether.SessionData;
+
+/**
+ *
+ * Executes an individual mojo
+ *
+ * NOTE: This class is not part of any public api and can be changed or deleted without prior notice.
+ *
+ * @author Jason van Zyl
+ * @author Benjamin Bentmann
+ * @author Kristian Rosenvold
+ * @since 3.0
+ */
+@Component(role = MojoExecutor.class)
+public class MojoExecutor {
+
+ @Requirement
+ private BuildPluginManager pluginManager;
+
+ @Requirement
+ private MavenPluginManager mavenPluginManager;
+
+ @Requirement
+ private LifecycleDependencyResolver lifeCycleDependencyResolver;
+
+ @Requirement
+ private ExecutionEventCatapult eventCatapult;
+
+ private final ReadWriteLock aggregatorLock = new ReentrantReadWriteLock();
+
+ public MojoExecutor() {
+ }
+
+ public DependencyContext newDependencyContext(MavenSession session, List mojoExecutions) {
+ Set scopesToCollect = new TreeSet<>();
+ Set scopesToResolve = new TreeSet<>();
+
+ collectDependencyRequirements(scopesToResolve, scopesToCollect, mojoExecutions);
+
+ return new DependencyContext(session.getCurrentProject(), scopesToCollect, scopesToResolve);
+ }
+
+ private void collectDependencyRequirements(Set scopesToResolve, Set scopesToCollect,
+ Collection mojoExecutions) {
+ for (MojoExecution mojoExecution : mojoExecutions) {
+ MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
+
+ scopesToResolve.addAll(toScopes(mojoDescriptor.getDependencyResolutionRequired()));
+
+ scopesToCollect.addAll(toScopes(mojoDescriptor.getDependencyCollectionRequired()));
+ }
+ }
+
+ private Collection toScopes(String classpath) {
+ Collection scopes = Collections.emptyList();
+
+ if (StringUtils.isNotEmpty(classpath)) {
+ if (Artifact.SCOPE_COMPILE.equals(classpath)) {
+ scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED);
+ } else if (Artifact.SCOPE_RUNTIME.equals(classpath)) {
+ scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME);
+ } else if (Artifact.SCOPE_COMPILE_PLUS_RUNTIME.equals(classpath)) {
+ scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED,
+ Artifact.SCOPE_RUNTIME);
+ } else if (Artifact.SCOPE_RUNTIME_PLUS_SYSTEM.equals(classpath)) {
+ scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_RUNTIME);
+ } else if (Artifact.SCOPE_TEST.equals(classpath)) {
+ scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED,
+ Artifact.SCOPE_RUNTIME, Artifact.SCOPE_TEST);
+ }
+ }
+ return Collections.unmodifiableCollection(scopes);
+ }
+
+ public void execute(MavenSession session, List mojoExecutions, ProjectIndex projectIndex)
+ throws LifecycleExecutionException
+
+ {
+ DependencyContext dependencyContext = newDependencyContext(session, mojoExecutions);
+
+ PhaseRecorder phaseRecorder = new PhaseRecorder(session.getCurrentProject());
+
+ for (MojoExecution mojoExecution : mojoExecutions) {
+ execute(session, mojoExecution, projectIndex, dependencyContext, phaseRecorder);
+ }
+ }
+
+ public void execute(MavenSession session, MojoExecution mojoExecution, ProjectIndex projectIndex,
+ DependencyContext dependencyContext, PhaseRecorder phaseRecorder)
+ throws LifecycleExecutionException {
+ execute(session, mojoExecution, projectIndex, dependencyContext);
+ phaseRecorder.observeExecution(mojoExecution);
+ }
+
+ private void execute(MavenSession session, MojoExecution mojoExecution, ProjectIndex projectIndex,
+ DependencyContext dependencyContext)
+ throws LifecycleExecutionException {
+ MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
+
+ try {
+ mavenPluginManager.checkRequiredMavenVersion(mojoDescriptor.getPluginDescriptor());
+ } catch (PluginIncompatibleException e) {
+ throw new LifecycleExecutionException(mojoExecution, session.getCurrentProject(), e);
+ }
+
+ if (mojoDescriptor.isProjectRequired() && !session.getRequest().isProjectPresent()) {
+ Throwable cause = new MissingProjectException(
+ "Goal requires a project to execute" + " but there is no POM in this directory ("
+ + session.getExecutionRootDirectory() + ")."
+ + " Please verify you invoked Maven from the correct directory.");
+ throw new LifecycleExecutionException(mojoExecution, null, cause);
+ }
+
+ if (mojoDescriptor.isOnlineRequired() && session.isOffline()) {
+ if (MojoExecution.Source.CLI.equals(mojoExecution.getSource())) {
+ Throwable cause = new IllegalStateException(
+ "Goal requires online mode for execution" + " but Maven is currently offline.");
+ throw new LifecycleExecutionException(mojoExecution, session.getCurrentProject(), cause);
+ } else {
+ eventCatapult.fire(ExecutionEvent.Type.MojoSkipped, session, mojoExecution);
+
+ return;
+ }
+ }
+
+ try (ProjectLock lock = new ProjectLock(session, mojoDescriptor, aggregatorLock)) {
+ doExecute(session, mojoExecution, projectIndex, dependencyContext);
+ }
+ }
+
+ /**
+ * 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
+ * all other executions until finished.
+ * We also lock on a given project to forbid a forked lifecycle to be executed concurrently with the project.
+ * TODO: ideally, the builder should take care of the ordering in a smarter way
+ * TODO: and concurrency issues fixed with MNG-7157
+ */
+ private static class ProjectLock implements AutoCloseable {
+ final Lock acquiredAggregatorLock;
+ final Lock acquiredProjectLock;
+
+ ProjectLock(MavenSession session, MojoDescriptor mojoDescriptor, ReadWriteLock aggregatorLock) {
+ if (session.getRequest().getDegreeOfConcurrency() > 1) {
+ boolean aggregator = mojoDescriptor.isAggregator();
+ acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : aggregatorLock.readLock();
+ acquiredProjectLock = getProjectLock(session);
+ acquiredAggregatorLock.lock();
+ acquiredProjectLock.lock();
+ } else {
+ acquiredAggregatorLock = null;
+ acquiredProjectLock = null;
+ }
+ }
+
+ @Override
+ public void close() {
+ // release the lock in the reverse order of the acquisition
+ if (acquiredProjectLock != null) {
+ acquiredProjectLock.unlock();
+ }
+ if (acquiredAggregatorLock != null) {
+ acquiredAggregatorLock.unlock();
+ }
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private Lock getProjectLock(MavenSession session) {
+ SessionData data = session.getRepositorySession().getData();
+ ConcurrentMap locks = (ConcurrentMap) data.get(ProjectLock.class);
+ // initialize the value if not already done (in case of a concurrent access) to the method
+ if (locks == null) {
+ // the call to data.set(k, null, v) is effectively a call to data.putIfAbsent(k, v)
+ data.set(ProjectLock.class, null, new ConcurrentHashMap<>());
+ locks = (ConcurrentMap) data.get(ProjectLock.class);
+ }
+ Lock acquiredProjectLock = locks.get(session.getCurrentProject());
+ if (acquiredProjectLock == null) {
+ acquiredProjectLock = new ReentrantLock();
+ Lock prev = locks.putIfAbsent(session.getCurrentProject(), acquiredProjectLock);
+ if (prev != null) {
+ acquiredProjectLock = prev;
+ }
+ }
+ return acquiredProjectLock;
+ }
+ }
+
+ private void doExecute(MavenSession session, MojoExecution mojoExecution, ProjectIndex projectIndex,
+ DependencyContext dependencyContext)
+ throws LifecycleExecutionException {
+ MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
+
+ List forkedProjects = executeForkedExecutions(mojoExecution, session, projectIndex);
+
+ ensureDependenciesAreResolved(mojoDescriptor, session, dependencyContext);
+
+ eventCatapult.fire(ExecutionEvent.Type.MojoStarted, session, mojoExecution);
+
+ try {
+ try {
+ pluginManager.executeMojo(session, mojoExecution);
+ } catch (MojoFailureException | PluginManagerException | PluginConfigurationException
+ | MojoExecutionException e) {
+ throw new LifecycleExecutionException(mojoExecution, session.getCurrentProject(), e);
+ }
+
+ eventCatapult.fire(ExecutionEvent.Type.MojoSucceeded, session, mojoExecution);
+ } catch (LifecycleExecutionException e) {
+ eventCatapult.fire(ExecutionEvent.Type.MojoFailed, session, mojoExecution, e);
+
+ throw e;
+ } finally {
+ for (MavenProject forkedProject : forkedProjects) {
+ forkedProject.setExecutionProject(null);
+ }
+ }
+ }
+
+ public void ensureDependenciesAreResolved(MojoDescriptor mojoDescriptor, MavenSession session,
+ DependencyContext dependencyContext)
+ throws LifecycleExecutionException
+
+ {
+ MavenProject project = dependencyContext.getProject();
+ boolean aggregating = mojoDescriptor.isAggregator();
+
+ if (dependencyContext.isResolutionRequiredForCurrentProject()) {
+ Collection scopesToCollect = dependencyContext.getScopesToCollectForCurrentProject();
+ Collection scopesToResolve = dependencyContext.getScopesToResolveForCurrentProject();
+
+ lifeCycleDependencyResolver.resolveProjectDependencies(project, scopesToCollect, scopesToResolve, session,
+ aggregating, Collections. emptySet());
+
+ dependencyContext.synchronizeWithProjectState();
+ }
+
+ if (aggregating) {
+ Collection scopesToCollect = toScopes(mojoDescriptor.getDependencyCollectionRequired());
+ Collection scopesToResolve = toScopes(mojoDescriptor.getDependencyResolutionRequired());
+
+ if (dependencyContext.isResolutionRequiredForAggregatedProjects(scopesToCollect, scopesToResolve)) {
+ for (MavenProject aggregatedProject : session.getProjects()) {
+ if (aggregatedProject != project) {
+ lifeCycleDependencyResolver.resolveProjectDependencies(aggregatedProject, scopesToCollect,
+ scopesToResolve, session, aggregating,
+ Collections. emptySet());
+ }
+ }
+ }
+ }
+
+ ArtifactFilter artifactFilter = getArtifactFilter(mojoDescriptor);
+ List projectsToResolve = LifecycleDependencyResolver.getProjects(session.getCurrentProject(), session,
+ mojoDescriptor.isAggregator());
+ for (MavenProject projectToResolve : projectsToResolve) {
+ projectToResolve.setArtifactFilter(artifactFilter);
+ }
+ }
+
+ private ArtifactFilter getArtifactFilter(MojoDescriptor mojoDescriptor) {
+ String scopeToResolve = mojoDescriptor.getDependencyResolutionRequired();
+ String scopeToCollect = mojoDescriptor.getDependencyCollectionRequired();
+
+ List scopes = new ArrayList<>(2);
+ if (StringUtils.isNotEmpty(scopeToCollect)) {
+ scopes.add(scopeToCollect);
+ }
+ if (StringUtils.isNotEmpty(scopeToResolve)) {
+ scopes.add(scopeToResolve);
+ }
+
+ if (scopes.isEmpty()) {
+ return null;
+ } else {
+ return new CumulativeScopeArtifactFilter(scopes);
+ }
+ }
+
+ public List executeForkedExecutions(MojoExecution mojoExecution, MavenSession session,
+ ProjectIndex projectIndex)
+ throws LifecycleExecutionException {
+ List forkedProjects = Collections.emptyList();
+
+ Map> forkedExecutions = mojoExecution.getForkedExecutions();
+
+ if (!forkedExecutions.isEmpty()) {
+ eventCatapult.fire(ExecutionEvent.Type.ForkStarted, session, mojoExecution);
+
+ MavenProject project = session.getCurrentProject();
+
+ forkedProjects = new ArrayList<>(forkedExecutions.size());
+
+ try {
+ for (Map.Entry> fork : forkedExecutions.entrySet()) {
+ String projectId = fork.getKey();
+
+ int index = projectIndex.getIndices().get(projectId);
+
+ MavenProject forkedProject = projectIndex.getProjects().get(projectId);
+
+ forkedProjects.add(forkedProject);
+
+ MavenProject executedProject = forkedProject.clone();
+
+ forkedProject.setExecutionProject(executedProject);
+
+ List mojoExecutions = fork.getValue();
+
+ if (mojoExecutions.isEmpty()) {
+ continue;
+ }
+
+ try {
+ session.setCurrentProject(executedProject);
+ session.getProjects().set(index, executedProject);
+ projectIndex.getProjects().put(projectId, executedProject);
+
+ eventCatapult.fire(ExecutionEvent.Type.ForkedProjectStarted, session, mojoExecution);
+
+ execute(session, mojoExecutions, projectIndex);
+
+ eventCatapult.fire(ExecutionEvent.Type.ForkedProjectSucceeded, session, mojoExecution);
+ } catch (LifecycleExecutionException e) {
+ eventCatapult.fire(ExecutionEvent.Type.ForkedProjectFailed, session, mojoExecution, e);
+
+ throw e;
+ } finally {
+ projectIndex.getProjects().put(projectId, forkedProject);
+ session.getProjects().set(index, forkedProject);
+ session.setCurrentProject(project);
+ }
+ }
+
+ eventCatapult.fire(ExecutionEvent.Type.ForkSucceeded, session, mojoExecution);
+ } catch (LifecycleExecutionException e) {
+ eventCatapult.fire(ExecutionEvent.Type.ForkFailed, session, mojoExecution, e);
+
+ throw e;
+ }
+ }
+
+ return forkedProjects;
+ }
+}
diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/execution/LockingEventSpy.java b/daemon/src/main/java/org/mvndaemon/mvnd/execution/LockingEventSpy.java
deleted file mode 100644
index 2b6dfe5ab..000000000
--- a/daemon/src/main/java/org/mvndaemon/mvnd/execution/LockingEventSpy.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2021 the original author or authors.
- *
- * Licensed 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.mvndaemon.mvnd.execution;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import javax.inject.Named;
-import javax.inject.Singleton;
-import org.apache.maven.eventspy.AbstractEventSpy;
-import org.apache.maven.eventspy.EventSpy;
-import org.apache.maven.execution.ExecutionEvent;
-import org.apache.maven.project.MavenProject;
-import org.eclipse.aether.SessionData;
-import org.eclipse.sisu.Typed;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * EventSpy implementation that provides a per-project locking mechanism
- * to make sure a given project can not be build twice concurrently.
- * This case can happen when running parallel builds with forked lifecycles
- */
-@Singleton
-@Named
-@Typed(EventSpy.class)
-public class LockingEventSpy extends AbstractEventSpy {
-
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- private static final Object LOCKS_KEY = new Object();
-
- @SuppressWarnings({ "unchecked", "rawtypes" })
- private Lock getLock(ExecutionEvent event) {
- SessionData data = event.getSession().getRepositorySession().getData();
- Map locks = (Map) data.get(LOCKS_KEY);
- // initialize the value if not already done (in case of a concurrent access) to the method
- if (locks == null) {
- // the call to data.set(k, null, v) is effectively a call to data.putIfAbsent(k, v)
- data.set(LOCKS_KEY, null, new ConcurrentHashMap<>());
- locks = (Map) data.get(LOCKS_KEY);
- }
- return locks.computeIfAbsent(event.getProject(), p -> new ReentrantLock());
- }
-
- @Override
- public void onEvent(Object event) throws Exception {
- if (event instanceof ExecutionEvent) {
- ExecutionEvent executionEvent = (ExecutionEvent) event;
- switch (executionEvent.getType()) {
- case ProjectStarted:
- case ForkedProjectStarted: {
- Lock lock = getLock(executionEvent);
- if (!lock.tryLock()) {
- logger.warn("Suspending concurrent execution of project '{}'", executionEvent.getProject());
- lock.lockInterruptibly();
- logger.warn("Resuming execution of project '{}'", executionEvent.getProject());
- }
- break;
- }
- case ProjectSucceeded:
- case ProjectFailed:
- case ForkedProjectSucceeded:
- case ForkedProjectFailed:
- getLock(executionEvent).unlock();
- break;
- }
- }
- }
-
-}