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.artifact.Artifact;
40  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
41  import org.apache.maven.artifact.resolver.filter.CumulativeScopeArtifactFilter;
42  import org.apache.maven.execution.ExecutionEvent;
43  import org.apache.maven.execution.MavenSession;
44  import org.apache.maven.internal.MultilineMessageHelper;
45  import org.apache.maven.lifecycle.LifecycleExecutionException;
46  import org.apache.maven.lifecycle.MissingProjectException;
47  import org.apache.maven.plugin.BuildPluginManager;
48  import org.apache.maven.plugin.MavenPluginManager;
49  import org.apache.maven.plugin.MojoExecution;
50  import org.apache.maven.plugin.MojoExecutionException;
51  import org.apache.maven.plugin.MojoExecutionRunner;
52  import org.apache.maven.plugin.MojoFailureException;
53  import org.apache.maven.plugin.MojosExecutionStrategy;
54  import org.apache.maven.plugin.PluginConfigurationException;
55  import org.apache.maven.plugin.PluginIncompatibleException;
56  import org.apache.maven.plugin.PluginManagerException;
57  import org.apache.maven.plugin.descriptor.MojoDescriptor;
58  import org.apache.maven.project.MavenProject;
59  import org.codehaus.plexus.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  @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 Map<Thread, MojoDescriptor> mojos = new ConcurrentHashMap<>();
91  
92      @Inject
93      public MojoExecutor(
94              BuildPluginManager pluginManager,
95              MavenPluginManager mavenPluginManager,
96              LifecycleDependencyResolver lifeCycleDependencyResolver,
97              ExecutionEventCatapult eventCatapult,
98              Provider<MojosExecutionStrategy> mojosExecutionStrategy) {
99          this.pluginManager = pluginManager;
100         this.mavenPluginManager = mavenPluginManager;
101         this.lifeCycleDependencyResolver = lifeCycleDependencyResolver;
102         this.eventCatapult = eventCatapult;
103         this.mojosExecutionStrategy = mojosExecutionStrategy;
104     }
105 
106     public DependencyContext newDependencyContext(MavenSession session, List<MojoExecution> mojoExecutions) {
107         Set<String> scopesToCollect = new TreeSet<>();
108         Set<String> scopesToResolve = new TreeSet<>();
109 
110         collectDependencyRequirements(scopesToResolve, scopesToCollect, mojoExecutions);
111 
112         return new DependencyContext(session.getCurrentProject(), scopesToCollect, scopesToResolve);
113     }
114 
115     private void collectDependencyRequirements(
116             Set<String> scopesToResolve, Set<String> scopesToCollect, Collection<MojoExecution> mojoExecutions) {
117         for (MojoExecution mojoExecution : mojoExecutions) {
118             MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
119 
120             scopesToResolve.addAll(toScopes(mojoDescriptor.getDependencyResolutionRequired()));
121 
122             scopesToCollect.addAll(toScopes(mojoDescriptor.getDependencyCollectionRequired()));
123         }
124     }
125 
126     private Collection<String> toScopes(String classpath) {
127         Collection<String> scopes = Collections.emptyList();
128 
129         if (StringUtils.isNotEmpty(classpath)) {
130             if (Artifact.SCOPE_COMPILE.equals(classpath)) {
131                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED);
132             } else if (Artifact.SCOPE_RUNTIME.equals(classpath)) {
133                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME);
134             } else if (Artifact.SCOPE_COMPILE_PLUS_RUNTIME.equals(classpath)) {
135                 scopes = Arrays.asList(
136                         Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED, Artifact.SCOPE_RUNTIME);
137             } else if (Artifact.SCOPE_RUNTIME_PLUS_SYSTEM.equals(classpath)) {
138                 scopes = Arrays.asList(Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_RUNTIME);
139             } else if (Artifact.SCOPE_TEST.equals(classpath)) {
140                 scopes = Arrays.asList(
141                         Artifact.SCOPE_COMPILE,
142                         Artifact.SCOPE_SYSTEM,
143                         Artifact.SCOPE_PROVIDED,
144                         Artifact.SCOPE_RUNTIME,
145                         Artifact.SCOPE_TEST);
146             }
147         }
148         return Collections.unmodifiableCollection(scopes);
149     }
150 
151     public void execute(
152             final MavenSession session, final List<MojoExecution> mojoExecutions, final ProjectIndex projectIndex)
153             throws LifecycleExecutionException {
154 
155         final DependencyContext dependencyContext = newDependencyContext(session, mojoExecutions);
156 
157         final PhaseRecorder phaseRecorder = new PhaseRecorder(session.getCurrentProject());
158 
159         mojosExecutionStrategy.get().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.checkPrerequisites(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             // TODO: when resolver 1.7.3 is released, the code below should be changed to
278             // TODO: Map<MavenProject, Lock> locks = ( Map ) ((Map) data).computeIfAbsent(
279             // TODO:         ProjectLock.class, l -> new ConcurrentHashMap<>() );
280             Map<MavenProject, OwnerReentrantLock> locks = (Map) data.get(ProjectLock.class);
281             // initialize the value if not already done (in case of a concurrent access) to the method
282             if (locks == null) {
283                 // the call to data.set(k, null, v) is effectively a call to data.putIfAbsent(k, v)
284                 data.set(ProjectLock.class, null, new ConcurrentHashMap<>());
285                 locks = (Map) data.get(ProjectLock.class);
286             }
287             return locks.computeIfAbsent(session.getCurrentProject(), p -> new OwnerReentrantLock());
288         }
289     }
290 
291     static class OwnerReentrantLock extends ReentrantLock {
292         @Override
293         public Thread getOwner() {
294             return super.getOwner();
295         }
296     }
297 
298     static class OwnerReentrantReadWriteLock extends ReentrantReadWriteLock {
299         @Override
300         public Thread getOwner() {
301             return super.getOwner();
302         }
303     }
304 
305     private static void warn(String msg) {
306         for (String s : MultilineMessageHelper.format(msg)) {
307             LOGGER.warn(s);
308         }
309     }
310 
311     private void doExecute(
312             MavenSession session,
313             MojoExecution mojoExecution,
314             ProjectIndex projectIndex,
315             DependencyContext dependencyContext)
316             throws LifecycleExecutionException {
317         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
318 
319         List<MavenProject> forkedProjects = executeForkedExecutions(mojoExecution, session, projectIndex);
320 
321         ensureDependenciesAreResolved(mojoDescriptor, session, dependencyContext);
322 
323         try (ProjectLock lock = new ProjectLock(session, mojoDescriptor)) {
324             doExecute2(session, mojoExecution);
325         } finally {
326             for (MavenProject forkedProject : forkedProjects) {
327                 forkedProject.setExecutionProject(null);
328             }
329         }
330     }
331 
332     private void doExecute2(MavenSession session, MojoExecution mojoExecution) throws LifecycleExecutionException {
333         eventCatapult.fire(ExecutionEvent.Type.MojoStarted, session, mojoExecution);
334         try {
335             try {
336                 pluginManager.executeMojo(session, mojoExecution);
337             } catch (MojoFailureException
338                     | PluginManagerException
339                     | PluginConfigurationException
340                     | MojoExecutionException e) {
341                 throw new LifecycleExecutionException(mojoExecution, session.getCurrentProject(), e);
342             }
343 
344             eventCatapult.fire(ExecutionEvent.Type.MojoSucceeded, session, mojoExecution);
345         } catch (LifecycleExecutionException e) {
346             eventCatapult.fire(ExecutionEvent.Type.MojoFailed, session, mojoExecution, e);
347 
348             throw e;
349         }
350     }
351 
352     public void ensureDependenciesAreResolved(
353             MojoDescriptor mojoDescriptor, MavenSession session, DependencyContext dependencyContext)
354             throws LifecycleExecutionException {
355 
356         MavenProject project = dependencyContext.getProject();
357         boolean aggregating = mojoDescriptor.isAggregator();
358 
359         if (dependencyContext.isResolutionRequiredForCurrentProject()) {
360             Collection<String> scopesToCollect = dependencyContext.getScopesToCollectForCurrentProject();
361             Collection<String> scopesToResolve = dependencyContext.getScopesToResolveForCurrentProject();
362 
363             lifeCycleDependencyResolver.resolveProjectDependencies(
364                     project, scopesToCollect, scopesToResolve, session, aggregating, Collections.emptySet());
365 
366             dependencyContext.synchronizeWithProjectState();
367         }
368 
369         if (aggregating) {
370             Collection<String> scopesToCollect = toScopes(mojoDescriptor.getDependencyCollectionRequired());
371             Collection<String> scopesToResolve = toScopes(mojoDescriptor.getDependencyResolutionRequired());
372 
373             if (dependencyContext.isResolutionRequiredForAggregatedProjects(scopesToCollect, scopesToResolve)) {
374                 for (MavenProject aggregatedProject : session.getProjects()) {
375                     if (aggregatedProject != project) {
376                         lifeCycleDependencyResolver.resolveProjectDependencies(
377                                 aggregatedProject,
378                                 scopesToCollect,
379                                 scopesToResolve,
380                                 session,
381                                 aggregating,
382                                 Collections.emptySet());
383                     }
384                 }
385             }
386         }
387 
388         ArtifactFilter artifactFilter = getArtifactFilter(mojoDescriptor);
389         List<MavenProject> projectsToResolve = LifecycleDependencyResolver.getProjects(
390                 session.getCurrentProject(), session, mojoDescriptor.isAggregator());
391         for (MavenProject projectToResolve : projectsToResolve) {
392             projectToResolve.setArtifactFilter(artifactFilter);
393         }
394     }
395 
396     private ArtifactFilter getArtifactFilter(MojoDescriptor mojoDescriptor) {
397         String scopeToResolve = mojoDescriptor.getDependencyResolutionRequired();
398         String scopeToCollect = mojoDescriptor.getDependencyCollectionRequired();
399 
400         List<String> scopes = new ArrayList<>(2);
401         if (StringUtils.isNotEmpty(scopeToCollect)) {
402             scopes.add(scopeToCollect);
403         }
404         if (StringUtils.isNotEmpty(scopeToResolve)) {
405             scopes.add(scopeToResolve);
406         }
407 
408         if (scopes.isEmpty()) {
409             return null;
410         } else {
411             return new CumulativeScopeArtifactFilter(scopes);
412         }
413     }
414 
415     public List<MavenProject> executeForkedExecutions(
416             MojoExecution mojoExecution, MavenSession session, ProjectIndex projectIndex)
417             throws LifecycleExecutionException {
418         List<MavenProject> forkedProjects = Collections.emptyList();
419 
420         Map<String, List<MojoExecution>> forkedExecutions = mojoExecution.getForkedExecutions();
421 
422         if (!forkedExecutions.isEmpty()) {
423             eventCatapult.fire(ExecutionEvent.Type.ForkStarted, session, mojoExecution);
424 
425             MavenProject project = session.getCurrentProject();
426 
427             forkedProjects = new ArrayList<>(forkedExecutions.size());
428 
429             try {
430                 for (Map.Entry<String, List<MojoExecution>> fork : forkedExecutions.entrySet()) {
431                     String projectId = fork.getKey();
432 
433                     int index = projectIndex.getIndices().get(projectId);
434 
435                     MavenProject forkedProject = projectIndex.getProjects().get(projectId);
436 
437                     forkedProjects.add(forkedProject);
438 
439                     MavenProject executedProject = forkedProject.clone();
440 
441                     forkedProject.setExecutionProject(executedProject);
442 
443                     List<MojoExecution> mojoExecutions = fork.getValue();
444 
445                     if (mojoExecutions.isEmpty()) {
446                         continue;
447                     }
448 
449                     try {
450                         session.setCurrentProject(executedProject);
451                         session.getProjects().set(index, executedProject);
452                         projectIndex.getProjects().put(projectId, executedProject);
453 
454                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectStarted, session, mojoExecution);
455 
456                         execute(session, mojoExecutions, projectIndex);
457 
458                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectSucceeded, session, mojoExecution);
459                     } catch (LifecycleExecutionException e) {
460                         eventCatapult.fire(ExecutionEvent.Type.ForkedProjectFailed, session, mojoExecution, e);
461 
462                         throw e;
463                     } finally {
464                         projectIndex.getProjects().put(projectId, forkedProject);
465                         session.getProjects().set(index, forkedProject);
466                         session.setCurrentProject(project);
467                     }
468                 }
469 
470                 eventCatapult.fire(ExecutionEvent.Type.ForkSucceeded, session, mojoExecution);
471             } catch (LifecycleExecutionException e) {
472                 eventCatapult.fire(ExecutionEvent.Type.ForkFailed, session, mojoExecution, e);
473 
474                 throw e;
475             }
476         }
477 
478         return forkedProjects;
479     }
480 }