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