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.source;
20  
21  import java.io.File;
22  import java.nio.file.Files;
23  import java.nio.file.Path;
24  import java.nio.file.Paths;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.Objects;
30  
31  import org.apache.maven.api.Artifact;
32  import org.apache.maven.api.Project;
33  import org.apache.maven.api.Session;
34  import org.apache.maven.api.Type;
35  import org.apache.maven.api.di.Inject;
36  import org.apache.maven.api.model.Resource;
37  import org.apache.maven.api.plugin.Log;
38  import org.apache.maven.api.plugin.Mojo;
39  import org.apache.maven.api.plugin.MojoException;
40  import org.apache.maven.api.plugin.annotations.Parameter;
41  import org.apache.maven.api.services.ArtifactManager;
42  import org.apache.maven.api.services.ProjectManager;
43  import org.apache.maven.archiver.MavenArchiveConfiguration;
44  import org.apache.maven.archiver.MavenArchiver;
45  import org.codehaus.plexus.archiver.Archiver;
46  import org.codehaus.plexus.archiver.ArchiverException;
47  import org.codehaus.plexus.archiver.jar.JarArchiver;
48  import org.codehaus.plexus.archiver.util.DefaultFileSet;
49  import org.codehaus.plexus.util.FileUtils;
50  
51  /**
52   * Base class for bundling sources into a jar archive.
53   *
54   * @since 2.0.3
55   */
56  public abstract class AbstractSourceJarMojo implements Mojo {
57      private static final String[] DEFAULT_INCLUDES = new String[] {"**/**"};
58  
59      private static final String[] DEFAULT_EXCLUDES = new String[] {};
60  
61      /**
62       * List of files to include. Specified as fileset patterns which are relative to the input directory whose contents
63       * is being packaged into the JAR.
64       *
65       * @since 2.1
66       */
67      @Parameter
68      protected String[] includes;
69  
70      /**
71       * List of files to exclude. Specified as fileset patterns which are relative to the input directory whose contents
72       * is being packaged into the JAR.
73       *
74       * @since 2.1
75       */
76      @Parameter
77      protected String[] excludes;
78  
79      /**
80       * Exclude commonly excluded files such as SCM configuration. These are defined in the plexus
81       * FileUtils.getDefaultExcludes()
82       *
83       * @since 2.1
84       */
85      @Parameter(property = "maven.source.useDefaultExcludes", defaultValue = "true")
86      protected boolean useDefaultExcludes;
87  
88      /**
89       * The Maven Project Object
90       */
91      @Inject
92      protected Project project;
93  
94      /**
95       * The Jar archiver.
96       */
97      @Inject
98      protected JarArchiver jarArchiver;
99  
100     /**
101      * The archive configuration to use. See <a href="http://maven.apache.org/shared/maven-archiver/index.html">Maven
102      * Archiver Reference</a>. <br/>
103      * <b>Note: Since 3.0.0 the resulting archives contain a maven descriptor. If you need to suppress the generation of
104      * the maven descriptor you can simply achieve this by using the
105      * <a href="http://maven.apache.org/shared/maven-archiver/index.html#archive">archiver configuration</a>.</b>.
106      *
107      * @since 2.1
108      */
109     @Parameter
110     protected MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
111 
112     /**
113      * Path to the default MANIFEST file to use. It will be used if <code>useDefaultManifestFile</code> is set to
114      * <code>true</code>.
115      *
116      * @since 2.1
117      */
118     // CHECKSTYLE_OFF: LineLength
119     @Parameter(
120             defaultValue = "${project.build.outputDirectory}/META-INF/MANIFEST.MF",
121             readonly = false,
122             required = true)
123     // CHECKSTYLE_ON: LineLength
124     protected Path defaultManifestFile;
125 
126     /**
127      * Set this to <code>true</code> to enable the use of the <code>defaultManifestFile</code>. <br/>
128      *
129      * @since 2.1
130      */
131     @Parameter(property = "maven.source.useDefaultManifestFile", defaultValue = "false")
132     protected boolean useDefaultManifestFile;
133 
134     /**
135      * Specifies whether to attach the artifact to the project
136      */
137     @Parameter(property = "maven.source.attach", defaultValue = "true")
138     protected boolean attach;
139 
140     /**
141      * Specifies whether to exclude resources from the sources-jar. This can be convenient if your project
142      * includes large resources, such as images, and you don't want to include them in the sources-jar.
143      *
144      * @since 2.0.4
145      */
146     @Parameter(property = "maven.source.excludeResources", defaultValue = "false")
147     protected boolean excludeResources;
148 
149     /**
150      * Specifies whether to include the POM file in the sources-jar.
151      *
152      * @since 2.1
153      */
154     @Parameter(property = "maven.source.includePom", defaultValue = "false")
155     protected boolean includePom;
156 
157     /**
158      * The directory where the generated archive file will be put.
159      */
160     @Parameter(defaultValue = "${project.build.directory}")
161     protected Path outputDirectory;
162 
163     /**
164      * The filename to be used for the generated archive file. For the source:jar goal, "-sources" is appended to this
165      * filename. For the source:test-jar goal, "-test-sources" is appended.
166      */
167     @Parameter(defaultValue = "${project.build.finalName}")
168     protected String finalName;
169 
170     /**
171      * Contains the full list of projects in the reactor.
172      */
173     @Parameter(defaultValue = "${session.projects}", readonly = true)
174     protected List<Project> reactorProjects;
175 
176     /**
177      * Whether creating the archive should be forced. If set to true, the jar will always be created. If set to false,
178      * the jar will only be created when the sources are newer than the jar.
179      *
180      * @since 2.1
181      */
182     @Parameter(property = "maven.source.forceCreation", defaultValue = "false")
183     protected boolean forceCreation;
184 
185     /**
186      * A flag used to disable the source procedure. This is primarily intended for usage from the command line to
187      * occasionally adjust the build.
188      *
189      * @since 2.2
190      */
191     @Parameter(property = "maven.source.skip", defaultValue = "false")
192     protected boolean skipSource;
193 
194     /**
195      * The Maven session.
196      */
197     @Inject
198     protected Session session;
199 
200     /**
201      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
202      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
203      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
204      *
205      * @since 3.2.0
206      */
207     @Parameter(defaultValue = "${project.build.outputTimestamp}")
208     protected String outputTimestamp;
209 
210     @Inject
211     protected Log log;
212 
213     /**
214      * Used for attaching the source jar to the project.
215      */
216     protected ProjectManager projectManager;
217 
218     // ----------------------------------------------------------------------
219     // Public methods
220     // ----------------------------------------------------------------------
221 
222     public Log getLog() {
223         return log;
224     }
225 
226     /**
227      * {@inheritDoc}
228      */
229     public void execute() throws MojoException {
230         if (skipSource) {
231             getLog().info("Skipping source per configuration.");
232             return;
233         }
234 
235         projectManager = session.getService(ProjectManager.class);
236         doExecute();
237     }
238 
239     protected void doExecute() {
240         packageSources(project);
241     }
242 
243     // ----------------------------------------------------------------------
244     // Protected methods
245     // ----------------------------------------------------------------------
246 
247     /**
248      * @return the wanted classifier, ie <code>sources</code> or <code>test-sources</code>
249      */
250     protected abstract String getClassifier();
251 
252     /**
253      * @param p {@link Project} not null
254      * @return the compile or test sources
255      * @throws MojoException in case of an error.
256      */
257     protected abstract List<Path> getSources(Project p) throws MojoException;
258 
259     /**
260      * @param p {@link Project} not null
261      * @return the compile or test resources
262      * @throws MojoException in case of an error.
263      */
264     protected abstract List<Resource> getResources(Project p) throws MojoException;
265 
266     /**
267      * @param p {@link Project}
268      * @throws MojoException in case of an error.
269      */
270     protected void packageSources(Project p) throws MojoException {
271         String type = p.getPackaging().type().id();
272         if (!Type.POM.equals(type) && !Type.BOM.equals(type)) {
273             packageSources(Collections.singletonList(p));
274         }
275     }
276 
277     /**
278      * @param theProjects {@link Project}
279      * @throws MojoException in case of an error.
280      */
281     protected void packageSources(List<Project> theProjects) throws MojoException {
282         Artifact currentProjectArtifact = project.getMainArtifact().get();
283         if (!currentProjectArtifact.getClassifier().isEmpty()) {
284             getLog().warn("NOT adding sources to artifacts with classifier as Maven only supports one classifier "
285                     + "per artifact. Current artifact [" + currentProjectArtifact.key() + "] has a ["
286                     + currentProjectArtifact.getClassifier() + "] classifier.");
287 
288             return;
289         }
290 
291         MavenArchiver archiver = createArchiver();
292 
293         for (Project pItem : theProjects) {
294             Project subProject = getProject(pItem);
295 
296             String type = subProject.getPackaging().type().id();
297             if (Type.POM.equals(type) || Type.BOM.equals(type)) {
298                 continue;
299             }
300 
301             archiveProjectContent(subProject, archiver.getArchiver());
302         }
303 
304         if (archiver.getArchiver().getResources().hasNext() || forceCreation) {
305 
306             if (useDefaultManifestFile && Files.exists(defaultManifestFile) && archive.getManifestFile() == null) {
307                 getLog().info("Adding existing MANIFEST to archive. Found under: " + defaultManifestFile);
308                 archive.setManifestFile(defaultManifestFile);
309             }
310 
311             Path outputFile = outputDirectory.resolve(finalName + "-" + getClassifier() + getExtension());
312 
313             try {
314                 archiver.setOutputFile(outputFile.toFile());
315                 archive.setForced(forceCreation);
316 
317                 getLog().debug("create archive " + outputFile);
318                 archiver.createArchive(session, project, archive);
319             } catch (ArchiverException e) {
320                 throw new MojoException("Error creating source archive: " + e.getMessage(), e);
321             }
322 
323             if (attach) {
324                 Artifact artifact = session.createArtifact(
325                         project.getGroupId(),
326                         project.getArtifactId(),
327                         project.getVersion(),
328                         getClassifier(),
329                         null,
330                         getType());
331                 boolean requiresAttach = true;
332                 for (Artifact attachedArtifact : projectManager.getAttachedArtifacts(project)) {
333                     if (Objects.equals(artifact.key(), attachedArtifact.key())) {
334                         Path attachedFile = session.getService(ArtifactManager.class)
335                                 .getPath(attachedArtifact)
336                                 .orElse(null);
337                         if (attachedFile != null && !outputFile.equals(attachedFile)) {
338                             getLog().error("Artifact " + attachedArtifact.key()
339                                     + " already attached to a file " + relative(attachedFile) + ": attach to "
340                                     + relative(outputFile) + " should be done with another classifier");
341                             throw new MojoException("Presumably you have configured maven-source-plugin "
342                                     + "to execute twice in your build to different output files. "
343                                     + "You have to configure a classifier for at least one of them.");
344                         }
345                         requiresAttach = false;
346                         getLog().info("Artifact " + attachedArtifact.key() + " already attached to "
347                                 + relative(outputFile) + ": ignoring same re-attach (same artifact, same file)");
348                     }
349                 }
350                 if (requiresAttach) {
351                     projectManager.attachArtifact(project, artifact, outputFile);
352                 }
353             } else {
354                 getLog().info("NOT adding java-sources to attached artifacts list.");
355             }
356         } else {
357             getLog().info("No sources in project. Archive not created.");
358         }
359     }
360 
361     private String relative(Path to) {
362         Path basedir = project.getBasedir().toAbsolutePath();
363         return basedir.relativize(to.toAbsolutePath()).toString();
364     }
365 
366     private Artifact getPreviouslyAttached(Artifact artifact, Project checkProject, String classifier) {
367         return artifact.getExtension().equals(getExtension())
368                         && artifact.getGroupId().equals(checkProject.getGroupId())
369                         && artifact.getArtifactId().equals(checkProject.getArtifactId())
370                         && artifact.getVersion().toString().equals(checkProject.getVersion())
371                         && Objects.equals(artifact.getClassifier(), classifier)
372                 ? artifact
373                 : null;
374     }
375 
376     /**
377      * @param project {@link Project}
378      * @param archiver {@link Archiver}
379      * @throws MojoException in case of an error.
380      */
381     protected void archiveProjectContent(Project project, Archiver archiver) throws MojoException {
382         if (includePom) {
383             try {
384                 File pom = project.getPomPath().toFile();
385                 archiver.addFile(pom, pom.getName());
386             } catch (ArchiverException e) {
387                 throw new MojoException("Error adding POM file to target jar file.", e);
388             }
389         }
390 
391         for (Path sourceDirectory : getSources(project)) {
392             if (Files.exists(sourceDirectory)) {
393                 addDirectory(archiver, sourceDirectory, getCombinedIncludes(null), getCombinedExcludes(null));
394             }
395         }
396 
397         // MAPI: this should be taken from the resources plugin
398         for (Resource resource : getResources(project)) {
399 
400             Path sourceDirectory = Paths.get(resource.getDirectory());
401 
402             if (!Files.exists(sourceDirectory)) {
403                 continue;
404             }
405 
406             List<String> resourceIncludes = resource.getIncludes();
407 
408             String[] combinedIncludes = getCombinedIncludes(resourceIncludes);
409 
410             List<String> resourceExcludes = resource.getExcludes();
411 
412             String[] combinedExcludes = getCombinedExcludes(resourceExcludes);
413 
414             String targetPath = resource.getTargetPath();
415             if (targetPath != null) {
416                 if (!targetPath.trim().endsWith("/")) {
417                     targetPath += "/";
418                 }
419                 addDirectory(archiver, sourceDirectory, targetPath, combinedIncludes, combinedExcludes);
420             } else {
421                 addDirectory(archiver, sourceDirectory, combinedIncludes, combinedExcludes);
422             }
423         }
424     }
425 
426     /**
427      * @return {@link MavenArchiver}
428      * @throws MojoException in case of an error.
429      */
430     protected MavenArchiver createArchiver() throws MojoException {
431         MavenArchiver archiver = new MavenArchiver();
432         archiver.setArchiver(jarArchiver);
433         archiver.setCreatedBy("Maven Source Plugin", "org.apache.maven.plugins", "maven-source-plugin");
434         archiver.setBuildJdkSpecDefaultEntry(false);
435 
436         // configure for Reproducible Builds based on outputTimestamp value
437         archiver.configureReproducibleBuild(outputTimestamp);
438 
439         if (project.getBuild() != null) {
440             List<org.apache.maven.api.model.Resource> resources =
441                     project.getBuild().getResources();
442 
443             for (org.apache.maven.api.model.Resource r : resources) {
444                 if (r.getDirectory().endsWith("maven-shared-archive-resources")) {
445                     addDirectory(
446                             archiver.getArchiver(),
447                             Paths.get(r.getDirectory()),
448                             getCombinedIncludes(null),
449                             getCombinedExcludes(null));
450                 }
451             }
452         }
453 
454         return archiver;
455     }
456 
457     /**
458      * @param archiver {@link Archiver}
459      * @param sourceDirectory {@link Path}
460      * @param pIncludes The list of includes.
461      * @param pExcludes The list of excludes.
462      * @throws MojoException in case of an error.
463      */
464     protected void addDirectory(Archiver archiver, Path sourceDirectory, String[] pIncludes, String[] pExcludes)
465             throws MojoException {
466         try {
467             getLog().debug("add directory " + sourceDirectory + " to archiver");
468             archiver.addFileSet(DefaultFileSet.fileSet(sourceDirectory.toFile()).includeExclude(pIncludes, pExcludes));
469         } catch (ArchiverException e) {
470             throw new MojoException("Error adding directory to source archive.", e);
471         }
472     }
473 
474     /**
475      * @param archiver {@link Archiver}
476      * @param sourceDirectory {@link Path}
477      * @param prefix The prefix.
478      * @param pIncludes the includes.
479      * @param pExcludes the excludes.
480      * @throws MojoException in case of an error.
481      */
482     protected void addDirectory(
483             Archiver archiver, Path sourceDirectory, String prefix, String[] pIncludes, String[] pExcludes)
484             throws MojoException {
485         try {
486             getLog().debug("add directory " + sourceDirectory + " to archiver with prefix " + prefix);
487             archiver.addFileSet(DefaultFileSet.fileSet(sourceDirectory.toFile())
488                     .prefixed(prefix)
489                     .includeExclude(pIncludes, pExcludes));
490         } catch (ArchiverException e) {
491             throw new MojoException("Error adding directory to source archive.", e);
492         }
493     }
494 
495     /**
496      * @return The extension {@code .jar}
497      */
498     protected String getExtension() {
499         return ".jar";
500     }
501 
502     /**
503      * @param p {@link Project}
504      * @return The execution projet.
505      */
506     protected Project getProject(Project p) {
507         return projectManager.getExecutionProject(p).orElse(p);
508     }
509 
510     /**
511      * @return The type {@code java-source}
512      */
513     protected String getType() {
514         return "java-source";
515     }
516 
517     /**
518      * Combines the includes parameter and additional includes. Defaults to {@link #DEFAULT_INCLUDES} If the
519      * additionalIncludes parameter is null, it is not added to the combined includes.
520      *
521      * @param additionalIncludes The includes specified in the pom resources section
522      * @return The combined array of includes.
523      */
524     private String[] getCombinedIncludes(List<String> additionalIncludes) {
525         List<String> combinedIncludes = new ArrayList<>();
526 
527         if (includes != null && includes.length > 0) {
528             combinedIncludes.addAll(Arrays.asList(includes));
529         }
530 
531         if (additionalIncludes != null && !additionalIncludes.isEmpty()) {
532             combinedIncludes.addAll(additionalIncludes);
533         }
534 
535         // If there are no other includes, use the default.
536         if (combinedIncludes.isEmpty()) {
537             combinedIncludes.addAll(Arrays.asList(DEFAULT_INCLUDES));
538         }
539 
540         return combinedIncludes.toArray(new String[0]);
541     }
542 
543     /**
544      * Combines the user parameter {@link #excludes}, the default excludes from plexus FileUtils, and the contents of
545      * the parameter addionalExcludes.
546      *
547      * @param additionalExcludes Additional excludes to add to the array
548      * @return The combined list of excludes.
549      */
550     private String[] getCombinedExcludes(List<String> additionalExcludes) {
551         List<String> combinedExcludes = new ArrayList<>();
552 
553         if (useDefaultExcludes) {
554             combinedExcludes.addAll(FileUtils.getDefaultExcludesAsList());
555         }
556 
557         if (excludes != null && excludes.length > 0) {
558             combinedExcludes.addAll(Arrays.asList(excludes));
559         }
560 
561         if (additionalExcludes != null && !additionalExcludes.isEmpty()) {
562             combinedExcludes.addAll(additionalExcludes);
563         }
564 
565         if (combinedExcludes.isEmpty()) {
566             combinedExcludes.addAll(Arrays.asList(DEFAULT_EXCLUDES));
567         }
568 
569         return combinedExcludes.toArray(new String[0]);
570     }
571 
572     /**
573      * @return The current project.
574      */
575     protected Project getProject() {
576         return project;
577     }
578 
579     /**
580      * @param project {@link Project}
581      */
582     protected void setProject(Project project) {
583         this.project = project;
584     }
585 }