1
2
3
4
5
6
7
8
9
10
11
12
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
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
73
74 @Parameter(property = "repack.main-class")
75 public String mainClass = null;
76
77
78
79
80 @Parameter(alias = "includes")
81 public Set<DependencyDefinition> includedDependencies = ImmutableSet.of();
82
83
84
85
86 @Parameter(alias = "excludedDependencies")
87 public Set<DependencyDefinition> excludedDependencies = ImmutableSet.of();
88
89
90
91
92 @Parameter(defaultValue = "false", property = "repack.include-system-scope")
93 public boolean includeSystemScope = false;
94
95
96
97
98 @Parameter(defaultValue = "false", property = "repack.include-provided-scope")
99 public boolean includeProvidedScope = false;
100
101
102
103
104 @Parameter(defaultValue = "false", property = "repack.include-optional")
105 public boolean includeOptional = false;
106
107
108
109
110 @Parameter(defaultValue = "${project.build.directory}", property = "repack.output-directory")
111 public File outputDirectory;
112
113
114
115
116 @Parameter(defaultValue = "${project.build.finalName}", property = "repack.final-name")
117 public String finalName;
118
119
120
121
122 @Parameter(defaultValue = "false", property = "repack.skip")
123 public boolean skip = false;
124
125
126
127
128 @Parameter(defaultValue = "false", property = "repack.quiet")
129 public boolean quiet = false;
130
131
132
133
134 @Parameter(defaultValue = "true", property = "repack.report")
135 public boolean report = true;
136
137
138
139
140 @Parameter(defaultValue = "repacked", property = "repack.classifier")
141 public String repackClassifier = "repacked";
142
143
144
145
146 @Parameter(defaultValue = "true", property = "repack.attach-artifact")
147 public boolean attachRepackedArtifact = true;
148
149
150
151
152 @Parameter
153 public Set<DependencyDefinition> runtimeUnpackedDependencies = ImmutableSet.of();
154
155
156
157
158 @Parameter
159 public Set<DependencyDefinition> optionalDependencies = ImmutableSet.of();
160
161
162
163
164
165 @Parameter(defaultValue = "${project.build.outputTimestamp}", property = "repack.output-timestamp")
166 public String outputTimestamp;
167
168
169
170
171
172 @Parameter(defaultValue = "JAR", property = "repack.layout")
173 public LayoutType layout = LayoutType.JAR;
174
175
176
177
178
179 @Parameter
180 public LayoutFactory layoutFactory = null;
181
182
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
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
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
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
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
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
342 if (!includeSystemScope) {
343 filters.addFilter(new ScopeExclusionFilter(Artifact.SCOPE_SYSTEM));
344 }
345
346
347 if (!includeProvidedScope) {
348 filters.addFilter(new ScopeExclusionFilter(Artifact.SCOPE_PROVIDED));
349 }
350
351
352
353 if (!includeOptional) {
354 filters.addFilter(new OptionalArtifactFilter(optionalDependencies));
355 }
356
357
358 if (!includedDependencies.isEmpty()) {
359
360 filters.addFilter(new DependencyDefinitionFilter(includedDependencies, true));
361 }
362
363
364 if (!excludedDependencies.isEmpty()) {
365 filters.addFilter(new DependencyDefinitionFilter(excludedDependencies, false));
366 }
367
368 return filters;
369 }
370
371 private FileTime parseOutputTimestamp() {
372
373
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 }