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