![JAR search and dependency download from the Maven repository](/logo.png)
org.tentackle.maven.plugin.jlink.JLinkResolver Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tentackle-jlink-maven-plugin Show documentation
Show all versions of tentackle-jlink-maven-plugin Show documentation
Maven Plugin to Create Self-Contained Applications
/*
* 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