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 MavenProject project;
64
65 @Parameter(defaultValue = "${session}", readonly = true, required = true)
66 MavenSession session;
67
68 @Component
69 MavenProjectHelper projectHelper;
70
71
72
73
74 @Parameter(property = "repack.main-class")
75 String mainClass = null;
76
77
78
79
80 private Set<DependencyDefinition> includedDependencies = ImmutableSet.of();
81
82
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
94
95 private Set<DependencyDefinition> excludedDependencies = ImmutableSet.of();
96
97
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
109
110 @Parameter(defaultValue = "false", property = "repack.include-system-scope")
111 boolean includeSystemScope = false;
112
113
114
115
116 @Parameter(defaultValue = "false", property = "repack.include-provided-scope")
117 boolean includeProvidedScope = false;
118
119
120
121
122 @Parameter(defaultValue = "false", property = "repack.include-optional")
123 boolean includeOptional = false;
124
125
126
127
128 @Parameter(defaultValue = "${project.build.directory}", property = "repack.output-directory")
129 File outputDirectory;
130
131
132
133
134 @Parameter(defaultValue = "${project.build.finalName}", property = "repack.final-name")
135 String finalName;
136
137
138
139
140 @Parameter(defaultValue = "false", property = "repack.skip")
141 boolean skip = false;
142
143
144
145
146 @Parameter(defaultValue = "false", property = "repack.quiet")
147 boolean quiet = false;
148
149
150
151
152 @Parameter(defaultValue = "true", property = "repack.report")
153 boolean report = true;
154
155
156
157
158 @Parameter(defaultValue = "repacked", property = "repack.classifier")
159 String repackClassifier = "repacked";
160
161
162
163
164 @Parameter(defaultValue = "true", property = "repack.attach-artifact")
165 boolean attachRepackedArtifact = true;
166
167
168
169
170 private Set<DependencyDefinition> runtimeUnpackedDependencies = ImmutableSet.of();
171
172
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
184
185 private Set<DependencyDefinition> optionalDependencies = ImmutableSet.of();
186
187
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
199
200
201 @Parameter(defaultValue = "${project.build.outputTimestamp}", property = "repack.output-timestamp")
202 String outputTimestamp;
203
204
205
206
207
208 @Parameter(defaultValue = "JAR", property = "repack.layout")
209 LayoutType layout = LayoutType.JAR;
210
211
212
213
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
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
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
343 if (!includeSystemScope) {
344 filters.addFilter(new ScopeExclusionFilter(Artifact.SCOPE_SYSTEM));
345 }
346
347
348 if (!includeProvidedScope) {
349 filters.addFilter(new ScopeExclusionFilter(Artifact.SCOPE_PROVIDED));
350 }
351
352
353
354 if (!includeOptional) {
355 filters.addFilter(new OptionalArtifactFilter(optionalDependencies));
356 }
357
358
359 if (!includedDependencies.isEmpty()) {
360
361 filters.addFilter(new DependencyDefinitionFilter(includedDependencies, true));
362 }
363
364
365 if (!excludedDependencies.isEmpty()) {
366 filters.addFilter(new DependencyDefinitionFilter(excludedDependencies, false));
367 }
368
369 return filters;
370 }
371
372 private FileTime parseOutputTimestamp() {
373
374
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 }