View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.lifecycle.internal;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.TreeSet;
33  import java.util.concurrent.ConcurrentHashMap;
34  import java.util.concurrent.locks.Lock;
35  import java.util.concurrent.locks.ReentrantLock;
36  import java.util.concurrent.locks.ReentrantReadWriteLock;
37  
38  import org.apache.maven.artifact.Artifact;
39  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
40  import org.apache.maven.artifact.resolver.filter.CumulativeScopeArtifactFilter;
41  import org.apache.maven.execution.ExecutionEvent;
42  import org.apache.maven.execution.MavenSession;
43  import org.apache.maven.internal.MultilineMessageHelper;
44  import org.apache.maven.lifecycle.LifecycleExecutionException;
45  import org.apache.maven.lifecycle.MissingProjectException;
46  import org.apache.maven.message.MessageBuilderFactory;
47  import org.apache.maven.plugin.BuildPluginManager;
48  import org.apache.maven.plugin.MavenPluginManager;
49  import org.apache.maven.plugin.MojoExecution;
50  import org.apache.maven.plugin.MojoExecutionException;
51  import org.apache.maven.plugin.MojoExecutionRunner;
52  import org.apache.maven.plugin.MojoFailureException;
53  import org.apache.maven.plugin.MojosExecutionStrategy;
54  import org.apache.maven.plugin.PluginConfigurationException;
55  import org.apache.maven.plugin.PluginIncompatibleException;
56  import org.apache.maven.plugin.PluginManagerException;
57  import org.apache.maven.plugin.descriptor.MojoDescriptor;
58  import org.apache.maven.project.MavenProject;
59  import org.codehaus.plexus.PlexusContainer;
60  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
61  import org.codehaus.plexus.util.StringUtils;
62  import org.eclipse.aether.SessionData;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  /**
67   * <p>
68   * Executes an individual mojo
69   * </p>
70   * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
71   *
72   * @author Jason van Zyl
73   * @author Benjamin Bentmann
74   * @author Kristian Rosenvold
75   * @since 3.0
76   */
77  @Singleton
78  @Named
79  public class MojoExecutor {
80  
81      private static final Logger LOGGER = LoggerFactory.getLogger(MojoExecutor.class);
82  
83      @Inject
84      private BuildPluginManager pluginManager;
85  
86      @Inject
87      private MavenPluginManager mavenPluginManager;
88  
89      @Inject
90      private LifecycleDependencyResolver lifeCycleDependencyResolver;
91  
92      @Inject
93      private ExecutionEventCatapult eventCatapult;
94  
95      @Inject
96      private MessageBuilderFactory messageBuilderFactory;
97  
98      private final OwnerReentrantReadWriteLock aggregatorLock = new OwnerReentrantReadWriteLock();
99  
100     @Inject
101     private PlexusContainer container;
102 
103     private final Map<Thread, MojoDescriptor> mojos = new ConcurrentHashMap<>();
104 
105     public MojoExecutor() {}
106 
107     public DependencyContext newDependencyContext(MavenSession session, List<MojoExecution> mojoExecutions) {
108         Set<String> scopesToCollect = new TreeSet<>();
109         Set<String> scopesToResolve = new TreeSet<>();
110 
111         collectDependencyRequirements(scopesToResolve, scopesToCollect, mojoExecutions);
112 
113         return new DependencyContext(session.getCurrentProject(), scopesToCollect, scopesToResolve);
114     }
115 
116     private void collectDependencyRequirements(
117             Set<String> scopesToResolve, Set<String> scopesToCollect, Collection<MojoExecution> mojoExecutions) {
118         for (MojoExecution mojoExecution : mojoExecutions) {
119             MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
120 
121             scopesToResolve.addAll(toScopes(mojoDescriptor.getDependencyResolutionRequired()));
122 
123             scopesToCollect.addAll(toScopes(mojoDescriptor.getDependencyCollectionRequired()));
124         }
125     }
126 
127     private Collection<String> toScopes(String classpath) {
128         Collection<String> scopes = Collections.emptyList();
129 
130         if (StringUtils.isNotEmpty(classpath)) {
131             if (Artifact.SCOPE_COMPILE.equals(classpath)) {
132                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED);
133             } else if (Artifact.SCOPE_RUNTIME.equals(classpath)) {
134                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME);
135             } else if (Artifact.SCOPE_COMPILE_PLUS_RUNTIME.equals(classpath)) {
136                 scopes = Arrays.asList(
137                         Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED, Artifact.SCOPE_RUNTIME);
138             } else if (Artifact.SCOPE_RUNTIME_PLUS_SYSTEM.equals(classpath)) {
139                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_RUNTIME);
140             } else if (Artifact.SCOPE_TEST.equals(classpath)) {
141                 scopes = Arrays.asList(
142                         Artifact.SCOPE_COMPILE,
143                         Artifact.SCOPE_SYSTEM,
144                         Artifact.SCOPE_PROVIDED,
145                         Artifact.SCOPE_RUNTIME,
146                         Artifact.SCOPE_TEST);
147             }
148         }
149         return Collections.unmodifiableCollection(scopes);
150     }
151 
152     public void execute(
153             final MavenSession session, final List<MojoExecution> mojoExecutions, final ProjectIndex projectIndex)
154             throws LifecycleExecutionException {
155 
156         final DependencyContext dependencyContext = newDependencyContext(session, mojoExecutions);
157 
158         final PhaseRecorder phaseRecorder = new PhaseRecorder(session.getCurrentProject());
159 
160         MojosExecutionStrategy strategy;
161         try {
162             strategy = container.lookup(MojosExecutionStrategy.class);
163         } catch (ComponentLookupException e) {
164             throw new IllegalStateException("Unable to lookup MojosExecutionStrategy", e);
165         }
166         strategy.execute(mojoExecutions, session, new MojoExecutionRunner() {
167             @Override
168             public void run(MojoExecution mojoExecution) throws LifecycleExecutionException {
169                 MojoExecutor.this.execute(session, mojoExecution, projectIndex, dependencyContext, phaseRecorder);
170             }
171         });
172     }
173 
174     private void execute(
175             MavenSession session,
176             MojoExecution mojoExecution,
177             ProjectIndex projectIndex,
178             DependencyContext dependencyContext,
179             PhaseRecorder phaseRecorder)
180             throws LifecycleExecutionException {
181         execute(session, mojoExecution, projectIndex, dependencyContext);
182         phaseRecorder.observeExecution(mojoExecution);
183     }
184 
185     private void execute(
186             MavenSession session,
187             MojoExecution mojoExecution,
188             ProjectIndex projectIndex,
189             DependencyContext dependencyContext)
190             throws LifecycleExecutionException {
191         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
192 
193         try {
194             mavenPluginManager.checkPrerequisites(mojoDescriptor.getPluginDescriptor());
195         } catch (PluginIncompatibleException e) {
196             throw new LifecycleExecutionException(messageBuilderFactory, mojoExecution, session.getCurrentProject(), e);
197         }
198 
199         if (mojoDescriptor.isProjectRequired() && !session.getRequest().isProjectPresent()) {
200             Throwable cause = new MissingProjectException(
201                     "Goal requires a project to execute" + " but there is no POM in this directory ("
202                             + session.getExecutionRootDirectory() + ")."
203                             + " Please verify you invoked Maven from the correct directory.");
204             throw new LifecycleExecutionException(messageBuilderFactory, mojoExecution, null, cause);
205         }
206 
207         if (mojoDescriptor.isOnlineRequired() && session.isOffline()) {
208             if (MojoExecution.Source.CLI.equals(mojoExecution.getSource())) {
209                 Throwable cause = new IllegalStateException(
210                         "Goal requires online mode for execution" + " but Maven is currently offline.");
211                 throw new LifecycleExecutionException(
212                         messageBuilderFactory, mojoExecution, session.getCurrentProject(), cause);
213             } else {
214                 eventCatapult.fire(ExecutionEvent.Type.MojoSkipped, session, mojoExecution);
215 
216                 return;
217             }
218         }
219 
220         doExecute(session, mojoExecution, projectIndex, dependencyContext);
221     }
222 
223     /**
224      * Aggregating mojo executions (possibly) modify all MavenProjects, including those that are currently in use
225      * by concurrently running mojo executions. To prevent race conditions, an aggregating execution will block
226      * all other executions until finished.
227      * We also lock on a given project to forbid a forked lifecycle to be executed concurrently with the project.
228      * TODO: ideally, the builder should take care of the ordering in a smarter way
229      * TODO: and concurrency issues fixed with MNG-7157
230      */
231     private class ProjectLock implements AutoCloseable {
232         final Lock acquiredAggregatorLock;
233         final OwnerReentrantLock acquiredProjectLock;
234 
235         ProjectLock(MavenSession session, MojoDescriptor mojoDescriptor) {
236             mojos.put(Thread.currentThread(), mojoDescriptor);
237             if (session.getRequest().getDegreeOfConcurrency() > 1) {
238                 boolean aggregator = mojoDescriptor.isAggregator();
239                 acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : aggregatorLock.readLock();
240                 acquiredProjectLock = getProjectLock(session);
241                 if (!acquiredAggregatorLock.tryLock()) {
242                     Thread owner = aggregatorLock.getOwner();
243                     MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null;
244                     String str = ownerMojo != null ? " The " + ownerMojo.getId() : "An";
245                     String msg = str + " aggregator mojo is already being executed "
246                             + "in this parallel build, those kind of mojos require exclusive access to "
247                             + "reactor to prevent race conditions. This mojo execution will be blocked "
248                             + "until the aggregator mojo is done.";
249                     warn(msg);
250                     acquiredAggregatorLock.lock();
251                 }
252                 if (!acquiredProjectLock.tryLock()) {
253                     Thread owner = acquiredProjectLock.getOwner();
254                     MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null;
255                     String str = ownerMojo != null ? " The " + ownerMojo.getId() : "A";
256                     String msg = str + " mojo is already being executed "
257                             + "on the project " + session.getCurrentProject().getGroupId()
258                             + ":" + session.getCurrentProject().getArtifactId() + ". "
259                             + "This mojo execution will be blocked "
260                             + "until the mojo is done.";
261                     warn(msg);
262                     acquiredProjectLock.lock();
263                 }
264             } else {
265                 acquiredAggregatorLock = null;
266                 acquiredProjectLock = null;
267             }
268         }
269 
270         @Override
271         public void close() {
272             // release the lock in the reverse order of the acquisition
273             if (acquiredProjectLock != null) {
274                 acquiredProjectLock.unlock();
275             }
276             if (acquiredAggregatorLock != null) {
277                 acquiredAggregatorLock.unlock();
278             }
279             mojos.remove(Thread.currentThread());
280         }
281 
282         @SuppressWarnings({"unchecked", "rawtypes"})
283         private OwnerReentrantLock getProjectLock(MavenSession session) {
284             SessionData data = session.getRepositorySession().getData();
285             Map<MavenProject, OwnerReentrantLock> locks =
286                     (Map) data.computeIfAbsent(ProjectLock.class, ConcurrentHashMap::new);
287             return locks.computeIfAbsent(session.getCurrentProject(), p -> new OwnerReentrantLock());
288         }
289     }
290 
291     static class OwnerReentrantLock extends ReentrantLock {
292         @Override
293         public Thread getOwner() {
294             return super.getOwner();
295         }
296     }
297 
298     static class OwnerReentrantReadWriteLock extends ReentrantReadWriteLock {
299         @Override
300         public Thread getOwner() {
301             return super.getOwner();
302         }
303     }
304 
305     private static void warn(String msg) {
306         for (String s : MultilineMessageHelper.format(msg)) {
307             LOGGER.warn(s);
308         }
309     }
310 
311     private void doExecute(
312             MavenSession session,
313             MojoExecution mojoExecution,
314             ProjectIndex projectIndex,
315             DependencyContext dependencyContext)
316             throws LifecycleExecutionException {
317         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
318 
319         List<MavenProject> forkedProjects = executeForkedExecutions(mojoExecution, session, projectIndex);
320 
321         try (ProjectLock lock = new ProjectLock(session, mojoDescriptor)) {
322             ensureDependenciesAreResolved(mojoDescriptor, session, dependencyContext);
323 
324             doExecute2(session, mojoExecution);
325         } finally {
326             for (MavenProject forkedProject : forkedProjects) {
327                 forkedProject.setExecutionProject(null);
328             }
329         }
330     }
331 
332     private void doExecute2(MavenSession session, MojoExecution mojoExecution) throws LifecycleExecutionException {
333         eventCatapult.fire(ExecutionEvent.Type.MojoStarted, session, mojoExecution);
334         try {
335             try {
336                 pluginManager.executeMojo(session, mojoExecution);
337             } catch (MojoFailureException
338                     | PluginManagerException
339                     | PluginConfigurationException
340                     | MojoExecutionException e) {
341                 throw new LifecycleExecutionException(
342                         messageBuilderFactory, mojoExecution, session.getCurrentProject(), e);
343             }
344 
345             eventCatapult.fire(ExecutionEvent.Type.MojoSucceeded, session, mojoExecution);
346         } catch (LifecycleExecutionException e) {
347             eventCatapult.fire(ExecutionEvent.Type.MojoFailed, session, mojoExecution, e);
348 
349             throw e;
350         }
351     }
352 
353     public void ensureDependenciesAreResolved(
354             MojoDescriptor mojoDescriptor, MavenSession session, DependencyContext dependencyContext)
355             throws LifecycleExecutionException {
356 
357         MavenProject project = dependencyContext.getProject();
358         boolean aggregating = mojoDescriptor.isAggregator();
359 
360         if (dependencyContext.isResolutionRequiredForCurrentProject()) {
361             Collection<String> scopesToCollect = dependencyContext.getScopesToCollectForCurrentProject();
362             Collection<String> scopesToResolve = dependencyContext.getScopesToResolveForCurrentProject();
363 
364             lifeCycleDependencyResolver.resolveProjectDependencies(
365                     project, scopesToCollect, scopesToResolve, session, aggregating, Collections.<Artifact>emptySet());
366 
367             dependencyContext.synchronizeWithProjectState();
368         }
369 
370         if (aggregating) {
371             Collection<String> scopesToCollect = toScopes(mojoDescriptor.getDependencyCollectionRequired());
372             Collection<String> scopesToResolve = toScopes(mojoDescriptor.getDependencyResolutionRequired());
373 
374             if (dependencyContext.isResolutionRequiredForAggregatedProjects(scopesToCollect, scopesToResolve)) {
375                 for (MavenProject aggregatedProject : session.getProjects()) {
376                     if (aggregatedProject != project) {
377                         lifeCycleDependencyResolver.resolveProjectDependencies(
378                                 aggregatedProject,
379                                 scopesToCollect,
380                                 scopesToResolve,
381                                 session,
382                                 aggregating,
383                                 Collections.<Artifact>emptySet());
384                     }
385                 }
386             }
387         }
388 
389         ArtifactFilter artifactFilter = getArtifactFilter(mojoDescriptor);
390         List<MavenProject> projectsToResolve = LifecycleDependencyResolver.getProjects(
391                 session.getCurrentProject(), session, mojoDescriptor.isAggregator());
392         for (MavenProject projectToResolve : projectsToResolve) {
393             projectToResolve.setArtifactFilter(artifactFilter);
394         }
395     }
396 
397     private ArtifactFilter getArtifactFilter(MojoDescriptor mojoDescriptor) {
398         String scopeToResolve = mojoDescriptor.getDependencyResolutionRequired();
399         String scopeToCollect = mojoDescriptor.getDependencyCollectionRequired();
400 
401         List<String> scopes = new ArrayList<>(2);
402         if (StringUtils.isNotEmpty(scopeToCollect)) {
403             scopes.add(scopeToCollect);
404         }
405         if (StringUtils.isNotEmpty(scopeToResolve)) {
406             scopes.add(scopeToResolve);
407         }
408 
409         if (scopes.isEmpty()) {
410             return null;
411         } else {
412             return new CumulativeScopeArtifactFilter(scopes);
413         }
414     }
415 
416     public List<MavenProject> executeForkedExecutions(
417             MojoExecution mojoExecution, MavenSession session, ProjectIndex projectIndex)
418             throws LifecycleExecutionException {
419         List<MavenProject> forkedProjects = Collections.emptyList();
420 
421         Map<String, List<MojoExecution>> forkedExecutions = mojoExecution.getForkedExecutions();
422 
423         if (!forkedExecutions.isEmpty()) {
424             eventCatapult.fire(ExecutionEvent.Type.ForkStarted, session, mojoExecution);
425 
426             MavenProject project = session.getCurrentProject();
427 
428             forkedProjects = new ArrayList<>(forkedExecutions.size());
429 
430             try {
431                 for (Map.Entry<String, List<MojoExecution>> fork : forkedExecutions.entrySet()) {
432                     String projectId = fork.getKey();
433 
434                     int index = projectIndex.getIndices().get(projectId);
435 
436                     MavenProject forkedProject = projectIndex.getProjects().get(projectId);
437 
438                     forkedProjects.add(forkedProject);
439 
440                     MavenProject executedProject = forkedProject.clone();
441 
442                     forkedProject.setExecutionProject(executedProject);
443 
444                     List<MojoExecution> mojoExecutions = fork.getValue();
445 
446                     if (mojoExecutions.isEmpty()) {
447                         continue;
448                     }
449 
450                     try {
451                         session.setCurrentProject(executedProject);
452                         session.getProjects().set(index, executedProject);
453                         projectIndex.getProjects().put(projectId, executedProject);
454 
455                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectStarted, session, mojoExecution);
456 
457                         execute(session, mojoExecutions, projectIndex);
458 
459                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectSucceeded, session, mojoExecution);
460                     } catch (LifecycleExecutionException e) {
461                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectFailed, session, mojoExecution, e);
462 
463                         throw e;
464                     } finally {
465                         projectIndex.getProjects().put(projectId, forkedProject);
466                         session.getProjects().set(index, forkedProject);
467                         session.setCurrentProject(project);
468                     }
469                 }
470 
471                 eventCatapult.fire(ExecutionEvent.Type.ForkSucceeded, session, mojoExecution);
472             } catch (LifecycleExecutionException e) {
473                 eventCatapult.fire(ExecutionEvent.Type.ForkFailed, session, mojoExecution, e);
474 
475                 throw e;
476             }
477         }
478 
479         return forkedProjects;
480     }
481 }