InlineMojo.java
- /*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.basepom.inline.mojo;
- import static com.google.common.base.Preconditions.checkState;
- import static java.lang.String.format;
- import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
- import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
- import org.basepom.inline.transformer.ClassPath;
- import org.basepom.inline.transformer.ClassPathResource;
- import org.basepom.inline.transformer.ClassPathTag;
- import org.basepom.inline.transformer.JarTransformer;
- import org.basepom.inline.transformer.TransformerException;
- import java.io.BufferedReader;
- import java.io.BufferedWriter;
- import java.io.File;
- import java.io.IOException;
- import java.io.UncheckedIOException;
- import java.nio.charset.StandardCharsets;
- import java.nio.file.Files;
- import java.util.List;
- import java.util.Map;
- import java.util.Optional;
- import java.util.Set;
- import java.util.function.BiConsumer;
- import java.util.function.Consumer;
- import java.util.function.Predicate;
- import java.util.jar.JarEntry;
- import java.util.jar.JarOutputStream;
- import java.util.stream.Collectors;
- import javax.xml.stream.XMLStreamException;
- import com.google.common.base.Functions;
- import com.google.common.base.Joiner;
- import com.google.common.collect.ImmutableList;
- import com.google.common.collect.ImmutableMap;
- import com.google.common.collect.ImmutableSet;
- import com.google.common.collect.ImmutableSetMultimap;
- import com.google.common.collect.ImmutableSortedSet;
- import com.google.common.collect.Iterables;
- import com.google.common.collect.Sets;
- import com.google.common.io.CharStreams;
- import org.apache.maven.execution.MavenSession;
- import org.apache.maven.plugin.AbstractMojo;
- import org.apache.maven.plugin.MojoExecutionException;
- import org.apache.maven.plugins.annotations.Component;
- import org.apache.maven.plugins.annotations.LifecyclePhase;
- import org.apache.maven.plugins.annotations.Mojo;
- import org.apache.maven.plugins.annotations.Parameter;
- import org.apache.maven.plugins.annotations.ResolutionScope;
- import org.apache.maven.project.DependencyResolutionException;
- import org.apache.maven.project.MavenProject;
- import org.apache.maven.project.MavenProjectHelper;
- import org.apache.maven.project.ProjectBuilder;
- import org.apache.maven.project.ProjectBuildingException;
- import org.apache.maven.project.ProjectDependenciesResolver;
- import org.eclipse.aether.RepositorySystem;
- import org.eclipse.aether.artifact.Artifact;
- import org.eclipse.aether.graph.Dependency;
- import org.eclipse.aether.util.artifact.JavaScopes;
- import org.jdom2.JDOMException;
- /**
- * Inlines one or more dependencies of the project, relocated the classes and writes a new artifact.
- */
- @Mojo(name = "inline", defaultPhase = LifecyclePhase.PACKAGE,
- requiresProject = true, threadSafe = true,
- requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
- requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
- public final class InlineMojo extends AbstractMojo {
- private static final PluginLog LOG = new PluginLog(InlineMojo.class);
- private static final Predicate<Dependency> EXCLUDE_SYSTEM_SCOPE = dependency -> !JavaScopes.SYSTEM.equals(dependency.getScope());
- private static final Predicate<Dependency> EXCLUDE_PROVIDED_SCOPE = dependency -> !JavaScopes.PROVIDED.equals(dependency.getScope());
- @Parameter(defaultValue = "${project}", readonly = true, required = true)
- private MavenProject project;
- @Parameter(defaultValue = "${session}", readonly = true, required = true)
- private MavenSession mavenSession;
- @Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
- private List<MavenProject> reactorProjects;
- @Component
- private ProjectBuilder mavenProjectBuilder;
- @Component
- private ProjectDependenciesResolver projectDependenciesResolver;
- @Component
- private RepositorySystem repositorySystem;
- @Component
- private MavenProjectHelper projectHelper;
- /**
- * The destination directory for the inlined artifact.
- */
- @Parameter(defaultValue = "${project.build.directory}")
- private File outputDirectory;
- /**
- * The POM file to use.
- */
- @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
- private File pomFile;
- /**
- * Direct dependencies to inline. Each dependency here must be
- * listed in the project POM. Any transitive dependency is added
- * to the final jar, unless it is in {@code RUNTIME} scope.
- * {@code RUNTIME} dependencies become a runtime dependency of the
- * resulting final jar <b>unless</b> they are listed here. In that
- * case, they are inlined in the final jar as well.
- */
- @Parameter
- private List<InlineDependency> inlineDependencies = ImmutableList.of();
- // called by maven
- public void setInlineDependencies(List<InlineDependency> inlineDependencies) {
- this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
- }
- /**
- * Include dependencies. A dependency is given as <tt>groupId:artifactId</tt>. The wildcard character '*' is supported for group id and artifact id.
- * <p>
- * Includes and excludes operate on the list of potential dependencies to inline. They can not be used to add additional dependencies that are not
- * listed in the <inlineDependency> elements.
- */
- @Parameter
- private List<ArtifactIdentifier> includes = ImmutableList.of();
- // called by maven
- public void setIncludes(List<String> includes) {
- this.includes = includes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
- }
- /**
- * Exclude dependencies from inclusion. A dependency is given as <tt>groupId:artifactId</tt>. Any transitive dependency that has been pulled in can be
- * excluded here. The wildcard character '*' is supported for group id and artifact id.
- * <p>
- * Includes and excludes operate on the list of potential dependencies to inline. They can not be used to add additional dependencies that are not
- * listed in the <inlineDependency> elements.
- */
- @Parameter
- private List<ArtifactIdentifier> excludes = ImmutableList.of();
- // called by maven
- public void setExcludes(List<String> excludes) {
- this.excludes = excludes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
- }
- /**
- * Adds external jar processors. These must be on the dependency path for the plugin. See the "Additional Processors" documentation.
- */
- @Parameter
- private List<String> additionalProcessors = ImmutableList.of();
- // called by maven
- public void setAdditionalProcessors(List<String> processors) {
- this.additionalProcessors = ImmutableList.copyOf(processors);
- }
- /**
- * Hide inlined classes from IDE autocompletion.
- */
- @Parameter(defaultValue = "true", property = "inline.hide-classes")
- private boolean hideClasses;
- /**
- * Skip the execution.
- */
- @Parameter(defaultValue = "false", property = "inline.skip")
- private boolean skip;
- /**
- * Silence all non-output and non-error messages.
- */
- @Parameter(defaultValue = "false", property = "inline.quiet")
- private boolean quiet;
- /**
- * Defines the package prefix for all relocated classes. This prefix must be a valid package name. All relocated classes are put under this prefix.
- */
- @Parameter(required = true, property = "inline.prefix")
- private String prefix;
- /**
- * Fail if an inline dependency is defined but the corresponding dependency is not actually found.
- */
- @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
- private boolean failOnNoMatch;
- /**
- * Fail if any duplicate exists after processing the contents.
- */
- @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
- private boolean failOnDuplicate;
- /**
- * The path to the output file for the inlined artifact. When this parameter is set, the created archive will neither replace the project's main artifact
- * nor will it be attached. Hence, this parameter causes the parameters {@link #inlinedArtifactAttached}, {@link #inlinedClassifierName} to be ignored when
- * used.
- */
- @Parameter
- private File outputJarFile;
- /**
- * The path to the output file for the new POM file. When this parameter is set, the created pom file will not replace the project's pom file.
- */
- @Parameter
- private File outputPomFile;
- /**
- * If true, attach the inlined artifact, if false replace the original artifact.
- */
- @Parameter(defaultValue = "false", property = "inline.attachArtifact")
- private boolean inlinedArtifactAttached;
- /**
- * If true, replace the POM file with a new version that has all inlined dependencies removed. It is possible to write a POM file that works to build the
- * jar with inlined dependencies and then use the same POM file for the resulting artifact (by having all dependencies marked as <tt>provided</tt> and
- * ensure that those dependencies do not have additional, transitive dependencies. This tends to be error prone and it is recommended to have the plugin
- * rewrite the POM file.
- */
- @Parameter(defaultValue = "true", property = "inline.replacePomFile")
- private boolean replacePomFile;
- /**
- * The name of the classifier used in case the inlined artifact is attached.
- */
- @Parameter(defaultValue = "inlined")
- private String inlinedClassifierName;
- @Override
- public void execute() throws MojoExecutionException {
- if (this.skip) {
- LOG.report(quiet, "skipping plugin execution");
- return;
- }
- if ("pom".equals(project.getPackaging())) {
- LOG.report(quiet, "ignoring POM project");
- return;
- }
- if (project.getArtifact().getFile() == null) {
- throw new MojoExecutionException("No project artifact found!");
- }
- try {
- ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyBuilder = ImmutableSetMultimap.builder();
- ImmutableSet.Builder<Dependency> pomDependenciesToAdd = ImmutableSet.builder();
- computeDependencyMap(dependencyBuilder, pomDependenciesToAdd);
- ImmutableSetMultimap<InlineDependency, Dependency> dependencyMap = dependencyBuilder.build();
- rewriteJarFile(dependencyMap);
- rewritePomFile(pomDependenciesToAdd.build(), ImmutableSet.copyOf(dependencyMap.values()));
- } catch (UncheckedIOException e) {
- throw new MojoExecutionException(e.getCause());
- } catch (TransformerException | IOException | DependencyResolutionException | ProjectBuildingException | XMLStreamException | JDOMException e) {
- throw new MojoExecutionException(e);
- }
- }
- private void computeDependencyMap(
- ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyMapBuilder,
- ImmutableSet.Builder<Dependency> pomBuilder)
- throws DependencyResolutionException, ProjectBuildingException {
- DependencyBuilder dependencyBuilder = new DependencyBuilder(project, mavenSession, mavenProjectBuilder, projectDependenciesResolver, reactorProjects);
- ImmutableSet<ArtifactIdentifier> directArtifacts = project.getDependencyArtifacts().stream()
- .map(ArtifactIdentifier::new)
- .collect(ImmutableSet.toImmutableSet());
- ImmutableList<Dependency> directDependencies = dependencyBuilder.mapProject(project,
- (node, parents) -> directArtifacts.contains(new ArtifactIdentifier(node)));
- // build the full set of dependencies with all scopes and everything.
- ImmutableList<Dependency> projectDependencies = dependencyBuilder.mapProject(project,
- ScopeLimitingFilter.computeDependencyScope(ScopeLimitingFilter.COMPILE_PLUS_RUNTIME));
- Map<String, Dependency> idMap = projectDependencies.stream()
- .filter(dependency -> dependency.getArtifact() != null)
- .collect(ImmutableMap.toImmutableMap(InlineMojo::getId, Functions.identity()));
- BiConsumer<InlineDependency, Dependency> dependencyConsumer = (inlineDependency, dependency) -> {
- LOG.debug("%s matches %s for inlining.", inlineDependency, dependency);
- dependencyMapBuilder.put(inlineDependency, dependency);
- };
- ImmutableSet.Builder<Dependency> directExcludes = ImmutableSet.builder();
- // first find all the direct dependencies. Add anything that is not hit to the additional exclude list
- ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();
- directDependencies.stream()
- // remove anything that does not match the filter set.
- // optionals also need to be matched by the inline dependency below
- .filter(createFilterSet(true))
- .forEach(dependency -> {
- Optional<InlineDependency> inlineDependency = findInlineDependencyMatch(dependency);
- if (inlineDependency.isPresent()) {
- dependencyConsumer.accept(inlineDependency.get(), dependency);
- directLogBuilder.add(dependency.toString());
- } else {
- directExcludes.add(dependency);
- }
- });
- ImmutableSortedSet<String> directLog = directLogBuilder.build();
- if (!quiet) {
- LOG.info("Inlined dependencies");
- LOG.info("====================");
- for (String dependency : directLog) {
- LOG.info(" %s", dependency);
- }
- LOG.info("");
- }
- Set<ArtifactIdentifier> excludes = directExcludes.build().stream()
- .map(ArtifactIdentifier::new)
- .collect(Collectors.toUnmodifiableSet());
- this.excludes = ImmutableList.copyOf(Iterables.concat(this.excludes, excludes));
- LOG.debug("Excludes after creating includes: %s", this.excludes);
- var directDependencyMap = dependencyMapBuilder.build().asMap();
- ImmutableSortedSet.Builder<String> transitiveLogBuilder = ImmutableSortedSet.naturalOrder();
- for (var dependencyEntry : directDependencyMap.entrySet()) {
- InlineDependency inlineDependency = dependencyEntry.getKey();
- for (Dependency projectDependency : dependencyEntry.getValue()) {
- Consumer<Dependency> consumer;
- if (inlineDependency.isInlineTransitive()) {
- // transitive deps are added to the jar
- consumer = dependency -> {
- Optional<InlineDependency> explicitMatch = findInlineDependencyMatch(dependency);
- // If the dependency is not a runtime dependency, it is included in the inline jar
- // Runtime dependencies are only included if they are explicitly listed as an
- // included dependency. Otherwise, they are added as a runtime dep to the inline jar.
- if (!JavaScopes.RUNTIME.equals(dependency.getScope()) || explicitMatch.isPresent()) {
- dependencyConsumer.accept(inlineDependency, dependency);
- transitiveLogBuilder.add(dependency.toString());
- } else {
- pomBuilder.add(dependency);
- }
- };
- } else {
- // non-transitive deps need to be written into the POM.
- consumer = pomBuilder::add;
- }
- dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
- .stream()
- // replace deps in the transitive set with deps in the root set if present (will
- // override the scope here with the root scope)
- .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
- // remove system and provided dependencies, keep optionals if allowed
- .filter(createFilterSet(inlineDependency.isInlineOptionals()))
- // make sure that the inline dependency actually pulls the dep in.
- .filter(this::isDependencyIncluded)
- .forEach(consumer);
- }
- }
- if (!quiet) {
- LOG.info("");
- LOG.info("Transitive dependencies");
- LOG.info("=======================");
- for (String dependency : Sets.difference(transitiveLogBuilder.build(), directLog)) {
- LOG.info(" %s", dependency);
- }
- LOG.info("");
- }
- }
- private Optional<InlineDependency> findInlineDependencyMatch(Dependency dependency) {
- for (InlineDependency inlineDependency : inlineDependencies) {
- if (inlineDependency.matchDependency(dependency)) {
- return Optional.of(inlineDependency);
- }
- }
- return Optional.empty();
- }
- private static String getId(Dependency dependency) {
- Artifact artifact = dependency.getArtifact();
- checkState(artifact != null, "Artifact for dependency %s is null!", dependency);
- return Joiner.on(':').join(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier());
- }
- private Predicate<Dependency> createFilterSet(boolean includeOptional) {
- // filter system scope dependencies. Those are never inlined.
- Predicate<Dependency> predicate = EXCLUDE_SYSTEM_SCOPE;
- predicate = predicate.and(EXCLUDE_PROVIDED_SCOPE);
- if (!includeOptional) {
- predicate = predicate.and(Predicate.not(Dependency::isOptional));
- }
- return predicate;
- }
- public boolean isDependencyIncluded(Dependency dependency) {
- boolean included = this.includes.stream()
- .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
- .findFirst()
- .orElse(this.includes.isEmpty());
- boolean excluded = this.excludes.stream()
- .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
- .findFirst()
- .orElse(false);
- return included && !excluded;
- }
- private void rewriteJarFile(ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
- File outputJar = (this.outputJarFile != null) ? outputJarFile : inlinedArtifactFileWithClassifier();
- doJarTransformation(outputJar, dependencies);
- if (this.outputJarFile == null) {
- if (this.inlinedArtifactAttached) {
- LOG.info("Attaching inlined artifact.");
- projectHelper.attachArtifact(project, project.getArtifact().getType(), inlinedClassifierName, outputJar);
- } else {
- LOG.info("Replacing original artifact with inlined artifact.");
- File originalArtifact = project.getArtifact().getFile();
- if (originalArtifact != null) {
- File backupFile = new File(originalArtifact.getParentFile(), "original-" + originalArtifact.getName());
- Files.move(originalArtifact.toPath(), backupFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
- Files.move(outputJar.toPath(), originalArtifact.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
- }
- }
- }
- }
- private void rewritePomFile(Set<Dependency> dependenciesToAdd, Set<Dependency> dependenciesToRemove) throws IOException, XMLStreamException, JDOMException {
- String pomContents;
- try (BufferedReader reader = Files.newBufferedReader(project.getFile().toPath(), StandardCharsets.UTF_8)) {
- pomContents = CharStreams.toString(reader);
- }
- PomUtil pomUtil = new PomUtil(pomContents);
- dependenciesToRemove.forEach(pomUtil::removeDependency);
- dependenciesToAdd.forEach(pomUtil::addDependency);
- // some rewriters (maven flatten plugin) rewrites the new pom name as a hidden file.
- String pomName = this.pomFile.getName();
- pomName = "new-" + (pomName.startsWith(".") ? pomName.substring(1) : pomName);
- File newPomFile = this.outputPomFile != null ? outputPomFile : new File(this.outputDirectory, pomName);
- try (BufferedWriter writer = Files.newBufferedWriter(newPomFile.toPath(), StandardCharsets.UTF_8)) {
- pomUtil.writePom(writer);
- }
- if (this.replacePomFile) {
- project.setPomFile(newPomFile);
- }
- }
- private void doJarTransformation(File outputJar, ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
- try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(outputJar.toPath()))) {
- Consumer<ClassPathResource> jarConsumer = getJarWriter(jarOutputStream);
- JarTransformer transformer = new JarTransformer(jarConsumer, true, ImmutableSet.copyOf(additionalProcessors));
- // Build the class path
- ClassPath classPath = new ClassPath(project.getBasedir());
- // maintain the manifest file for the main artifact
- var artifact = project.getArtifact();
- classPath.addFile(artifact.getFile(), artifact.getGroupId(), artifact.getArtifactId(), ClassPathTag.ROOT_JAR);
- dependencies.forEach(
- (inlineDependency, dependency) -> {
- var dependencyArtifact = dependency.getArtifact();
- checkState(dependencyArtifact.getFile() != null, "Could not locate artifact file for %s", dependencyArtifact);
- classPath.addFile(dependencyArtifact.getFile(), prefix, dependencyArtifact.getGroupId(), dependencyArtifact.getArtifactId(),
- hideClasses);
- });
- transformer.transform(classPath);
- }
- }
- private Consumer<ClassPathResource> getJarWriter(JarOutputStream jarOutputStream) {
- return classPathResource -> {
- try {
- String name = classPathResource.getName();
- LOG.debug(format("Writing '%s' to jar", name));
- JarEntry outputEntry = new JarEntry(name);
- outputEntry.setTime(classPathResource.getLastModifiedTime());
- outputEntry.setCompressedSize(-1);
- jarOutputStream.putNextEntry(outputEntry);
- jarOutputStream.write(classPathResource.getContent());
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- };
- }
- private File inlinedArtifactFileWithClassifier() {
- final var artifact = project.getArtifact();
- String inlineName = String.format("%s-%s-%s.%s",
- project.getArtifactId(),
- artifact.getVersion(),
- this.inlinedClassifierName,
- artifact.getArtifactHandler().getExtension());
- return new File(this.outputDirectory, inlineName);
- }
- }