All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.maven.plugin.jlink.JLinkResolver Maven / Gradle / Ivy

There is a newer version: 21.16.1.0
Show newest version
/*
 * Tentackle - https://tentackle.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.tentackle.maven.plugin.jlink;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.toolchain.Toolchain;
import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;

import org.tentackle.common.StringHelper;
import org.tentackle.common.ToolRunner;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Determines the strategy how to invoke jlink.
 */
public class JLinkResolver {

  /**
   * Jdeps output cache.
   * Speeds up the build process if multiple images are created within the same maven run.
   */
  private static final Map> JDEPS_MAP = new ConcurrentHashMap<>();


  /**
   * Holds the resolver results.
   */
  public class Result {

    private final List jlinkModules;
    private final List runtimeModuleNames;
    private final Set modulePath;
    private final Set classPath;
    private final Set extraClassPathElements;
    private boolean modular;
    private boolean isPlainModular;
    private String imagePathPrefix;

    public Result() {
      jlinkModules = new ArrayList<>();
      runtimeModuleNames = new ArrayList<>();
      modulePath = new LinkedHashSet<>();
      classPath = new LinkedHashSet<>();
      extraClassPathElements = new LinkedHashSet<>();
    }

    /**
     * Gets the prefix to the runtime image path.
     *
     * @return the path, empty if jlink image, else path to runtime image
     */
    public String getImagePathPrefix() {
      if (imagePathPrefix == null) {
        imagePathPrefix = mojo.getImagePathPrefix();
        if (!imagePathPrefix.isEmpty() && !imagePathPrefix.endsWith("/")) {
          imagePathPrefix += "/";
        }
      }
      return imagePathPrefix;
    }

    /**
     * Gets the modules to be passed to jlink.
     *
     * @return the modules
     */
    public List getJlinkModules() {
      return jlinkModules;
    }

    /**
     * Gets the modules to add to the module path explicitly.
     *
     * @return the modules not passed to jlink
     */
    public List getModulePath() {
      return new ArrayList<>(modulePath);
    }

    /**
     * Gets the artifacts to add to the class path explicitly.
     *
     * @return the class path
     */
    public List getClassPath() {
      return new ArrayList<>(classPath);
    }

    private void addJlinkModules(Collection modules) {
      jlinkModules.addAll(modules);
    }

    private void addRuntimeModuleNames(Collection moduleNames) {
      runtimeModuleNames.addAll(moduleNames);
    }

    private void addToModulePath(ModularArtifact module) {
      modulePath.add(module);
    }

    private void addToClassPath(Artifact artifact) {
      classPath.add(artifact);
    }

    private void setModular(boolean modular) {
      this.modular = modular;
    }

    /**
     * Returns whether this is a modular application.
     *
     * @return true if modular
     * @see #isPlainModular()
     */
    public boolean isModular() {
      return modular;
    }

    private void setPlainModular(boolean plainModular) {
      isPlainModular = plainModular;
    }

    /**
     * Returns whether this is a plain modular application.
* Plain modular means that all requirements are met by real modules. * If not plain, the jlink tool is used to build a runtime image only * and the application modules are placed on the module-path explicitly. * * @return true if plain modular, else mixed modular or classpath application * @see #isModular() */ public boolean isPlainModular() { return isPlainModular; } @Override public String toString() { StringBuilder buf = new StringBuilder(); if (!runtimeModuleNames.isEmpty()) { // Jlink only used to create the runtime w/o application modules buf.append("runtime modules passed to jlink: "); boolean needComma = false; for (String name: runtimeModuleNames) { if (needComma) { buf.append(", "); } else { needComma = true; } buf.append(name); } } if (!jlinkModules.isEmpty()) { buf.append("application modules passed to jlink: "); boolean needComma = false; for (ModularArtifact artifact: jlinkModules) { if (needComma) { buf.append(", "); } else { needComma = true; } buf.append(artifact.getModuleName()); } } if (!modulePath.isEmpty()) { buf.append("\nmodule-path passed to bin/java: "); boolean needComma = false; for (ModularArtifact artifact: modulePath) { if (needComma) { buf.append(", "); } else { needComma = true; } buf.append(artifact.getModuleName()); } } if (!classPath.isEmpty()) { buf.append("\nclasspath passed to bin/java: "); boolean needComma = false; for (Artifact artifact: classPath) { if (needComma) { buf.append(", "); } else { needComma = true; } buf.append(artifact.getFile().getName()); } } return buf.toString(); } /** * Adds additional elements to the classpath.
* Such as the conf directory. * * @param element the classpath element to add */ public void addToClasspath(String element) { extraClassPathElements.add(element); } /** * Gets the additional elements added to the classpath. * * @return the extra classpath elements */ public List getExtraClassPathElements() { return new ArrayList<>(extraClassPathElements); } /** * Generates the module-path option to be passed to jlink. * * @param jlinkRunner the jlink tool runner */ public void generateJlinkModulePath(ToolRunner jlinkRunner) { if (!jlinkModules.isEmpty()) { jlinkRunner.arg("--module-path"); boolean needSep = false; StringBuilder buf = new StringBuilder(); for (ModularArtifact module : jlinkModules) { if (needSep) { buf.append(File.pathSeparatorChar); } else { needSep = true; } buf.append(module.getPath()); } jlinkRunner.arg(buf); } } /** * Generates the module names option passed to jlink. * * @param jlinkRunner the jlink tool runner */ public void generateJlinkModules(ToolRunner jlinkRunner) { if (!runtimeModuleNames.isEmpty() || !jlinkModules.isEmpty()) { jlinkRunner.arg("--add-modules"); boolean needComma = false; StringBuilder buf = new StringBuilder(); for (String name : runtimeModuleNames) { if (needComma) { buf.append(','); } else { needComma = true; } buf.append(name); } for (ModularArtifact module : jlinkModules) { if (needComma) { buf.append(','); } else { needComma = true; } buf.append(module.getModuleName()); } jlinkRunner.arg(buf); } } /** * Returns whether the module path is not empty. * * @return true if module-path must be configured */ public boolean isModulePathRequired() { return modular && !modulePath.isEmpty(); } /** * Generates the module path option to be passed to bin/java. * * @return the option, empty if no extra module path */ public String generateModulePath() { StringBuilder buf = new StringBuilder(); if (modular) { boolean needSep = false; for (ModularArtifact artifact : modulePath) { if (needSep) { buf.append(File.pathSeparatorChar); } else { needSep = true; } buf.append(getImagePathPrefix()).append(AbstractJLinkMojo.DEST_MODULEPATH).append('/').append(artifact.getFileName()); } } return buf.toString(); } /** * Gets a modular artifact by its name. * * @param moduleName the module name * @return the artifact, null if no such artifact */ public ModularArtifact getModuleArtifact(String moduleName) { if (modular) { for (ModularArtifact artifact : modulePath) { if (artifact.getModuleName().equals(moduleName)) { return artifact; } } for (ModularArtifact artifact : jlinkModules) { if (artifact.getModuleName().equals(moduleName)) { return artifact; } } } return null; } /** * Returns whether the class path is not empty. * * @return true if classpath must be configured */ public boolean isClassPathRequired() { return !extraClassPathElements.isEmpty() || !classPath.isEmpty(); } /** * Generates the class path option to be passed to bin/java. * * @return the option, empty if no extra class path */ public String generateClassPath() { StringBuilder buf = new StringBuilder(); boolean needSep = false; String prefix = getImagePathPrefix(); for (String element : extraClassPathElements) { if (needSep) { buf.append(File.pathSeparatorChar); } else { needSep = true; } if (".".equals(element) && !prefix.isEmpty()) { // translate dot prefixed by a directory to the directory alone (without trailing slash) buf.append(prefix, 0, prefix.length() - 1); } else { buf.append(prefix).append(element); } } for (Artifact artifact : classPath) { if (needSep) { buf.append(File.pathSeparatorChar); } else { needSep = true; } buf.append(prefix).append(AbstractJLinkMojo.DEST_CLASSPATH).append('/').append(artifact.getFile().getName()); } return buf.toString(); } } private final Set artifacts; // project's dependencies (maven artifacts) private final AbstractJLinkMojo mojo; // the jlink mojo private final Map automaticModules; // : private final Map modules; // : private final Set requiredModules; // all required application modules private final Set runtimeModules; // all required java runtime modules /** * Creates a resolver. * * @param mojo the base mojo * @param artifacts the maven artifacts */ public JLinkResolver(AbstractJLinkMojo mojo, Set artifacts) { this.artifacts = filterArtifacts(artifacts); this.mojo = mojo; automaticModules = new LinkedHashMap<>(); modules = new LinkedHashMap<>(); requiredModules = new HashSet<>(); runtimeModules = new HashSet<>(); } /** * Resolves the maven artifacts for passing to jlink. * * @return the resolver result * @throws MojoExecutionException if loading artifacts failed */ public Result resolve() throws MojoExecutionException { Result result = new Result(); for (Map.Entry entry: loadArtifacts().entrySet()) { Artifact artifact = entry.getKey(); JavaModuleDescriptor moduleDescriptor = entry.getValue(); String moduleName = moduleDescriptor.name(); if (isEmptyFxDummyArtifact(moduleDescriptor)) { mojo.getLog().debug(moduleName + " is an automatic dummy javafx module -> skipped"); } else if (moduleDescriptor.isAutomatic()) { mojo.getLog().debug(moduleName + " is an automatic module"); automaticModules.put(moduleName, new ModularArtifact(artifact, moduleDescriptor)); } else if (mojo.isClasspathDependency(artifact)) { mojo.getLog().info("full-blown module " + moduleName + " moved to the class-path"); automaticModules.put(moduleName, new ModularArtifact(artifact, moduleDescriptor)); } else if (mojo.isModulePathOnly(moduleName)) { mojo.getLog().info("full-blown module " + moduleName + " moved to the module-path"); automaticModules.put(moduleName, new ModularArtifact(artifact, moduleDescriptor)); } else { mojo.getLog().debug(moduleName + " is a full-blown module"); modules.put(moduleName, new ModularArtifact(artifact, moduleDescriptor)); addRequired(moduleName); for (JavaModuleDescriptor.JavaRequires requires : moduleDescriptor.requires()) { if (requires.modifiers() == null || !requires.modifiers().contains(JavaModuleDescriptor.JavaRequires.JavaModifier.STATIC)) { addRequired(requires.name()); mojo.getLog().debug(moduleName + " requires " + requires.name()); } } } } // move module requirements that start with java. or jdk. back from the runtimeModules to the requiredModules // those are old school modules (like java.mail) that are not runtime modules but simple dependencies for (Iterator iter = runtimeModules.iterator(); iter.hasNext(); ) { String moduleName = iter.next(); if (automaticModules.get(moduleName) != null || modules.get(moduleName) != null) { iter.remove(); requiredModules.add(moduleName); } } result.setModular(isModularApplication()); if (result.isModular()) { mojo.getLog().debug("building a modular application"); // check if all requirements are met by real modules. // this is the only way to use jlink for application modules. Set unresolvedModules = new LinkedHashSet<>(); for (String requiredModule : requiredModules) { if (modules.get(requiredModule) == null) { ModularArtifact artifact = automaticModules.get(requiredModule); if (artifact == null) { throw new MojoExecutionException("required module not found in dependencies: " + requiredModule); } unresolvedModules.add(artifact); } } if (unresolvedModules.isEmpty() && !mojo.isModulePathOnly()) { mojo.getLog().debug("YEAH! can build a full-blown modular application!"); result.addJlinkModules(modules.values()); result.setPlainModular(true); } else { for (ModularArtifact unresolvedModule: unresolvedModules) { if (mojo.isClasspathDependency(unresolvedModule.getArtifact())) { // moved to the classpath explicitly result.addToClassPath(unresolvedModule.getArtifact()); } else { mojo.getLog().info("required " + unresolvedModule + " is not a full-blown module"); result.addToModulePath(unresolvedModule); // must go to module path! } } mojo.getLog().info("linking runtime modules only: project dependencies moved to the module-path"); result.addRuntimeModuleNames(runtimeModules); for (ModularArtifact artifact: modules.values()) { addToModuleOrClasspath(artifact, result); } } } else { mojo.getLog().debug("building a classpath application"); result.addRuntimeModuleNames(runtimeModules); for (Artifact artifact: artifacts) { result.addToClassPath(artifact); } } // add modules not referenced by anyone List allModules = new ArrayList<>(); allModules.addAll(automaticModules.values()); // automatic come first because they may provide services allModules.addAll(modules.values()); for (ModularArtifact artifact: allModules) { String moduleName = artifact.getModuleName(); if (!moduleName.equals(mojo.getMainModule()) && !requiredModules.contains(moduleName)) { addToModuleOrClasspath(artifact, result); } } // add explicit additional modules, such as jdk.jcmd result.addRuntimeModuleNames(mojo.getAddModules()); // add explicit extra classpath elements if (mojo.getExtraClasspathElements() != null) { for (String element: mojo.getExtraClasspathElements()) { result.addToClasspath(element); } } return result; } private Set filterArtifacts(Set artifacts) { Set filteredArtifacts = new LinkedHashSet<>(); // keep order for (Artifact artifact : artifacts) { if (!"pom".equals(artifact.getType())) { // skip BOM/POM files (their deps are included in the artifact list) filteredArtifacts.add(artifact); } } return filteredArtifacts; } private Map loadArtifacts() throws MojoExecutionException { // build file to artifact map Map artifactMap = new LinkedHashMap<>(); // keep order for (Artifact artifact: artifacts) { File artifactFile = artifact.getFile(); artifactMap.put(artifactFile, artifact); } // resolve modules and automatic modules ResolvePathsRequest request = ResolvePathsRequest.ofFiles(artifactMap.keySet()); Toolchain toolchain = mojo.getToolchain(); try { File javaHomeDir = mojo.getJavaHome(toolchain); if (javaHomeDir != null) { request.setJdkHome(javaHomeDir); } } catch (MojoExecutionException mx) { // don't abort, just continue with the current JDK and hope the best mojo.getLog().warn(mx.getMessage()); } Map artifactJavaModuleDescriptorMap = new LinkedHashMap<>(); // keep order int maxNameLength = 0; try { for (Map.Entry entry: mojo.getLocationManager().resolvePaths(request).getPathElements().entrySet()) { File file = entry.getKey(); JavaModuleDescriptor descriptor = entry.getValue(); if (descriptor == null) { // there are cases that the descriptor is null for old artifacts with Java > 11.0.2 String name = mojo.toDescriptorName(file); mojo.getLog().warn("missing descriptor for " + file + " -> creating default descriptor '" + name + "'"); descriptor = JavaModuleDescriptor.newAutomaticModule(name).build(); } if (descriptor.name() != null && descriptor.name().length() > maxNameLength) { maxNameLength = descriptor.name().length(); } artifactJavaModuleDescriptorMap.put(artifactMap.get(file), descriptor); } } catch (IOException e) { throw new MojoExecutionException("cannot resolve paths", e); } // run jdeps on non-JPMS modules for (Artifact artifact: artifacts) { JavaModuleDescriptor moduleDescriptor = artifactJavaModuleDescriptorMap.get(artifact); if (moduleDescriptor != null) { if (isEmptyFxDummyArtifact(moduleDescriptor)) { mojo.getLog().debug("empty dummy FX artifact " + moduleDescriptor.name() + " skipped"); } else { if (moduleDescriptor.isAutomatic()) { mojo.getLog().info(String.format("automatic module %-" + maxNameLength + "s %s", moduleDescriptor.name(), artifact)); runJdeps(artifact); } else { mojo.getLog().info(String.format("full-blown module %-" + maxNameLength + "s %s", moduleDescriptor.name(), artifact)); } } } else { mojo.getLog().info(String.format("artifact %-" + maxNameLength + "s %s", "", artifact)); runJdeps(artifact); } } return artifactJavaModuleDescriptorMap; } private boolean isModularApplication() throws MojoExecutionException { if (!StringHelper.isAllWhitespace(mojo.getMainModule())) { if (modules.get(mojo.getMainModule()) != null) { return true; } throw new MojoExecutionException("no such main module: " + mojo.getMainModule()); } return false; } private void runJdeps(Artifact artifact) throws MojoExecutionException { List lines = JDEPS_MAP.get(artifact); // no computeIfAbsent due to checked exception if (lines == null) { ToolRunner runner = new ToolRunner(mojo.getJdepsTool()).arg("--list-deps").arg("--ignore-missing-deps") .arg(artifact.getFile()).run(); int errCode = runner.getErrCode(); if (errCode != 0) { if (runner.getOutputAsString().contains("multi-release")) { runner = new ToolRunner(mojo.getJdepsTool()).arg("--list-deps").arg("--ignore-missing-deps") .arg("--multi-release").arg(mojo.getJavaMajorRuntimeVersion()) .arg(artifact.getFile()).run(); errCode = runner.getErrCode(); } if (errCode != 0) { throw new MojoExecutionException(runner + " failed for " + artifact + "\nerror code: " + errCode + "\nstdout: " + runner.getOutputAsString() + "\nstderr: " + runner.getErrorsAsString()); } } lines = runner.getOutput(); JDEPS_MAP.put(artifact, lines); } for (String line : lines) { line = line.trim(); if (!line.isEmpty() && !line.contains(" ")) { // a valid module name (no "not found" and such) mojo.getLog().debug(artifact + " (non-module): " + line); addRequired(line); } } } private void addRequired(String moduleName) { // remove optional package name, if any int ndx = moduleName.indexOf('/'); if (ndx > 0) { moduleName = moduleName.substring(0, ndx); } if (mojo.getExcludeModules().contains(moduleName)) { mojo.getLog().debug("module " + moduleName + " excluded"); } else { if (moduleName.startsWith("java.") || moduleName.startsWith("jdk.")) { // this is only a first guess. // if it turns out later that this is a dependency, it will be moved back to application artifacts runtimeModules.add(moduleName); } else { requiredModules.add(moduleName); } } } private void addToModuleOrClasspath(ModularArtifact artifact, Result result) { if (mojo.isClasspathDependency(artifact.getArtifact()) || !result.isModular()) { result.addToClassPath(artifact.getArtifact()); } else { result.addToModulePath(artifact); } } private boolean isEmptyFxDummyArtifact(JavaModuleDescriptor moduleDescriptor) { return moduleDescriptor.isAutomatic() && moduleDescriptor.name().startsWith("javafx.") && moduleDescriptor.name().endsWith("Empty"); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy