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