View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package org.basepom.mojo.repack;
16  
17  import static com.google.common.base.Preconditions.checkNotNull;
18  import static com.google.common.base.Preconditions.checkState;
19  import static com.google.common.collect.ImmutableSet.toImmutableSet;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.file.attribute.FileTime;
24  import java.time.OffsetDateTime;
25  import java.util.Arrays;
26  import java.util.Set;
27  import java.util.concurrent.TimeUnit;
28  import java.util.regex.Pattern;
29  
30  import com.google.common.base.Strings;
31  import com.google.common.collect.ImmutableSet;
32  import org.apache.maven.artifact.Artifact;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugins.annotations.Component;
37  import org.apache.maven.plugins.annotations.LifecyclePhase;
38  import org.apache.maven.plugins.annotations.Mojo;
39  import org.apache.maven.plugins.annotations.Parameter;
40  import org.apache.maven.plugins.annotations.ResolutionScope;
41  import org.apache.maven.project.MavenProject;
42  import org.apache.maven.project.MavenProjectHelper;
43  import org.apache.maven.shared.artifact.filter.collection.ArtifactFilterException;
44  import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
45  import org.springframework.boot.loader.tools.Layers;
46  import org.springframework.boot.loader.tools.LayoutFactory;
47  import org.springframework.boot.loader.tools.Libraries;
48  import org.springframework.boot.loader.tools.Repackager;
49  
50  /**
51   * Repack archives for execution using {@literal java -jar}. Can also be used to repack a jar with nested dependencies by using <code>layout=NONE</code>.
52   */
53  @Mojo(name = "repack", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true,
54          requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
55          requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
56  public final class RepackMojo extends AbstractMojo {
57  
58      private static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("\\s+");
59  
60      private static final PluginLog LOG = new PluginLog(RepackMojo.class);
61  
62      @Parameter(defaultValue = "${project}", readonly = true, required = true)
63      public MavenProject project;
64  
65      @Parameter(defaultValue = "${session}", readonly = true, required = true)
66      public MavenSession session;
67  
68      @Component
69      public MavenProjectHelper projectHelper;
70  
71      /**
72       * The name of the main class. If not specified the first compiled class found that contains a {@code main} method will be used.
73       */
74      @Parameter(property = "repack.main-class")
75      public String mainClass = null;
76  
77      /**
78       * Collection of artifact definitions to include.
79       */
80      @Parameter(alias = "includes")
81      public Set<DependencyDefinition> includedDependencies = ImmutableSet.of();
82  
83      /**
84       * Collection of artifact definitions to exclude.
85       */
86      @Parameter(alias = "excludedDependencies")
87      public Set<DependencyDefinition> excludedDependencies = ImmutableSet.of();
88  
89      /**
90       * Include system scoped dependencies.
91       */
92      @Parameter(defaultValue = "false", property = "repack.include-system-scope")
93      public boolean includeSystemScope = false;
94  
95      /**
96       * Include provided scoped dependencies.
97       */
98      @Parameter(defaultValue = "false", property = "repack.include-provided-scope")
99      public boolean includeProvidedScope = false;
100 
101     /**
102      * Include optional dependencies
103      */
104     @Parameter(defaultValue = "false", property = "repack.include-optional")
105     public boolean includeOptional = false;
106 
107     /**
108      * Directory containing the generated archive.
109      */
110     @Parameter(defaultValue = "${project.build.directory}", property = "repack.output-directory")
111     public File outputDirectory;
112 
113     /**
114      * Name of the generated archive.
115      */
116     @Parameter(defaultValue = "${project.build.finalName}", property = "repack.final-name")
117     public String finalName;
118 
119     /**
120      * Skip the execution.
121      */
122     @Parameter(defaultValue = "false", property = "repack.skip")
123     public boolean skip = false;
124 
125     /**
126      * Silence all non-output and non-error messages.
127      */
128     @Parameter(defaultValue = "false", property = "repack.quiet")
129     public boolean quiet = false;
130 
131     /**
132      * Do a summary report.
133      */
134     @Parameter(defaultValue = "true", property = "repack.report")
135     public boolean report = true;
136 
137     /**
138      * Classifier to add to the repacked archive. Use the blank string to replace the main artifact.
139      */
140     @Parameter(defaultValue = "repacked", property = "repack.classifier")
141     public String repackClassifier = "repacked";
142 
143     /**
144      * Attach the repacked archive to the build cycle.
145      */
146     @Parameter(defaultValue = "true", property = "repack.attach-artifact")
147     public boolean attachRepackedArtifact = true;
148 
149     /**
150      * A list of the libraries that must be unpacked at runtime (do not work within the fat jar).
151      */
152     @Parameter
153     public Set<DependencyDefinition> runtimeUnpackedDependencies = ImmutableSet.of();
154 
155     /**
156      * A list of optional libraries that should be included even if optional dependencies are not included by default.
157      */
158     @Parameter
159     public Set<DependencyDefinition> optionalDependencies = ImmutableSet.of();
160 
161     /**
162      * Timestamp for reproducible output archive entries, either formatted as ISO 8601 (<code>yyyy-MM-dd'T'HH:mm:ssXXX</code>) or an {@code int} representing
163      * seconds since the epoch.
164      */
165     @Parameter(defaultValue = "${project.build.outputTimestamp}", property = "repack.output-timestamp")
166     public String outputTimestamp;
167 
168     /**
169      * The type of archive (which corresponds to how the dependencies are laid out inside it). Possible values are {@code JAR}, {@code WAR}, {@code ZIP},
170      * {@code DIR}, {@code NONE}. Defaults to {@code JAR}.
171      */
172     @Parameter(defaultValue = "JAR", property = "repack.layout")
173     public LayoutType layout = LayoutType.JAR;
174 
175     /**
176      * The layout factory that will be used to create the executable archive if no explicit layout is set. Alternative layouts implementations can be provided
177      * by 3rd parties.
178      */
179     @Parameter
180     public LayoutFactory layoutFactory = null;
181 
182     // called by maven
183     public void setIncludedDependencies(final String... includedDependencies) {
184         checkNotNull(includedDependencies, "includedDependencies is null");
185 
186         this.includedDependencies = Arrays.stream(includedDependencies)
187                 .map(DependencyDefinition::new)
188                 .collect(toImmutableSet());
189     }
190 
191     // called by maven
192     public void setExcludedDependencies(final String... excludedDependencies) {
193         checkNotNull(excludedDependencies, "excludedDependencies is null");
194 
195         this.excludedDependencies = Arrays.stream(excludedDependencies)
196                 .map(DependencyDefinition::new)
197                 .collect(toImmutableSet());
198     }
199 
200     // called by maven
201     public void setRuntimeUnpackedDependencies(final String... runtimeUnpackedDependencies) {
202         checkNotNull(runtimeUnpackedDependencies, "runtimeUnpackDependencies is null");
203 
204         this.runtimeUnpackedDependencies = Arrays.stream(runtimeUnpackedDependencies)
205                 .map(DependencyDefinition::new)
206                 .collect(toImmutableSet());
207     }
208 
209     // called by maven
210     public void setOptionalDependencies(final String... optionalDependencies) {
211         checkNotNull(optionalDependencies, "optionalDependencies is null");
212 
213         this.optionalDependencies = Arrays.stream(optionalDependencies)
214                 .map(DependencyDefinition::new)
215                 .collect(toImmutableSet());
216     }
217 
218     @Override
219     public void execute() throws MojoExecutionException {
220 
221         if (skip) {
222             LOG.report(quiet, "Skipping plugin execution");
223             return;
224         }
225 
226         if ("pom".equals(project.getPackaging())) {
227             LOG.report(quiet, "Ignoring POM project");
228             return;
229         }
230 
231         checkState(this.outputDirectory != null, "output directory was unset!");
232         checkState(this.outputDirectory.exists(), "output directory '%s' does not exist!", this.outputDirectory.getAbsolutePath());
233 
234         if (Strings.nullToEmpty(finalName).isBlank()) {
235             this.finalName = project.getArtifactId() + '-' + project.getVersion();
236             LOG.report(quiet, "Final name unset, falling back to %s", this.finalName);
237         }
238 
239         if (Strings.nullToEmpty(repackClassifier).isBlank()) {
240             if (Strings.nullToEmpty(project.getArtifact().getClassifier()).isBlank()) {
241                 LOG.report(quiet, "Repacked archive will replace main artifact");
242             } else {
243                 LOG.report(quiet, "Repacked archive will have no classifier, main artifact has classifier '%s'", project.getArtifact().getClassifier());
244             }
245         } else {
246             if (repackClassifier.equals(project.getArtifact().getClassifier())) {
247                 LOG.report(quiet, "Repacked archive will replace main artifact using classifier '%s'", repackClassifier);
248             } else {
249                 LOG.report(quiet, "Repacked archive will use classifier '%s', main artifact has %s", repackClassifier,
250                         project.getArtifact().getClassifier() == null ? "no classifier" : "classifier '" + project.getArtifact().getClassifier() + "'");
251             }
252         }
253 
254         try {
255             Artifact source = project.getArtifact();
256 
257             Repackager repackager = new Repackager(source.getFile());
258 
259             if (mainClass != null && !mainClass.isEmpty()) {
260                 repackager.setMainClass(mainClass);
261             } else {
262                 repackager.addMainClassTimeoutWarningListener((duration, mainMethod) ->
263                         LOG.warn("Searching for the main class is taking some time, "
264                                 + "consider using the mainClass configuration parameter."));
265             }
266 
267             if (layoutFactory != null) {
268                 LOG.report(quiet, "Using %s Layout Factory to repack the %s artifact.", layoutFactory.getClass().getSimpleName(), project.getArtifact());
269                 repackager.setLayoutFactory(layoutFactory);
270             } else if (layout != null) {
271                 LOG.report(quiet, "Using %s Layout to repack the %s artifact.", layout, project.getArtifact());
272                 repackager.setLayout(layout.layout());
273             } else {
274                 LOG.warn("Neither Layout Factory nor Layout defined, resulting archive may be non-functional.");
275             }
276 
277             repackager.setLayers(Layers.IMPLICIT);
278             // tools need spring framework dependencies which are not guaranteed to be there. So turn this off.
279             repackager.setIncludeRelevantJarModeJars(false);
280 
281             File targetFile = getTargetFile();
282             Libraries libraries = getLibraries();
283             FileTime outputFileTimestamp = parseOutputTimestamp();
284 
285             repackager.repackage(targetFile, libraries, null, outputFileTimestamp);
286 
287             boolean repackReplacesSource = source.getFile().equals(targetFile);
288 
289             if (attachRepackedArtifact) {
290                 if (repackReplacesSource) {
291                     source.setFile(targetFile);
292                 } else {
293                     projectHelper.attachArtifact(project, project.getPackaging(), Strings.emptyToNull(repackClassifier), targetFile);
294                 }
295             } else if (repackReplacesSource && repackager.getBackupFile().exists()) {
296                 source.setFile(repackager.getBackupFile());
297             } else if (!repackClassifier.isEmpty()) {
298                 LOG.report(quiet, "Created repacked archive %s with classifier %s!", targetFile, repackClassifier);
299             }
300 
301             if (report) {
302                 Reporter.report(quiet, source, repackClassifier);
303             }
304         } catch (IOException ex) {
305             throw new MojoExecutionException(ex.getMessage(), ex);
306         }
307     }
308 
309     private File getTargetFile() {
310         StringBuilder targetFileName = new StringBuilder();
311 
312         targetFileName.append(finalName);
313 
314         if (!repackClassifier.isEmpty()) {
315             targetFileName.append('-').append(repackClassifier);
316         }
317 
318         targetFileName.append('.').append(project.getArtifact().getArtifactHandler().getExtension());
319 
320         return new File(outputDirectory, targetFileName.toString());
321     }
322 
323     /**
324      * Return {@link Libraries} that the packager can use.
325      */
326     private Libraries getLibraries() throws MojoExecutionException {
327 
328         try {
329             Set<Artifact> artifacts = ImmutableSet.copyOf(project.getArtifacts());
330             Set<Artifact> includedArtifacts = ImmutableSet.copyOf(buildFilters().filter(artifacts));
331             return new ArtifactsLibraries(quiet, artifacts, includedArtifacts, session.getProjects(), runtimeUnpackedDependencies);
332         } catch (ArtifactFilterException ex) {
333             throw new MojoExecutionException(ex.getMessage(), ex);
334         }
335     }
336 
337     private FilterArtifacts buildFilters() {
338 
339         FilterArtifacts filters = new FilterArtifacts();
340 
341         // remove all system scope artifacts
342         if (!includeSystemScope) {
343             filters.addFilter(new ScopeExclusionFilter(Artifact.SCOPE_SYSTEM));
344         }
345 
346         // remove all provided scope artifacts
347         if (!includeProvidedScope) {
348             filters.addFilter(new ScopeExclusionFilter(Artifact.SCOPE_PROVIDED));
349         }
350 
351         // if optionals are not included by default, filter out anything that is not included
352         // through a matcher
353         if (!includeOptional) {
354             filters.addFilter(new OptionalArtifactFilter(optionalDependencies));
355         }
356 
357         // add includes filter. If no includes are given, don't add a filter (everything is included)
358         if (!includedDependencies.isEmpty()) {
359             // an explicit include list given.
360             filters.addFilter(new DependencyDefinitionFilter(includedDependencies, true));
361         }
362 
363         // add excludes filter. If no excludes are given, don't add a filter (nothing gets excluded)
364         if (!excludedDependencies.isEmpty()) {
365             filters.addFilter(new DependencyDefinitionFilter(excludedDependencies, false));
366         }
367 
368         return filters;
369     }
370 
371     private FileTime parseOutputTimestamp() {
372         // Maven ignore a single-character timestamp as it is "useful to override a full
373         // value during pom inheritance"
374         if (outputTimestamp == null || outputTimestamp.length() < 2) {
375             return null;
376         }
377 
378         long timestamp;
379 
380         try {
381             timestamp = Long.parseLong(outputTimestamp);
382         } catch (NumberFormatException ex) {
383             timestamp = OffsetDateTime.parse(outputTimestamp).toInstant().getEpochSecond();
384         }
385 
386         return FileTime.from(timestamp, TimeUnit.SECONDS);
387     }
388 }