blob: 52a7ce02158c7af391cd3cf092b45a3b1c236457 [file] [log] [blame]
/*
* 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;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
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.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.maven.api.services.MessageBuilderFactory;
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.internal.MultilineMessageHelper;
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.MojoExecutionRunner;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.MojosExecutionStrategy;
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.eclipse.aether.SessionData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* Executes an individual mojo
* </p>
* <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
*
* @since 3.0
*/
@Named
@Singleton
public class MojoExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(MojoExecutor.class);
private final BuildPluginManager pluginManager;
private final MavenPluginManager mavenPluginManager;
private final LifecycleDependencyResolver lifeCycleDependencyResolver;
private final ExecutionEventCatapult eventCatapult;
private final OwnerReentrantReadWriteLock aggregatorLock = new OwnerReentrantReadWriteLock();
private final Provider<MojosExecutionStrategy> mojosExecutionStrategy;
private final MessageBuilderFactory messageBuilderFactory;
private final Map<Thread, MojoDescriptor> mojos = new ConcurrentHashMap<>();
@Inject
public MojoExecutor(
BuildPluginManager pluginManager,
MavenPluginManager mavenPluginManager,
LifecycleDependencyResolver lifeCycleDependencyResolver,
ExecutionEventCatapult eventCatapult,
Provider<MojosExecutionStrategy> mojosExecutionStrategy,
MessageBuilderFactory messageBuilderFactory) {
this.pluginManager = pluginManager;
this.mavenPluginManager = mavenPluginManager;
this.lifeCycleDependencyResolver = lifeCycleDependencyResolver;
this.eventCatapult = eventCatapult;
this.mojosExecutionStrategy = mojosExecutionStrategy;
this.messageBuilderFactory = messageBuilderFactory;
}
public DependencyContext newDependencyContext(MavenSession session, List<MojoExecution> mojoExecutions) {
Set<String> scopesToCollect = new TreeSet<>();
Set<String> scopesToResolve = new TreeSet<>();
collectDependencyRequirements(scopesToResolve, scopesToCollect, mojoExecutions);
return new DependencyContext(session.getCurrentProject(), scopesToCollect, scopesToResolve);
}
private void collectDependencyRequirements(
Set<String> scopesToResolve, Set<String> scopesToCollect, Collection<MojoExecution> mojoExecutions) {
for (MojoExecution mojoExecution : mojoExecutions) {
MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
scopesToResolve.addAll(toScopes(mojoDescriptor.getDependencyResolutionRequired()));
scopesToCollect.addAll(toScopes(mojoDescriptor.getDependencyCollectionRequired()));
}
}
private Collection<String> toScopes(String classpath) {
Collection<String> scopes = Collections.emptyList();
if (classpath != null && !classpath.isEmpty()) {
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(
final MavenSession session, final List<MojoExecution> mojoExecutions, final ProjectIndex projectIndex)
throws LifecycleExecutionException {
final DependencyContext dependencyContext = newDependencyContext(session, mojoExecutions);
final PhaseRecorder phaseRecorder = new PhaseRecorder(session.getCurrentProject());
mojosExecutionStrategy.get().execute(mojoExecutions, session, new MojoExecutionRunner() {
@Override
public void run(MojoExecution mojoExecution) throws LifecycleExecutionException {
MojoExecutor.this.execute(session, mojoExecution, projectIndex, dependencyContext, phaseRecorder);
}
});
}
private 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.checkPrerequisites(mojoDescriptor.getPluginDescriptor());
} catch (PluginIncompatibleException e) {
throw new LifecycleExecutionException(messageBuilderFactory, 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(messageBuilderFactory, 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(
messageBuilderFactory, mojoExecution, session.getCurrentProject(), cause);
} else {
eventCatapult.fire(ExecutionEvent.Type.MojoSkipped, session, mojoExecution);
return;
}
}
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 class ProjectLock implements AutoCloseable {
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;
}
}
@Override
public void close() {
// release the lock in the reverse order of the acquisition
if (acquiredProjectLock != null) {
acquiredProjectLock.unlock();
}
if (acquiredAggregatorLock != null) {
acquiredAggregatorLock.unlock();
}
mojos.remove(Thread.currentThread());
}
@SuppressWarnings({"unchecked", "rawtypes"})
private OwnerReentrantLock getProjectLock(MavenSession session) {
SessionData data = session.getRepositorySession().getData();
Map<MavenProject, OwnerReentrantLock> locks =
(Map) data.computeIfAbsent(ProjectLock.class, ConcurrentHashMap::new);
return locks.computeIfAbsent(session.getCurrentProject(), p -> new OwnerReentrantLock());
}
}
static class OwnerReentrantLock extends ReentrantLock {
@Override
public Thread getOwner() {
return super.getOwner();
}
}
static class OwnerReentrantReadWriteLock extends ReentrantReadWriteLock {
@Override
public Thread getOwner() {
return super.getOwner();
}
}
private static void warn(String msg) {
for (String s : MultilineMessageHelper.format(msg)) {
LOGGER.warn(s);
}
}
private void doExecute(
MavenSession session,
MojoExecution mojoExecution,
ProjectIndex projectIndex,
DependencyContext dependencyContext)
throws LifecycleExecutionException {
MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
List<MavenProject> forkedProjects = executeForkedExecutions(mojoExecution, session, projectIndex);
ensureDependenciesAreResolved(mojoDescriptor, session, dependencyContext);
try (ProjectLock lock = new ProjectLock(session, mojoDescriptor)) {
doExecute2(session, mojoExecution);
} finally {
for (MavenProject forkedProject : forkedProjects) {
forkedProject.setExecutionProject(null);
}
}
}
private void doExecute2(MavenSession session, MojoExecution mojoExecution) throws LifecycleExecutionException {
eventCatapult.fire(ExecutionEvent.Type.MojoStarted, session, mojoExecution);
try {
try {
pluginManager.executeMojo(session, mojoExecution);
} catch (MojoFailureException
| PluginManagerException
| PluginConfigurationException
| MojoExecutionException e) {
throw new LifecycleExecutionException(
messageBuilderFactory, mojoExecution, session.getCurrentProject(), e);
}
eventCatapult.fire(ExecutionEvent.Type.MojoSucceeded, session, mojoExecution);
} catch (LifecycleExecutionException e) {
eventCatapult.fire(ExecutionEvent.Type.MojoFailed, session, mojoExecution, e);
throw e;
}
}
public void ensureDependenciesAreResolved(
MojoDescriptor mojoDescriptor, MavenSession session, DependencyContext dependencyContext)
throws LifecycleExecutionException {
MavenProject project = dependencyContext.getProject();
boolean aggregating = mojoDescriptor.isAggregator();
if (dependencyContext.isResolutionRequiredForCurrentProject()) {
Collection<String> scopesToCollect = dependencyContext.getScopesToCollectForCurrentProject();
Collection<String> scopesToResolve = dependencyContext.getScopesToResolveForCurrentProject();
lifeCycleDependencyResolver.resolveProjectDependencies(
project, scopesToCollect, scopesToResolve, session, aggregating, Collections.emptySet());
dependencyContext.synchronizeWithProjectState();
}
if (aggregating) {
Collection<String> scopesToCollect = toScopes(mojoDescriptor.getDependencyCollectionRequired());
Collection<String> 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<MavenProject> 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<String> scopes = new ArrayList<>(2);
if (scopeToCollect != null && !scopeToCollect.isEmpty()) {
scopes.add(scopeToCollect);
}
if (scopeToResolve != null && !scopeToResolve.isEmpty()) {
scopes.add(scopeToResolve);
}
if (scopes.isEmpty()) {
return null;
} else {
return new CumulativeScopeArtifactFilter(scopes);
}
}
public List<MavenProject> executeForkedExecutions(
MojoExecution mojoExecution, MavenSession session, ProjectIndex projectIndex)
throws LifecycleExecutionException {
List<MavenProject> forkedProjects = Collections.emptyList();
Map<String, List<MojoExecution>> 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<String, List<MojoExecution>> 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<MojoExecution> 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;
}
}