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