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.plugins.invoker;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.UncheckedIOException;
26  import java.util.Collections;
27  import java.util.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  import java.util.Optional;
32  import java.util.stream.Collectors;
33  
34  import org.apache.maven.RepositoryUtils;
35  import org.apache.maven.execution.MavenSession;
36  import org.apache.maven.model.DependencyManagement;
37  import org.apache.maven.model.Model;
38  import org.apache.maven.model.Parent;
39  import org.apache.maven.plugin.AbstractMojo;
40  import org.apache.maven.plugin.MojoExecutionException;
41  import org.apache.maven.plugins.annotations.LifecyclePhase;
42  import org.apache.maven.plugins.annotations.Mojo;
43  import org.apache.maven.plugins.annotations.Parameter;
44  import org.apache.maven.plugins.annotations.ResolutionScope;
45  import org.apache.maven.project.MavenProject;
46  import org.apache.maven.project.artifact.ProjectArtifact;
47  import org.eclipse.aether.DefaultRepositoryCache;
48  import org.eclipse.aether.DefaultRepositorySystemSession;
49  import org.eclipse.aether.RepositorySystem;
50  import org.eclipse.aether.RepositorySystemSession;
51  import org.eclipse.aether.artifact.Artifact;
52  import org.eclipse.aether.artifact.ArtifactType;
53  import org.eclipse.aether.artifact.ArtifactTypeRegistry;
54  import org.eclipse.aether.artifact.DefaultArtifact;
55  import org.eclipse.aether.collection.CollectRequest;
56  import org.eclipse.aether.graph.DefaultDependencyNode;
57  import org.eclipse.aether.graph.Dependency;
58  import org.eclipse.aether.graph.DependencyFilter;
59  import org.eclipse.aether.installation.InstallRequest;
60  import org.eclipse.aether.installation.InstallationException;
61  import org.eclipse.aether.repository.LocalRepository;
62  import org.eclipse.aether.repository.LocalRepositoryManager;
63  import org.eclipse.aether.repository.RemoteRepository;
64  import org.eclipse.aether.resolution.ArtifactRequest;
65  import org.eclipse.aether.resolution.ArtifactResolutionException;
66  import org.eclipse.aether.resolution.ArtifactResult;
67  import org.eclipse.aether.resolution.DependencyRequest;
68  import org.eclipse.aether.resolution.DependencyResolutionException;
69  import org.eclipse.aether.resolution.DependencyResult;
70  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
71  import org.eclipse.aether.util.artifact.JavaScopes;
72  import org.eclipse.aether.util.artifact.SubArtifact;
73  import org.eclipse.aether.util.filter.DependencyFilterUtils;
74  
75  /**
76   * Installs the project artifacts of the main build into the local repository as a preparation to run the sub projects.
77   * More precisely, all artifacts of the project itself, all its locally reachable parent POMs and all its dependencies
78   * from the reactor will be installed to the local repository.
79   *
80   * @author Paul Gier
81   * @author Benjamin Bentmann
82   * @since 1.2
83   */
84  @Mojo(
85          name = "install",
86          defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST,
87          requiresDependencyResolution = ResolutionScope.TEST,
88          threadSafe = true)
89  public class InstallMojo extends AbstractMojo {
90  
91      // components used in Mojo
92      private final RepositorySystem repositorySystem;
93  
94      private final MavenSession session;
95  
96      private final MavenProject project;
97  
98      /**
99       * The path to the local repository into which the project artifacts should be installed for the integration tests.
100      * If not set, the regular local repository will be used. To prevent soiling of your regular local repository with
101      * possibly broken artifacts, it is strongly recommended to use an isolated repository for the integration tests
102      * (e.g. <code>${project.build.directory}/it-repo</code>).
103      */
104     @Parameter(
105             property = "invoker.localRepositoryPath",
106             defaultValue = "${session.localRepository.basedir}",
107             required = true)
108     private File localRepositoryPath;
109 
110     /**
111      * A flag used to disable the installation procedure. This is primarily intended for usage from the command line to
112      * occasionally adjust the build.
113      *
114      * @since 1.4
115      */
116     @Parameter(property = "invoker.skip", defaultValue = "false")
117     private boolean skipInstallation;
118 
119     /**
120      * Extra dependencies that need to be installed on the local repository.
121      * <p>
122      * Format:
123      * <pre>
124      * groupId:artifactId:version:type:classifier
125      * </pre>
126      * <p>
127      * Examples:
128      * <pre>
129      * org.apache.maven.plugins:maven-clean-plugin:2.4:maven-plugin
130      * org.apache.maven.plugins:maven-clean-plugin:2.4:jar:javadoc
131      * </pre>
132      * <p>
133      * If the type is 'maven-plugin' the plugin will try to resolve the artifact using plugin remote repositories,
134      * instead of using artifact remote repositories.
135      * <p>
136      * <b>NOTICE</b> all dependencies will be resolved with transitive dependencies in <code>runtime</code> scope.
137      *
138      * @since 1.6
139      */
140     @Parameter
141     private String[] extraArtifacts;
142 
143     /**
144      * Scope to resolve project artifacts.
145      *
146      * @since 3.5.0
147      */
148     @Parameter(property = "invoker.install.scope", defaultValue = "runtime")
149     private String scope;
150 
151     @Inject
152     public InstallMojo(RepositorySystem repositorySystem, MavenSession session, MavenProject project) {
153         this.repositorySystem = repositorySystem;
154         this.session = session;
155         this.project = project;
156     }
157 
158     /**
159      * Performs this mojo's tasks.
160      *
161      * @throws MojoExecutionException If the artifacts could not be installed.
162      */
163     public void execute() throws MojoExecutionException {
164         if (skipInstallation) {
165             getLog().info("Skipping artifact installation per configuration.");
166             return;
167         }
168 
169         Map<String, Artifact> resolvedArtifacts = new LinkedHashMap<>();
170 
171         try {
172 
173             resolveProjectArtifacts(resolvedArtifacts);
174             resolveProjectPoms(project, resolvedArtifacts);
175             resolveProjectDependencies(resolvedArtifacts);
176             resolveExtraArtifacts(resolvedArtifacts);
177             installArtifacts(resolvedArtifacts);
178 
179         } catch (DependencyResolutionException | InstallationException | ArtifactResolutionException e) {
180             throw new MojoExecutionException(e.getMessage(), e);
181         }
182     }
183 
184     private void resolveProjectArtifacts(Map<String, Artifact> resolvedArtifacts) {
185 
186         // pom packaging doesn't have a main artifact
187         if (project.getArtifact() != null && project.getArtifact().getFile() != null) {
188             Artifact artifact = RepositoryUtils.toArtifact(project.getArtifact());
189             resolvedArtifacts.put(ArtifactIdUtils.toId(artifact), artifact);
190         }
191 
192         project.getAttachedArtifacts().stream()
193                 .map(RepositoryUtils::toArtifact)
194                 .forEach(a -> resolvedArtifacts.put(ArtifactIdUtils.toId(a), a));
195     }
196 
197     private void resolveProjectPoms(MavenProject project, Map<String, Artifact> resolvedArtifacts)
198             throws ArtifactResolutionException {
199 
200         if (project == null) {
201             return;
202         }
203 
204         Artifact projectPom = RepositoryUtils.toArtifact(new ProjectArtifact(project));
205         if (projectPom.getFile() != null) {
206             resolvedArtifacts.put(projectPom.toString(), projectPom);
207         } else {
208             Artifact artifact = resolveArtifact(projectPom, project.getRemoteProjectRepositories());
209             resolvedArtifacts.put(ArtifactIdUtils.toId(artifact), artifact);
210         }
211         resolveProjectPoms(project.getParent(), resolvedArtifacts);
212     }
213 
214     private void resolveProjectDependencies(Map<String, Artifact> resolvedArtifacts)
215             throws ArtifactResolutionException, MojoExecutionException, DependencyResolutionException {
216 
217         DependencyFilter classpathFilter = DependencyFilterUtils.classpathFilter(scope);
218 
219         ArtifactTypeRegistry artifactTypeRegistry =
220                 session.getRepositorySession().getArtifactTypeRegistry();
221 
222         List<Dependency> managedDependencies = Optional.ofNullable(project.getDependencyManagement())
223                 .map(DependencyManagement::getDependencies)
224                 .orElseGet(Collections::emptyList)
225                 .stream()
226                 .map(d -> RepositoryUtils.toDependency(d, artifactTypeRegistry))
227                 .collect(Collectors.toList());
228 
229         List<Dependency> dependencies = project.getDependencies().stream()
230                 .map(d -> RepositoryUtils.toDependency(d, artifactTypeRegistry))
231                 .filter(d -> classpathFilter.accept(new DefaultDependencyNode(d), null))
232                 .collect(Collectors.toList());
233 
234         CollectRequest collectRequest = new CollectRequest();
235         collectRequest.setRootArtifact(RepositoryUtils.toArtifact(project.getArtifact()));
236         collectRequest.setDependencies(dependencies);
237         collectRequest.setManagedDependencies(managedDependencies);
238 
239         collectRequest.setRepositories(project.getRemoteProjectRepositories());
240 
241         DependencyRequest request = new DependencyRequest(collectRequest, classpathFilter);
242 
243         DependencyResult dependencyResult =
244                 repositorySystem.resolveDependencies(session.getRepositorySession(), request);
245 
246         List<Artifact> artifacts = dependencyResult.getArtifactResults().stream()
247                 .map(ArtifactResult::getArtifact)
248                 .collect(Collectors.toList());
249 
250         artifacts.forEach(a -> resolvedArtifacts.put(ArtifactIdUtils.toId(a), a));
251         resolvePomsForArtifacts(artifacts, resolvedArtifacts, collectRequest.getRepositories());
252     }
253 
254     /**
255      * Resolve extra artifacts.
256      */
257     private void resolveExtraArtifacts(Map<String, Artifact> resolvedArtifacts)
258             throws MojoExecutionException, DependencyResolutionException, ArtifactResolutionException {
259 
260         if (extraArtifacts == null) {
261             return;
262         }
263 
264         DependencyFilter classpathFilter = DependencyFilterUtils.classpathFilter(JavaScopes.RUNTIME);
265 
266         for (String extraArtifact : extraArtifacts) {
267             String[] gav = extraArtifact.split(":");
268             if (gav.length < 3 || gav.length > 5) {
269                 throw new MojoExecutionException("Invalid artifact " + extraArtifact);
270             }
271 
272             String groupId = gav[0];
273             String artifactId = gav[1];
274             String version = gav[2];
275 
276             String type = "jar";
277             if (gav.length > 3) {
278                 type = gav[3];
279             }
280 
281             String classifier = null;
282             if (gav.length == 5) {
283                 classifier = gav[4];
284             }
285 
286             ArtifactType artifactType =
287                     session.getRepositorySession().getArtifactTypeRegistry().get(type);
288 
289             List<RemoteRepository> remoteRepositories =
290                     artifactType != null && "maven-plugin".equals(artifactType.getId())
291                             ? project.getRemotePluginRepositories()
292                             : project.getRemoteProjectRepositories();
293 
294             Artifact artifact = new DefaultArtifact(groupId, artifactId, classifier, null, version, artifactType);
295 
296             resolvePomsForArtifacts(Collections.singletonList(artifact), resolvedArtifacts, remoteRepositories);
297 
298             CollectRequest collectRequest = new CollectRequest();
299             Dependency root = new Dependency(artifact, JavaScopes.COMPILE);
300             collectRequest.setRoot(root);
301             collectRequest.setRepositories(remoteRepositories);
302 
303             DependencyRequest request = new DependencyRequest(collectRequest, classpathFilter);
304             DependencyResult dependencyResult =
305                     repositorySystem.resolveDependencies(session.getRepositorySession(), request);
306 
307             List<Artifact> artifacts = dependencyResult.getArtifactResults().stream()
308                     .map(ArtifactResult::getArtifact)
309                     .collect(Collectors.toList());
310 
311             artifacts.forEach(a -> resolvedArtifacts.put(ArtifactIdUtils.toId(a), a));
312             resolvePomsForArtifacts(artifacts, resolvedArtifacts, collectRequest.getRepositories());
313         }
314     }
315 
316     private void resolvePomsForArtifacts(
317             List<Artifact> artifacts,
318             Map<String, Artifact> resolvedArtifacts,
319             List<RemoteRepository> remoteRepositories)
320             throws ArtifactResolutionException, MojoExecutionException {
321 
322         for (Artifact a : artifacts) {
323             Artifact artifactResult = resolveArtifact(new SubArtifact(a, "", "pom"), remoteRepositories);
324             resolvePomWithParents(artifactResult, resolvedArtifacts, remoteRepositories);
325         }
326     }
327 
328     private void resolvePomWithParents(
329             Artifact artifact, Map<String, Artifact> resolvedArtifacts, List<RemoteRepository> remoteRepositories)
330             throws MojoExecutionException, ArtifactResolutionException {
331 
332         if (resolvedArtifacts.containsKey(ArtifactIdUtils.toId(artifact))) {
333             return;
334         }
335 
336         Model model = PomUtils.loadPom(artifact.getFile());
337         Parent parent = model.getParent();
338         if (parent != null) {
339             DefaultArtifact pom =
340                     new DefaultArtifact(parent.getGroupId(), parent.getArtifactId(), "", "pom", parent.getVersion());
341             Artifact resolvedPom = resolveArtifact(pom, remoteRepositories);
342             resolvePomWithParents(resolvedPom, resolvedArtifacts, remoteRepositories);
343         }
344 
345         resolvedArtifacts.put(ArtifactIdUtils.toId(artifact), artifact);
346     }
347 
348     private Artifact resolveArtifact(Artifact artifact, List<RemoteRepository> remoteRepositories)
349             throws ArtifactResolutionException {
350 
351         ArtifactRequest request = new ArtifactRequest();
352         request.setArtifact(artifact);
353         request.setRepositories(remoteRepositories);
354         ArtifactResult artifactResult = repositorySystem.resolveArtifact(session.getRepositorySession(), request);
355         return artifactResult.getArtifact();
356     }
357 
358     /**
359      * Install list of artifacts into local repository.
360      */
361     private void installArtifacts(Map<String, Artifact> resolvedArtifacts) throws InstallationException {
362 
363         RepositorySystemSession systemSessionForLocalRepo = createSystemSessionForLocalRepo();
364 
365         // we can have on dependency two artifacts with the same groupId:artifactId
366         // with different version, in such case when we install both in one request
367         // metadata will contain only one version
368 
369         Map<String, List<Artifact>> collect = resolvedArtifacts.values().stream()
370                 .filter(a -> !hasTheSamePathAsTarget(a, systemSessionForLocalRepo))
371                 .collect(Collectors.groupingBy(
372                         a -> String.format("%s:%s:%s", a.getGroupId(), a.getArtifactId(), a.getVersion()),
373                         LinkedHashMap::new,
374                         Collectors.toList()));
375 
376         for (List<Artifact> artifacts : collect.values()) {
377             InstallRequest request = new InstallRequest();
378             request.setArtifacts(artifacts);
379             repositorySystem.install(systemSessionForLocalRepo, request);
380         }
381     }
382 
383     private boolean hasTheSamePathAsTarget(Artifact artifact, RepositorySystemSession systemSession) {
384         try {
385             LocalRepositoryManager lrm = systemSession.getLocalRepositoryManager();
386             File targetBasedir = lrm.getRepository().getBasedir();
387             if (targetBasedir == null) {
388                 return false;
389             }
390             File targetFile = new File(targetBasedir, lrm.getPathForLocalArtifact(artifact)).getCanonicalFile();
391             File sourceFile = artifact.getFile().getCanonicalFile();
392             if (Objects.equals(targetFile, sourceFile)) {
393                 getLog().debug("Skip install the same target " + sourceFile);
394                 return true;
395             }
396             return false;
397         } catch (IOException e) {
398             throw new UncheckedIOException(e);
399         }
400     }
401 
402     /**
403      * Create a new {@link  RepositorySystemSession} connected with local repo.
404      */
405     private RepositorySystemSession createSystemSessionForLocalRepo() {
406         RepositorySystemSession repositorySystemSession = session.getRepositorySession();
407         if (localRepositoryPath != null) {
408             // "clone" repository session and replace localRepository
409             DefaultRepositorySystemSession newSession =
410                     new DefaultRepositorySystemSession(session.getRepositorySession());
411             // Clear cache, since we're using a new local repository
412             newSession.setCache(new DefaultRepositoryCache());
413             // keep same repositoryType
414             String contentType = newSession.getLocalRepository().getContentType();
415             if ("enhanced".equals(contentType)) {
416                 contentType = "default";
417             }
418             LocalRepositoryManager localRepositoryManager = repositorySystem.newLocalRepositoryManager(
419                     newSession, new LocalRepository(localRepositoryPath, contentType));
420 
421             newSession.setLocalRepositoryManager(localRepositoryManager);
422             repositorySystemSession = newSession;
423             getLog().debug("localRepoPath: "
424                     + localRepositoryManager.getRepository().getBasedir());
425         }
426 
427         return repositorySystemSession;
428     }
429 }