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      MavenProject project;
64  
65      @Parameter(defaultValue = "${session}", readonly = true, required = true)
66      MavenSession session;
67  
68      @Component
69      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      String mainClass = null;
76  
77      /**
78       * Collection of artifact definitions to include.
79       */
80      private Set<DependencyDefinition> includedDependencies = ImmutableSet.of();
81  
82      // called by maven
83      @Parameter(alias = "includes")
84      public void setIncludedDependencies(final String... includedDependencies) {
85          checkNotNull(includedDependencies, "includedDependencies is null");
86  
87          this.includedDependencies = Arrays.stream(includedDependencies)
88                  .map(DependencyDefinition::new)
89                  .collect(toImmutableSet());
90      }
91  
92      /**
93       * Collection of artifact definitions to exclude.
94       */
95      private Set<DependencyDefinition> excludedDependencies = ImmutableSet.of();
96  
97      // called by maven
98      @Parameter(alias = "excludedDependencies")
99      public void setExcludedDependencies(final String... excludedDependencies) {
100         checkNotNull(excludedDependencies, "excludedDependencies is null");
101 
102         this.excludedDependencies = Arrays.stream(excludedDependencies)
103                 .map(DependencyDefinition::new)
104                 .collect(toImmutableSet());
105     }
106 
107     /**
108      * Include system scoped dependencies.
109      */
110     @Parameter(defaultValue = "false", property = "repack.include-system-scope")
111     boolean includeSystemScope = false;
112 
113     /**
114      * Include provided scoped dependencies.
115      */
116     @Parameter(defaultValue = "false", property = "repack.include-provided-scope")
117     boolean includeProvidedScope = false;
118 
119     /**
120      * Include optional dependencies
121      */
122     @Parameter(defaultValue = "false", property = "repack.include-optional")
123     boolean includeOptional = false;
124 
125     /**
126      * Directory containing the generated archive.
127      */
128     @Parameter(defaultValue = "${project.build.directory}", property = "repack.output-directory")
129     File outputDirectory;
130 
131     /**
132      * Name of the generated archive.
133      */
134     @Parameter(defaultValue = "${project.build.finalName}", property = "repack.final-name")
135     String finalName;
136 
137     /**
138      * Skip the execution.
139      */
140     @Parameter(defaultValue = "false", property = "repack.skip")
141     boolean skip = false;
142 
143     /**
144      * Silence all non-output and non-error messages.
145      */
146     @Parameter(defaultValue = "false", property = "repack.quiet")
147     boolean quiet = false;
148 
149     /**
150      * Do a summary report.
151      */
152     @Parameter(defaultValue = "true", property = "repack.report")
153     boolean report = true;
154 
155     /**
156      * Classifier to add to the repacked archive. Use the blank string to replace the main artifact.
157      */
158     @Parameter(defaultValue = "repacked", property = "repack.classifier")
159     String repackClassifier = "repacked";
160 
161     /**
162      * Attach the repacked archive to the build cycle.
163      */
164     @Parameter(defaultValue = "true", property = "repack.attach-artifact")
165     boolean attachRepackedArtifact = true;
166 
167     /**
168      * A list of the libraries that must be unpacked at runtime (do not work within the fat jar).
169      */
170     private Set<DependencyDefinition> runtimeUnpackedDependencies = ImmutableSet.of();
171 
172     // called by maven
173     @Parameter
174     public void setRuntimeUnpackedDependencies(final String... runtimeUnpackedDependencies) {
175         checkNotNull(runtimeUnpackedDependencies, "runtimeUnpackDependencies is null");
176 
177         this.runtimeUnpackedDependencies = Arrays.stream(runtimeUnpackedDependencies)
178                 .map(DependencyDefinition::new)
179                 .collect(toImmutableSet());
180     }
181 
182     /**
183      * A list of optional libraries that should be included even if optional dependencies are not included by default.
184      */
185     private Set<DependencyDefinition> optionalDependencies = ImmutableSet.of();
186 
187     // called by maven
188     @Parameter
189     public void setOptionalDependencies(final String... optionalDependencies) {
190         checkNotNull(optionalDependencies, "optionalDependencies is null");
191 
192         this.optionalDependencies = Arrays.stream(optionalDependencies)
193                 .map(DependencyDefinition::new)
194                 .collect(toImmutableSet());
195     }
196 
197     /**
198      * 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
199      * seconds since the epoch.
200      */
201     @Parameter(defaultValue = "${project.build.outputTimestamp}", property = "repack.output-timestamp")
202     String outputTimestamp;
203 
204     /**
205      * The type of archive (which corresponds to how the dependencies are laid out inside it). Possible values are {@code JAR}, {@code WAR}, {@code ZIP},
206      * {@code DIR}, {@code NONE}. Defaults to {@code JAR}.
207      */
208     @Parameter(defaultValue = "JAR", property = "repack.layout")
209     LayoutType layout = LayoutType.JAR;
210 
211     /**
212      * The layout factory that will be used to create the executable archive if no explicit layout is set. Alternative layouts implementations can be provided
213      * by 3rd parties.
214      */
215     @Parameter
216     LayoutFactory layoutFactory = null;
217 
218 
219     @Override
220     public void execute() throws MojoExecutionException {
221 
222         if (skip) {
223             LOG.report(quiet, "Skipping plugin execution");
224             return;
225         }
226 
227         if ("pom".equals(project.getPackaging())) {
228             LOG.report(quiet, "Ignoring POM project");
229             return;
230         }
231 
232         checkState(this.outputDirectory != null, "output directory was unset!");
233         checkState(this.outputDirectory.exists(), "output directory '%s' does not exist!", this.outputDirectory.getAbsolutePath());
234 
235         if (Strings.nullToEmpty(finalName).isBlank()) {
236             this.finalName = project.getArtifactId() + '-' + project.getVersion();
237             LOG.report(quiet, "Final name unset, falling back to %s", this.finalName);
238         }
239 
240         if (Strings.nullToEmpty(repackClassifier).isBlank()) {
241             if (Strings.nullToEmpty(project.getArtifact().getClassifier()).isBlank()) {
242                 LOG.report(quiet, "Repacked archive will replace main artifact");
243             } else {
244                 LOG.report(quiet, "Repacked archive will have no classifier, main artifact has classifier '%s'", project.getArtifact().getClassifier());
245             }
246         } else {
247             if (repackClassifier.equals(project.getArtifact().getClassifier())) {
248                 LOG.report(quiet, "Repacked archive will replace main artifact using classifier '%s'", repackClassifier);
249             } else {
250                 LOG.report(quiet, "Repacked archive will use classifier '%s', main artifact has %s", repackClassifier,
251                         project.getArtifact().getClassifier() == null ? "no classifier" : "classifier '" + project.getArtifact().getClassifier() + "'");
252             }
253         }
254 
255         try {
256             Artifact source = project.getArtifact();
257 
258             Repackager repackager = new Repackager(source.getFile());
259 
260             if (mainClass != null && !mainClass.isEmpty()) {
261                 repackager.setMainClass(mainClass);
262             } else {
263                 repackager.addMainClassTimeoutWarningListener((duration, mainMethod) ->
264                         LOG.warn("Searching for the main class is taking some time, "
265                                 + "consider using the mainClass configuration parameter."));
266             }
267 
268             if (layoutFactory != null) {
269                 LOG.report(quiet, "Using %s Layout Factory to repack the %s artifact.", layoutFactory.getClass().getSimpleName(), project.getArtifact());
270                 repackager.setLayoutFactory(layoutFactory);
271             } else if (layout != null) {
272                 LOG.report(quiet, "Using %s Layout to repack the %s artifact.", layout, project.getArtifact());
273                 repackager.setLayout(layout.layout());
274             } else {
275                 LOG.warn("Neither Layout Factory nor Layout defined, resulting archive may be non-functional.");
276             }
277 
278             repackager.setLayers(Layers.IMPLICIT);
279             // tools need spring framework dependencies which are not guaranteed to be there. So turn this off.
280             repackager.setIncludeRelevantJarModeJars(false);
281 
282             File targetFile = getTargetFile();
283             Libraries libraries = getLibraries();
284             FileTime outputFileTimestamp = parseOutputTimestamp();
285 
286             repackager.repackage(targetFile, libraries, null, outputFileTimestamp);
287 
288             boolean repackReplacesSource = source.getFile().equals(targetFile);
289 
290             if (attachRepackedArtifact) {
291                 if (repackReplacesSource) {
292                     source.setFile(targetFile);
293                 } else {
294                     projectHelper.attachArtifact(project, project.getPackaging(), Strings.emptyToNull(repackClassifier), targetFile);
295                 }
296             } else if (repackReplacesSource && repackager.getBackupFile().exists()) {
297                 source.setFile(repackager.getBackupFile());
298             } else if (!repackClassifier.isEmpty()) {
299                 LOG.report(quiet, "Created repacked archive %s with classifier %s!", targetFile, repackClassifier);
300             }
301 
302             if (report) {
303                 Reporter.report(quiet, source, repackClassifier);
304             }
305         } catch (IOException ex) {
306             throw new MojoExecutionException(ex.getMessage(), ex);
307         }
308     }
309 
310     private File getTargetFile() {
311         StringBuilder targetFileName = new StringBuilder();
312 
313         targetFileName.append(finalName);
314 
315         if (!repackClassifier.isEmpty()) {
316             targetFileName.append('-').append(repackClassifier);
317         }
318 
319         targetFileName.append('.').append(project.getArtifact().getArtifactHandler().getExtension());
320 
321         return new File(outputDirectory, targetFileName.toString());
322     }
323 
324     /**
325      * Return {@link Libraries} that the packager can use.
326      */
327     private Libraries getLibraries() throws MojoExecutionException {
328 
329         try {
330             Set<Artifact> artifacts = ImmutableSet.copyOf(project.getArtifacts());
331             Set<Artifact> includedArtifacts = ImmutableSet.copyOf(buildFilters().filter(artifacts));
332             return new ArtifactsLibraries(quiet, artifacts, includedArtifacts, session.getProjects(), runtimeUnpackedDependencies);
333         } catch (ArtifactFilterException ex) {
334             throw new MojoExecutionException(ex.getMessage(), ex);
335         }
336     }
337 
338     private FilterArtifacts buildFilters() {
339 
340         FilterArtifacts filters = new FilterArtifacts();
341 
342         // remove all system scope artifacts
343         if (!includeSystemScope) {
344             filters.addFilter(new ScopeExclusionFilter(Artifact.SCOPE_SYSTEM));
345         }
346 
347         // remove all provided scope artifacts
348         if (!includeProvidedScope) {
349             filters.addFilter(new ScopeExclusionFilter(Artifact.SCOPE_PROVIDED));
350         }
351 
352         // if optionals are not included by default, filter out anything that is not included
353         // through a matcher
354         if (!includeOptional) {
355             filters.addFilter(new OptionalArtifactFilter(optionalDependencies));
356         }
357 
358         // add includes filter. If no includes are given, don't add a filter (everything is included)
359         if (!includedDependencies.isEmpty()) {
360             // an explicit include list given.
361             filters.addFilter(new DependencyDefinitionFilter(includedDependencies, true));
362         }
363 
364         // add excludes filter. If no excludes are given, don't add a filter (nothing gets excluded)
365         if (!excludedDependencies.isEmpty()) {
366             filters.addFilter(new DependencyDefinitionFilter(excludedDependencies, false));
367         }
368 
369         return filters;
370     }
371 
372     private FileTime parseOutputTimestamp() {
373         // Maven ignore a single-character timestamp as it is "useful to override a full
374         // value during pom inheritance"
375         if (outputTimestamp == null || outputTimestamp.length() < 2) {
376             return null;
377         }
378 
379         long timestamp;
380 
381         try {
382             timestamp = Long.parseLong(outputTimestamp);
383         } catch (NumberFormatException ex) {
384             timestamp = OffsetDateTime.parse(outputTimestamp).toInstant().getEpochSecond();
385         }
386 
387         return FileTime.from(timestamp, TimeUnit.SECONDS);
388     }
389 }