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