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.Provider;
24  import javax.inject.Singleton;
25  
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.TreeSet;
34  import java.util.concurrent.ConcurrentHashMap;
35  import java.util.concurrent.locks.Lock;
36  import java.util.concurrent.locks.ReentrantLock;
37  import java.util.concurrent.locks.ReentrantReadWriteLock;
38  
39  import org.apache.maven.api.services.MessageBuilderFactory;
40  import org.apache.maven.artifact.Artifact;
41  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
42  import org.apache.maven.artifact.resolver.filter.CumulativeScopeArtifactFilter;
43  import org.apache.maven.execution.ExecutionEvent;
44  import org.apache.maven.execution.MavenSession;
45  import org.apache.maven.internal.MultilineMessageHelper;
46  import org.apache.maven.lifecycle.LifecycleExecutionException;
47  import org.apache.maven.lifecycle.MissingProjectException;
48  import org.apache.maven.plugin.BuildPluginManager;
49  import org.apache.maven.plugin.MavenPluginManager;
50  import org.apache.maven.plugin.MojoExecution;
51  import org.apache.maven.plugin.MojoExecutionException;
52  import org.apache.maven.plugin.MojoExecutionRunner;
53  import org.apache.maven.plugin.MojoFailureException;
54  import org.apache.maven.plugin.MojosExecutionStrategy;
55  import org.apache.maven.plugin.PluginConfigurationException;
56  import org.apache.maven.plugin.PluginIncompatibleException;
57  import org.apache.maven.plugin.PluginManagerException;
58  import org.apache.maven.plugin.descriptor.MojoDescriptor;
59  import org.apache.maven.project.MavenProject;
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  @Named
76  @Singleton
77  public class MojoExecutor {
78  
79      private static final Logger LOGGER = LoggerFactory.getLogger(MojoExecutor.class);
80  
81      private final BuildPluginManager pluginManager;
82      private final MavenPluginManager mavenPluginManager;
83      private final LifecycleDependencyResolver lifeCycleDependencyResolver;
84      private final ExecutionEventCatapult eventCatapult;
85  
86      private final OwnerReentrantReadWriteLock aggregatorLock = new OwnerReentrantReadWriteLock();
87  
88      private final Provider<MojosExecutionStrategy> mojosExecutionStrategy;
89  
90      private final MessageBuilderFactory messageBuilderFactory;
91  
92      private final Map<Thread, MojoDescriptor> mojos = new ConcurrentHashMap<>();
93  
94      @Inject
95      public MojoExecutor(
96              BuildPluginManager pluginManager,
97              MavenPluginManager mavenPluginManager,
98              LifecycleDependencyResolver lifeCycleDependencyResolver,
99              ExecutionEventCatapult eventCatapult,
100             Provider<MojosExecutionStrategy> mojosExecutionStrategy,
101             MessageBuilderFactory messageBuilderFactory) {
102         this.pluginManager = pluginManager;
103         this.mavenPluginManager = mavenPluginManager;
104         this.lifeCycleDependencyResolver = lifeCycleDependencyResolver;
105         this.eventCatapult = eventCatapult;
106         this.mojosExecutionStrategy = mojosExecutionStrategy;
107         this.messageBuilderFactory = messageBuilderFactory;
108     }
109 
110     public DependencyContext newDependencyContext(MavenSession session, List<MojoExecution> mojoExecutions) {
111         Set<String> scopesToCollect = new TreeSet<>();
112         Set<String> scopesToResolve = new TreeSet<>();
113 
114         collectDependencyRequirements(scopesToResolve, scopesToCollect, mojoExecutions);
115 
116         return new DependencyContext(session.getCurrentProject(), scopesToCollect, scopesToResolve);
117     }
118 
119     private void collectDependencyRequirements(
120             Set<String> scopesToResolve, Set<String> scopesToCollect, Collection<MojoExecution> mojoExecutions) {
121         for (MojoExecution mojoExecution : mojoExecutions) {
122             MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
123 
124             scopesToResolve.addAll(toScopes(mojoDescriptor.getDependencyResolutionRequired()));
125 
126             scopesToCollect.addAll(toScopes(mojoDescriptor.getDependencyCollectionRequired()));
127         }
128     }
129 
130     private Collection<String> toScopes(String classpath) {
131         Collection<String> scopes = Collections.emptyList();
132 
133         if (classpath != null && !classpath.isEmpty()) {
134             if (Artifact.SCOPE_COMPILE.equals(classpath)) {
135                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED);
136             } else if (Artifact.SCOPE_RUNTIME.equals(classpath)) {
137                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME);
138             } else if (Artifact.SCOPE_COMPILE_PLUS_RUNTIME.equals(classpath)) {
139                 scopes = Arrays.asList(
140                         Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED, Artifact.SCOPE_RUNTIME);
141             } else if (Artifact.SCOPE_RUNTIME_PLUS_SYSTEM.equals(classpath)) {
142                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_RUNTIME);
143             } else if (Artifact.SCOPE_TEST.equals(classpath)) {
144                 scopes = Arrays.asList(
145                         Artifact.SCOPE_COMPILE,
146                         Artifact.SCOPE_SYSTEM,
147                         Artifact.SCOPE_PROVIDED,
148                         Artifact.SCOPE_RUNTIME,
149                         Artifact.SCOPE_TEST);
150             }
151         }
152         return Collections.unmodifiableCollection(scopes);
153     }
154 
155     public void execute(
156             final MavenSession session, final List<MojoExecution> mojoExecutions, final ProjectIndex projectIndex)
157             throws LifecycleExecutionException {
158 
159         final DependencyContext dependencyContext = newDependencyContext(session, mojoExecutions);
160 
161         final PhaseRecorder phaseRecorder = new PhaseRecorder(session.getCurrentProject());
162 
163         mojosExecutionStrategy.get().execute(mojoExecutions, session, new MojoExecutionRunner() {
164             @Override
165             public void run(MojoExecution mojoExecution) throws LifecycleExecutionException {
166                 MojoExecutor.this.execute(session, mojoExecution, projectIndex, dependencyContext, phaseRecorder);
167             }
168         });
169     }
170 
171     private void execute(
172             MavenSession session,
173             MojoExecution mojoExecution,
174             ProjectIndex projectIndex,
175             DependencyContext dependencyContext,
176             PhaseRecorder phaseRecorder)
177             throws LifecycleExecutionException {
178         execute(session, mojoExecution, projectIndex, dependencyContext);
179         phaseRecorder.observeExecution(mojoExecution);
180     }
181 
182     private void execute(
183             MavenSession session,
184             MojoExecution mojoExecution,
185             ProjectIndex projectIndex,
186             DependencyContext dependencyContext)
187             throws LifecycleExecutionException {
188         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
189 
190         try {
191             mavenPluginManager.checkPrerequisites(mojoDescriptor.getPluginDescriptor());
192         } catch (PluginIncompatibleException e) {
193             throw new LifecycleExecutionException(messageBuilderFactory, mojoExecution, session.getCurrentProject(), e);
194         }
195 
196         if (mojoDescriptor.isProjectRequired() && !session.getRequest().isProjectPresent()) {
197             Throwable cause = new MissingProjectException(
198                     "Goal requires a project to execute" + " but there is no POM in this directory ("
199                             + session.getExecutionRootDirectory() + ")."
200                             + " Please verify you invoked Maven from the correct directory.");
201             throw new LifecycleExecutionException(messageBuilderFactory, mojoExecution, null, cause);
202         }
203 
204         if (mojoDescriptor.isOnlineRequired() && session.isOffline()) {
205             if (MojoExecution.Source.CLI.equals(mojoExecution.getSource())) {
206                 Throwable cause = new IllegalStateException(
207                         "Goal requires online mode for execution" + " but Maven is currently offline.");
208                 throw new LifecycleExecutionException(
209                         messageBuilderFactory, mojoExecution, session.getCurrentProject(), cause);
210             } else {
211                 eventCatapult.fire(ExecutionEvent.Type.MojoSkipped, session, mojoExecution);
212 
213                 return;
214             }
215         }
216 
217         doExecute(session, mojoExecution, projectIndex, dependencyContext);
218     }
219 
220     /**
221      * Aggregating mojo executions (possibly) modify all MavenProjects, including those that are currently in use
222      * by concurrently running mojo executions. To prevent race conditions, an aggregating execution will block
223      * all other executions until finished.
224      * We also lock on a given project to forbid a forked lifecycle to be executed concurrently with the project.
225      * TODO: ideally, the builder should take care of the ordering in a smarter way
226      * TODO: and concurrency issues fixed with MNG-7157
227      */
228     private class ProjectLock implements AutoCloseable {
229         final Lock acquiredAggregatorLock;
230         final OwnerReentrantLock acquiredProjectLock;
231 
232         ProjectLock(MavenSession session, MojoDescriptor mojoDescriptor) {
233             mojos.put(Thread.currentThread(), mojoDescriptor);
234             if (session.getRequest().getDegreeOfConcurrency() > 1) {
235                 boolean aggregator = mojoDescriptor.isAggregator();
236                 acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : aggregatorLock.readLock();
237                 acquiredProjectLock = getProjectLock(session);
238                 if (!acquiredAggregatorLock.tryLock()) {
239                     Thread owner = aggregatorLock.getOwner();
240                     MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null;
241                     String str = ownerMojo != null ? " The " + ownerMojo.getId() : "An";
242                     String msg = str + " aggregator mojo is already being executed "
243                             + "in this parallel build, those kind of mojos require exclusive access to "
244                             + "reactor to prevent race conditions. This mojo execution will be blocked "
245                             + "until the aggregator mojo is done.";
246                     warn(msg);
247                     acquiredAggregatorLock.lock();
248                 }
249                 if (!acquiredProjectLock.tryLock()) {
250                     Thread owner = acquiredProjectLock.getOwner();
251                     MojoDescriptor ownerMojo = owner != null ? mojos.get(owner) : null;
252                     String str = ownerMojo != null ? " The " + ownerMojo.getId() : "A";
253                     String msg = str + " mojo is already being executed "
254                             + "on the project " + session.getCurrentProject().getGroupId()
255                             + ":" + session.getCurrentProject().getArtifactId() + ". "
256                             + "This mojo execution will be blocked "
257                             + "until the mojo is done.";
258                     warn(msg);
259                     acquiredProjectLock.lock();
260                 }
261             } else {
262                 acquiredAggregatorLock = null;
263                 acquiredProjectLock = null;
264             }
265         }
266 
267         @Override
268         public void close() {
269             // release the lock in the reverse order of the acquisition
270             if (acquiredProjectLock != null) {
271                 acquiredProjectLock.unlock();
272             }
273             if (acquiredAggregatorLock != null) {
274                 acquiredAggregatorLock.unlock();
275             }
276             mojos.remove(Thread.currentThread());
277         }
278 
279         @SuppressWarnings({"unchecked", "rawtypes"})
280         private OwnerReentrantLock getProjectLock(MavenSession session) {
281             SessionData data = session.getRepositorySession().getData();
282             Map<MavenProject, OwnerReentrantLock> locks =
283                     (Map) data.computeIfAbsent(ProjectLock.class, ConcurrentHashMap::new);
284             return locks.computeIfAbsent(session.getCurrentProject(), p -> new OwnerReentrantLock());
285         }
286     }
287 
288     static class OwnerReentrantLock extends ReentrantLock {
289         @Override
290         public Thread getOwner() {
291             return super.getOwner();
292         }
293     }
294 
295     static class OwnerReentrantReadWriteLock extends ReentrantReadWriteLock {
296         @Override
297         public Thread getOwner() {
298             return super.getOwner();
299         }
300     }
301 
302     private static void warn(String msg) {
303         for (String s : MultilineMessageHelper.format(msg)) {
304             LOGGER.warn(s);
305         }
306     }
307 
308     private void doExecute(
309             MavenSession session,
310             MojoExecution mojoExecution,
311             ProjectIndex projectIndex,
312             DependencyContext dependencyContext)
313             throws LifecycleExecutionException {
314         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
315 
316         List<MavenProject> forkedProjects = executeForkedExecutions(mojoExecution, session, projectIndex);
317 
318         ensureDependenciesAreResolved(mojoDescriptor, session, dependencyContext);
319 
320         try (ProjectLock lock = new ProjectLock(session, mojoDescriptor)) {
321             doExecute2(session, mojoExecution);
322         } finally {
323             for (MavenProject forkedProject : forkedProjects) {
324                 forkedProject.setExecutionProject(null);
325             }
326         }
327     }
328 
329     private void doExecute2(MavenSession session, MojoExecution mojoExecution) throws LifecycleExecutionException {
330         eventCatapult.fire(ExecutionEvent.Type.MojoStarted, session, mojoExecution);
331         try {
332             try {
333                 pluginManager.executeMojo(session, mojoExecution);
334             } catch (MojoFailureException
335                     | PluginManagerException
336                     | PluginConfigurationException
337                     | MojoExecutionException e) {
338                 throw new LifecycleExecutionException(
339                         messageBuilderFactory, 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 (scopeToCollect != null && !scopeToCollect.isEmpty()) {
400             scopes.add(scopeToCollect);
401         }
402         if (scopeToResolve != null && !scopeToResolve.isEmpty()) {
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 }