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  /*
22   * Licensed to the Apache Software Foundation (ASF) under one
23   * or more contributor license agreements.  See the NOTICE file
24   * distributed with this work for additional information
25   * regarding copyright ownership.  The ASF licenses this file
26   * to you under the Apache License, Version 2.0 (the
27   * "License"); you may not use this file except in compliance
28   * with the License.  You may obtain a copy of the License at
29   *
30   *  http://www.apache.org/licenses/LICENSE-2.0
31   *
32   * Unless required by applicable law or agreed to in writing,
33   * software distributed under the License is distributed on an
34   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
35   * KIND, either express or implied.  See the License for the
36   * specific language governing permissions and limitations
37   * under the License.
38   */
39  
40  import java.io.File;
41  import java.io.IOException;
42  import java.util.Collection;
43  import java.util.Collections;
44  import java.util.HashMap;
45  import java.util.HashSet;
46  import java.util.LinkedHashSet;
47  import java.util.Map;
48  
49  import org.apache.maven.artifact.Artifact;
50  import org.apache.maven.artifact.factory.ArtifactFactory;
51  import org.apache.maven.artifact.repository.ArtifactRepository;
52  import org.apache.maven.execution.MavenSession;
53  import org.apache.maven.model.Model;
54  import org.apache.maven.model.Parent;
55  import org.apache.maven.plugin.AbstractMojo;
56  import org.apache.maven.plugin.MojoExecutionException;
57  import org.apache.maven.plugins.annotations.Component;
58  import org.apache.maven.plugins.annotations.LifecyclePhase;
59  import org.apache.maven.plugins.annotations.Mojo;
60  import org.apache.maven.plugins.annotations.Parameter;
61  import org.apache.maven.plugins.annotations.ResolutionScope;
62  import org.apache.maven.project.MavenProject;
63  import org.apache.maven.project.ProjectBuildingRequest;
64  import org.apache.maven.shared.artifact.filter.resolve.PatternExclusionsFilter;
65  import org.apache.maven.shared.transfer.artifact.install.ArtifactInstaller;
66  import org.apache.maven.shared.transfer.dependencies.DefaultDependableCoordinate;
67  import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver;
68  import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolverException;
69  import org.apache.maven.shared.transfer.repository.RepositoryManager;
70  import org.codehaus.plexus.util.FileUtils;
71  
72  /**
73   * Installs the project artifacts of the main build into the local repository as a preparation to run the sub projects.
74   * More precisely, all artifacts of the project itself, all its locally reachable parent POMs and all its dependencies
75   * from the reactor will be installed to the local repository.
76   *
77   * @since 1.2
78   * @author Paul Gier
79   * @author Benjamin Bentmann
80   *
81   */
82  // CHECKSTYLE_OFF: LineLength
83  @Mojo(
84          name = "install",
85          defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST,
86          requiresDependencyResolution = ResolutionScope.RUNTIME,
87          threadSafe = true)
88  // CHECKSTYLE_ON: LineLength
89  public class InstallMojo extends AbstractMojo {
90  
91      /**
92       * Maven artifact install component to copy artifacts to the local repository.
93       */
94      @Component
95      private ArtifactInstaller installer;
96  
97      @Component
98      private RepositoryManager repositoryManager;
99  
100     /**
101      * The component used to create artifacts.
102      */
103     @Component
104     private ArtifactFactory artifactFactory;
105 
106     /**
107      */
108     @Parameter(property = "localRepository", required = true, readonly = true)
109     private ArtifactRepository localRepository;
110 
111     /**
112      * The path to the local repository into which the project artifacts should be installed for the integration tests.
113      * If not set, the regular local repository will be used. To prevent soiling of your regular local repository with
114      * possibly broken artifacts, it is strongly recommended to use an isolated repository for the integration tests
115      * (e.g. <code>${project.build.directory}/it-repo</code>).
116      */
117     @Parameter(
118             property = "invoker.localRepositoryPath",
119             defaultValue = "${session.localRepository.basedir}",
120             required = true)
121     private File localRepositoryPath;
122 
123     /**
124      * The current Maven project.
125      */
126     @Parameter(defaultValue = "${project}", readonly = true, required = true)
127     private MavenProject project;
128 
129     @Parameter(defaultValue = "${session}", readonly = true, required = true)
130     private MavenSession session;
131 
132     /**
133      * The set of Maven projects in the reactor build.
134      */
135     @Parameter(defaultValue = "${reactorProjects}", readonly = true)
136     private Collection<MavenProject> reactorProjects;
137 
138     /**
139      * A flag used to disable the installation procedure. This is primarily intended for usage from the command line to
140      * occasionally adjust the build.
141      *
142      * @since 1.4
143      */
144     @Parameter(property = "invoker.skip", defaultValue = "false")
145     private boolean skipInstallation;
146 
147     /**
148      * The identifiers of already installed artifacts, used to avoid multiple installation of the same artifact.
149      */
150     private Collection<String> installedArtifacts;
151 
152     /**
153      * The identifiers of already copied artifacts, used to avoid multiple installation of the same artifact.
154      */
155     private Collection<String> copiedArtifacts;
156 
157     /**
158      * Extra dependencies that need to be installed on the local repository.<BR>
159      * Format:
160      *
161      * <pre>
162      * groupId:artifactId:version:type:classifier
163      * </pre>
164      *
165      * Examples:
166      *
167      * <pre>
168      * org.apache.maven.plugins:maven-clean-plugin:2.4:maven-plugin
169      * org.apache.maven.plugins:maven-clean-plugin:2.4:jar:javadoc
170      * </pre>
171      *
172      * If the type is 'maven-plugin' the plugin will try to resolve the artifact using plugin remote repositories,
173      * instead of using artifact remote repositories.
174      *
175      * @since 1.6
176      */
177     @Parameter
178     private String[] extraArtifacts;
179 
180     /**
181      */
182     @Component
183     private DependencyResolver resolver;
184 
185     /**
186      * if the local repository is not used as test repo, the parameter can force get artifacts from local repo
187      * if available instead of download the artifacts again.
188      * @since 3.2.1
189      */
190     @Parameter(property = "invoker.useLocalRepository", defaultValue = "false")
191     private boolean useLocalRepository;
192 
193     private ProjectBuildingRequest projectBuildingRequest;
194 
195     /**
196      * Performs this mojo's tasks.
197      *
198      * @throws MojoExecutionException If the artifacts could not be installed.
199      */
200     public void execute() throws MojoExecutionException {
201         if (skipInstallation) {
202             getLog().info("Skipping artifact installation per configuration.");
203             return;
204         }
205 
206         createTestRepository();
207 
208         installedArtifacts = new HashSet<>();
209         copiedArtifacts = new HashSet<>();
210 
211         installProjectDependencies(project, reactorProjects);
212         installProjectParents(project);
213         installProjectArtifacts(project);
214 
215         installExtraArtifacts(extraArtifacts);
216     }
217 
218     /**
219      * Creates the local repository for the integration tests. If the user specified a custom repository location, the
220      * custom repository will have the same identifier, layout and policies as the real local repository. That means
221      * apart from the location, the custom repository will be indistinguishable from the real repository such that its
222      * usage is transparent to the integration tests.
223      *
224      * @throws MojoExecutionException If the repository could not be created.
225      */
226     private void createTestRepository() throws MojoExecutionException {
227 
228         if (!localRepositoryPath.exists() && !localRepositoryPath.mkdirs()) {
229             throw new MojoExecutionException("Failed to create directory: " + localRepositoryPath);
230         }
231         projectBuildingRequest =
232                 repositoryManager.setLocalRepositoryBasedir(session.getProjectBuildingRequest(), localRepositoryPath);
233     }
234 
235     /**
236      * Installs the specified artifact to the local repository. Note: This method should only be used for artifacts that
237      * originate from the current (reactor) build. Artifacts that have been grabbed from the user's local repository
238      * should be installed to the test repository via {@link #copyArtifact(File, Artifact)}.
239      *
240      * @param file The file associated with the artifact, must not be <code>null</code>. This is in most cases the value
241      *            of <code>artifact.getFile()</code> with the exception of the main artifact from a project with
242      *            packaging "pom". Projects with packaging "pom" have no main artifact file. They have however artifact
243      *            metadata (e.g. site descriptors) which needs to be installed.
244      * @param artifact The artifact to install, must not be <code>null</code>.
245      * @throws MojoExecutionException If the artifact could not be installed (e.g. has no associated file).
246      */
247     private void installArtifact(File file, Artifact artifact) throws MojoExecutionException {
248         try {
249             if (file == null) {
250                 throw new IllegalStateException("Artifact has no associated file: " + artifact.getId());
251             }
252             if (!file.isFile()) {
253                 throw new IllegalStateException("Artifact is not fully assembled: " + file);
254             }
255 
256             if (installedArtifacts.add(artifact.getId())) {
257                 artifact.setFile(file);
258                 installer.install(projectBuildingRequest, localRepositoryPath, Collections.singletonList(artifact));
259             } else {
260                 getLog().debug("Not re-installing " + artifact + ", " + file);
261             }
262         } catch (Exception e) {
263             throw new MojoExecutionException("Failed to install artifact: " + artifact, e);
264         }
265     }
266 
267     /**
268      * Installs the specified artifact to the local repository. This method serves basically the same purpose as
269      * {@link #installArtifact(File, Artifact)} but is meant for artifacts that have been resolved
270      * from the user's local repository (and not the current build outputs). The subtle difference here is that
271      * artifacts from the repository have already undergone transformations and these manipulations should not be redone
272      * by the artifact installer. For this reason, this method performs plain copy operations to install the artifacts.
273      *
274      * @param file The file associated with the artifact, must not be <code>null</code>.
275      * @param artifact The artifact to install, must not be <code>null</code>.
276      * @throws MojoExecutionException If the artifact could not be installed (e.g. has no associated file).
277      */
278     private void copyArtifact(File file, Artifact artifact) throws MojoExecutionException {
279         try {
280             if (file == null) {
281                 throw new IllegalStateException("Artifact has no associated file: " + artifact.getId());
282             }
283             if (!file.isFile()) {
284                 throw new IllegalStateException("Artifact is not fully assembled: " + file);
285             }
286 
287             if (copiedArtifacts.add(artifact.getId())) {
288                 File destination = new File(
289                         localRepositoryPath,
290                         repositoryManager.getPathForLocalArtifact(projectBuildingRequest, artifact));
291 
292                 getLog().debug("Installing " + file + " to " + destination);
293 
294                 copyFileIfDifferent(file, destination);
295 
296                 MetadataUtils.createMetadata(destination, artifact);
297             } else {
298                 getLog().debug("Not re-installing " + artifact + ", " + file);
299             }
300         } catch (Exception e) {
301             throw new MojoExecutionException("Failed to stage artifact: " + artifact, e);
302         }
303     }
304 
305     private void copyFileIfDifferent(File src, File dst) throws IOException {
306         if (src.lastModified() != dst.lastModified() || src.length() != dst.length()) {
307             FileUtils.copyFile(src, dst);
308             dst.setLastModified(src.lastModified());
309         }
310     }
311 
312     /**
313      * Installs the main artifact and any attached artifacts of the specified project to the local repository.
314      *
315      * @param mvnProject The project whose artifacts should be installed, must not be <code>null</code>.
316      * @throws MojoExecutionException If any artifact could not be installed.
317      */
318     private void installProjectArtifacts(MavenProject mvnProject) throws MojoExecutionException {
319         try {
320             // Install POM (usually attached as metadata but that happens only as a side effect of the Install Plugin)
321             installProjectPom(mvnProject);
322 
323             // Install the main project artifact (if the project has one, e.g. has no "pom" packaging)
324             Artifact mainArtifact = mvnProject.getArtifact();
325             if (mainArtifact.getFile() != null) {
326                 installArtifact(mainArtifact.getFile(), mainArtifact);
327             }
328 
329             // Install any attached project artifacts
330             Collection<Artifact> attachedArtifacts = mvnProject.getAttachedArtifacts();
331             for (Artifact attachedArtifact : attachedArtifacts) {
332                 installArtifact(attachedArtifact.getFile(), attachedArtifact);
333             }
334         } catch (Exception e) {
335             throw new MojoExecutionException("Failed to install project artifacts: " + mvnProject, e);
336         }
337     }
338 
339     /**
340      * Installs the (locally reachable) parent POMs of the specified project to the local repository. The parent POMs
341      * from the reactor must be installed or the forked IT builds will fail when using a clean repository.
342      *
343      * @param mvnProject The project whose parent POMs should be installed, must not be <code>null</code>.
344      * @throws MojoExecutionException If any POM could not be installed.
345      */
346     private void installProjectParents(MavenProject mvnProject) throws MojoExecutionException {
347         try {
348             for (MavenProject parent = mvnProject.getParent(); parent != null; parent = parent.getParent()) {
349                 if (parent.getFile() == null) {
350                     copyParentPoms(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
351                     break;
352                 }
353                 installProjectPom(parent);
354             }
355         } catch (Exception e) {
356             throw new MojoExecutionException("Failed to install project parents: " + mvnProject, e);
357         }
358     }
359 
360     /**
361      * Installs the POM of the specified project to the local repository.
362      *
363      * @param mvnProject The project whose POM should be installed, must not be <code>null</code>.
364      * @throws MojoExecutionException If the POM could not be installed.
365      */
366     private void installProjectPom(MavenProject mvnProject) throws MojoExecutionException {
367         try {
368             Artifact pomArtifact = null;
369             if ("pom".equals(mvnProject.getPackaging())) {
370                 pomArtifact = mvnProject.getArtifact();
371             }
372             if (pomArtifact == null) {
373                 pomArtifact = artifactFactory.createProjectArtifact(
374                         mvnProject.getGroupId(), mvnProject.getArtifactId(), mvnProject.getVersion());
375             }
376             installArtifact(mvnProject.getFile(), pomArtifact);
377         } catch (Exception e) {
378             throw new MojoExecutionException("Failed to install POM: " + mvnProject, e);
379         }
380     }
381 
382     /**
383      * Installs the dependent projects from the reactor to the local repository. The dependencies on other modules from
384      * the reactor must be installed or the forked IT builds will fail when using a clean repository.
385      *
386      * @param mvnProject The project whose dependent projects should be installed, must not be <code>null</code>.
387      * @param reactorProjects The set of projects in the reactor build, must not be <code>null</code>.
388      * @throws MojoExecutionException If any dependency could not be installed.
389      */
390     private void installProjectDependencies(MavenProject mvnProject, Collection<MavenProject> reactorProjects)
391             throws MojoExecutionException {
392         // ... into dependencies that were resolved from reactor projects ...
393         Collection<String> dependencyProjects = new LinkedHashSet<>();
394         collectAllProjectReferences(mvnProject, dependencyProjects);
395 
396         // index available reactor projects
397         Map<String, MavenProject> projects = new HashMap<>(reactorProjects.size());
398         for (MavenProject reactorProject : reactorProjects) {
399             String projectId = reactorProject.getGroupId()
400                     + ':'
401                     + reactorProject.getArtifactId()
402                     + ':'
403                     + reactorProject.getVersion();
404 
405             projects.put(projectId, reactorProject);
406         }
407 
408         // group transitive dependencies (even those that don't contribute to the class path like POMs) ...
409         Collection<Artifact> artifacts = mvnProject.getArtifacts();
410         // ... and those that were resolved from the (local) repo
411         Collection<Artifact> dependencyArtifacts = new LinkedHashSet<>();
412 
413         for (Artifact artifact : artifacts) {
414             // workaround for MNG-2961 to ensure the base version does not contain a timestamp
415             artifact.isSnapshot();
416 
417             String projectId = artifact.getGroupId() + ':' + artifact.getArtifactId() + ':' + artifact.getBaseVersion();
418 
419             if (!projects.containsKey(projectId)) {
420                 dependencyArtifacts.add(artifact);
421             }
422         }
423 
424         // install dependencies
425         try {
426             // copy dependencies that where resolved from the local repo
427             for (Artifact artifact : dependencyArtifacts) {
428                 copyArtifact(artifact);
429             }
430 
431             // install dependencies that were resolved from the reactor
432             for (String projectId : dependencyProjects) {
433                 MavenProject dependencyProject = projects.get(projectId);
434                 if (dependencyProject == null) {
435                     getLog().warn("skip dependencyProject null for projectId=" + projectId);
436                     continue;
437                 }
438                 installProjectArtifacts(dependencyProject);
439                 installProjectParents(dependencyProject);
440             }
441         } catch (Exception e) {
442             throw new MojoExecutionException("Failed to install project dependencies: " + mvnProject, e);
443         }
444     }
445 
446     protected void collectAllProjectReferences(MavenProject project, Collection<String> dependencyProjects) {
447         for (MavenProject reactorProject : project.getProjectReferences().values()) {
448             String projectId = reactorProject.getGroupId()
449                     + ':'
450                     + reactorProject.getArtifactId()
451                     + ':'
452                     + reactorProject.getVersion();
453             if (dependencyProjects.add(projectId)) {
454                 collectAllProjectReferences(reactorProject, dependencyProjects);
455             }
456         }
457     }
458 
459     private void copyArtifact(Artifact artifact) throws MojoExecutionException {
460         copyPoms(artifact);
461 
462         Artifact depArtifact = artifactFactory.createArtifactWithClassifier(
463                 artifact.getGroupId(),
464                 artifact.getArtifactId(),
465                 artifact.getBaseVersion(),
466                 artifact.getType(),
467                 artifact.getClassifier());
468 
469         File artifactFile = artifact.getFile();
470 
471         copyArtifact(artifactFile, depArtifact);
472     }
473 
474     private void copyPoms(Artifact artifact) throws MojoExecutionException {
475         Artifact pomArtifact = artifactFactory.createProjectArtifact(
476                 artifact.getGroupId(), artifact.getArtifactId(), artifact.getBaseVersion());
477 
478         File pomFile = new File(localRepository.getBasedir(), localRepository.pathOf(pomArtifact));
479 
480         if (pomFile.isFile()) {
481             copyArtifact(pomFile, pomArtifact);
482             copyParentPoms(pomFile);
483         }
484     }
485 
486     /**
487      * Installs all parent POMs of the specified POM file that are available in the local repository.
488      *
489      * @param pomFile The path to the POM file whose parents should be installed, must not be <code>null</code>.
490      * @throws MojoExecutionException If any (existing) parent POM could not be installed.
491      */
492     private void copyParentPoms(File pomFile) throws MojoExecutionException {
493         Model model = PomUtils.loadPom(pomFile);
494         Parent parent = model.getParent();
495         if (parent != null) {
496             copyParentPoms(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
497         }
498     }
499 
500     /**
501      * Installs the specified POM and all its parent POMs to the local repository.
502      *
503      * @param groupId The group id of the POM which should be installed, must not be <code>null</code>.
504      * @param artifactId The artifact id of the POM which should be installed, must not be <code>null</code>.
505      * @param version The version of the POM which should be installed, must not be <code>null</code>.
506      * @throws MojoExecutionException If any (existing) parent POM could not be installed.
507      */
508     private void copyParentPoms(String groupId, String artifactId, String version) throws MojoExecutionException {
509         Artifact pomArtifact = artifactFactory.createProjectArtifact(groupId, artifactId, version);
510 
511         if (installedArtifacts.contains(pomArtifact.getId()) || copiedArtifacts.contains(pomArtifact.getId())) {
512             getLog().debug("Not re-installing " + pomArtifact);
513             return;
514         }
515 
516         File pomFile = new File(localRepository.getBasedir(), localRepository.pathOf(pomArtifact));
517         if (pomFile.isFile()) {
518             copyArtifact(pomFile, pomArtifact);
519             copyParentPoms(pomFile);
520         }
521     }
522 
523     private void installExtraArtifacts(String[] extraArtifacts) throws MojoExecutionException {
524         if (extraArtifacts == null) {
525             return;
526         }
527 
528         for (String extraArtifact : extraArtifacts) {
529             String[] gav = extraArtifact.split(":");
530             if (gav.length < 3 || gav.length > 5) {
531                 throw new MojoExecutionException("Invalid artifact " + extraArtifact);
532             }
533 
534             String groupId = gav[0];
535             String artifactId = gav[1];
536             String version = gav[2];
537 
538             String type = "jar";
539             if (gav.length > 3) {
540                 type = gav[3];
541             }
542 
543             String classifier = null;
544             if (gav.length == 5) {
545                 classifier = gav[4];
546             }
547 
548             DefaultDependableCoordinate coordinate = new DefaultDependableCoordinate();
549             try {
550                 coordinate.setGroupId(groupId);
551                 coordinate.setArtifactId(artifactId);
552                 coordinate.setVersion(version);
553                 coordinate.setType(type);
554                 coordinate.setClassifier(classifier);
555 
556                 if (!localRepository.getBasedir().equals(localRepositoryPath.getPath()) && useLocalRepository) {
557                     String previousId = localRepository.getId();
558                     try {
559                         // using another request with the correct target repo
560                         ProjectBuildingRequest projectBuildingRequest = repositoryManager.setLocalRepositoryBasedir(
561                                 session.getProjectBuildingRequest(), localRepositoryPath);
562                         projectBuildingRequest.setRemoteRepositories(Collections.singletonList(localRepository));
563                         resolver.resolveDependencies(
564                                 projectBuildingRequest,
565                                 coordinate,
566                                 new PatternExclusionsFilter(Collections.emptyList()));
567                     } finally {
568                         localRepository.setId(previousId);
569                     }
570                 } else {
571                     resolver.resolveDependencies(
572                             projectBuildingRequest, coordinate, new PatternExclusionsFilter(Collections.emptyList()));
573                 }
574             } catch (DependencyResolverException e) {
575                 throw new MojoExecutionException("Unable to resolve dependencies for: " + coordinate, e);
576             }
577         }
578     }
579 }