001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014 015package org.basepom.mojo.repack; 016 017import static com.google.common.base.Preconditions.checkNotNull; 018import static com.google.common.base.Preconditions.checkState; 019import static com.google.common.collect.ImmutableSet.toImmutableSet; 020 021import java.io.File; 022import java.io.IOException; 023import java.nio.file.attribute.FileTime; 024import java.time.OffsetDateTime; 025import java.util.Arrays; 026import java.util.Set; 027import java.util.concurrent.TimeUnit; 028import java.util.regex.Pattern; 029 030import com.google.common.base.Strings; 031import com.google.common.collect.ImmutableSet; 032import org.apache.maven.artifact.Artifact; 033import org.apache.maven.execution.MavenSession; 034import org.apache.maven.plugin.AbstractMojo; 035import org.apache.maven.plugin.MojoExecutionException; 036import org.apache.maven.plugins.annotations.Component; 037import org.apache.maven.plugins.annotations.LifecyclePhase; 038import org.apache.maven.plugins.annotations.Mojo; 039import org.apache.maven.plugins.annotations.Parameter; 040import org.apache.maven.plugins.annotations.ResolutionScope; 041import org.apache.maven.project.MavenProject; 042import org.apache.maven.project.MavenProjectHelper; 043import org.apache.maven.shared.artifact.filter.collection.ArtifactFilterException; 044import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts; 045import org.springframework.boot.loader.tools.Layers; 046import org.springframework.boot.loader.tools.LayoutFactory; 047import org.springframework.boot.loader.tools.Libraries; 048import org.springframework.boot.loader.tools.Repackager; 049 050/** 051 * 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>. 052 */ 053@Mojo(name = "repack", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, 054 requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, 055 requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) 056public final class RepackMojo extends AbstractMojo { 057 058 private static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("\\s+"); 059 060 private static final PluginLog LOG = new PluginLog(RepackMojo.class); 061 062 @Parameter(defaultValue = "${project}", readonly = true, required = true) 063 MavenProject project; 064 065 @Parameter(defaultValue = "${session}", readonly = true, required = true) 066 MavenSession session; 067 068 @Component 069 MavenProjectHelper projectHelper; 070 071 /** 072 * The name of the main class. If not specified the first compiled class found that contains a {@code main} method will be used. 073 */ 074 @Parameter(property = "repack.main-class") 075 String mainClass = null; 076 077 /** 078 * Collection of artifact definitions to include. 079 */ 080 private Set<DependencyDefinition> includedDependencies = ImmutableSet.of(); 081 082 // called by maven 083 @Parameter(alias = "includes") 084 public void setIncludedDependencies(final String... includedDependencies) { 085 checkNotNull(includedDependencies, "includedDependencies is null"); 086 087 this.includedDependencies = Arrays.stream(includedDependencies) 088 .map(DependencyDefinition::new) 089 .collect(toImmutableSet()); 090 } 091 092 /** 093 * Collection of artifact definitions to exclude. 094 */ 095 private Set<DependencyDefinition> excludedDependencies = ImmutableSet.of(); 096 097 // called by maven 098 @Parameter(alias = "excludedDependencies") 099 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}