net.dongliu.jlink.JLinkMojo Maven / Gradle / Ivy
package net.dongliu.jlink;
import net.dongliu.commons.Joiner;
import net.dongliu.commons.Strings;
import net.dongliu.commons.collection.Collections2;
import net.dongliu.jlink.model.JLinkLauncher;
import net.dongliu.jlink.model.JdkSetting;
import net.dongliu.jlink.util.ModuleInfo;
import net.dongliu.jlink.util.ProcessResult;
import net.dongliu.jlink.util.ProcessUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.*;
import org.apache.maven.project.MavenProject;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.codehaus.plexus.util.Os;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
/**
* @author dongliu
*/
@Mojo(name = "link", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class JLinkMojo extends AbstractMojo {
@Parameter(defaultValue = "${project}", readonly = true)
private MavenProject project;
@Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}.${project.packaging}")
private File projectArtifact;
@Parameter(property = "modulesDirectory", defaultValue = "${project.build.directory}/modules")
private File modulesDirectory;
@Parameter
private List jvmOptions;
@Parameter
private List excludeResources;
@Parameter
private List excludeFiles;
@Parameter
private boolean excludeDesktop;
@Component
private ToolchainManager toolchainManager;
@Parameter(defaultValue = "${session}", readonly = true)
private MavenSession mavenSession;
@Parameter
private JdkSetting baseJdk;
@Parameter
private List modulePaths;
@Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}")
private File output;
@Parameter(defaultValue = "${project.build.directory}/jlink_working")
private File workingDirectory;
@Parameter
private List addModules;
@Parameter
private boolean bindServices;
@Parameter
private List launchers;
@Parameter
private int compress;
@Parameter
private boolean stripDebug;
@Parameter
private boolean ignoreSigningInformation;
@Parameter
private boolean noHeaderFiles;
@Parameter
private boolean noManPages;
@Override
public void execute() throws MojoExecutionException {
String packaging = project.getModel().getPackaging();
if (!packaging.equalsIgnoreCase("jar")) {
getLog().error("require packaging type to be jar or jmod, '" + packaging + " not supported'");
return;
}
Path jmodsPath = getJlinkBaseJdk().resolve("jmods");
List finalModulePaths = new ArrayList<>();
if (modulePaths != null) {
for (String modulePath : modulePaths) {
finalModulePaths.add(Paths.get(modulePath));
}
}
if (!finalModulePaths.contains(jmodsPath)) {
finalModulePaths.add(jmodsPath);
}
if (!finalModulePaths.contains(modulesDirectory.toPath())) {
finalModulePaths.add(modulesDirectory.toPath());
}
copyModules();
try {
excludeDesktop(jmodsPath);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
DescribeModule describeModule = new DescribeModule(getTools("jar"), getTools("jmod"), projectArtifact.toPath());
ModuleInfo projectModuleInfo = describeModule.describe();
if (addModules == null) {
addModules = new ArrayList<>();
}
if (projectModuleInfo != null) {
final String name = projectModuleInfo.name();
if (!addModules.contains(name)) {
getLog().info("add project module: " + name);
addModules.add(name);
}
}
if (addModules.isEmpty()) {
throw new MojoExecutionException("add-modules empty");
}
getLog().info("creating jlink image at " + output.toString());
runJlink(finalModulePaths);
try {
tryAddJvmOptions();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void excludeDesktop(Path jmodsPath) throws MojoExecutionException, IOException {
if (!excludeDesktop) {
return;
}
Path desktopPath = jmodsPath.resolve("java.desktop.jmod");
String jmodPath = getTools("jmod").toString();
ProcessResult result = ProcessUtils.execute(jmodPath, "list", desktopPath.toString());
if (result.exitCode() != 0) {
getLog().error("describe java.desktop module error: " + result.stderr());
throw new MojoExecutionException("list desktop jmod failed, exit with: " + result.exitCode());
}
Set set = new HashSet<>();
List desktopExcludeResources = new ArrayList<>();
Path excludeResourcesPath = workingDirectory.toPath().resolve("desktop_exclude_resources.list");
getLog().info("generate desktop excluded resources to " + excludeResourcesPath);
for (String line : result.stdout().split("\n")) {
line = line.trim();
if (line.isEmpty() || !line.startsWith("classes/") || line.contains("/beans/")) {
continue;
}
line = line.substring("classes/".length());
String packageName = Strings.subStringBeforeLast(line, "/");
if (!set.contains(packageName)) {
String fileName = Strings.subStringAfterLast(line, "/");
String fileNameEscaped = fileName.replace(".", "\\.").replace("$", "\\$");
desktopExcludeResources.add("regex:/java.desktop/" + packageName + "/(?!" + fileNameEscaped + ")[_a-zA-Z0-9\\.\\$]+");
set.add(packageName);
}
}
Files.write(excludeResourcesPath, String.join("\n", desktopExcludeResources).getBytes());
if (excludeResources == null) {
excludeResources = new ArrayList<>();
}
excludeResources.add("@" + excludeResourcesPath);
if (excludeFiles == null) {
excludeFiles = new ArrayList<>();
}
excludeFiles.add("glob:/java.desktop/lib/*");
}
private void tryAddJvmOptions() throws IOException {
if (jvmOptions != null && !jvmOptions.isEmpty()) {
String jvmOptionStr = Joiner.of(" ").join(jvmOptions);
List paths = Files.list(output.toPath().resolve("bin")).collect(toList());
for (Path path : paths) {
String content = new String(Files.readAllBytes(path), ISO_8859_1);
if (content.contains("JLINK_VM_OPTIONS=")) {
getLog().info("add jvm options to launcher: " + path.getFileName());
content = content.replace("JLINK_VM_OPTIONS=", "JLINK_VM_OPTIONS=" + jvmOptionStr);
Files.write(path, content.getBytes(ISO_8859_1));
}
}
}
}
private Path getTools(String toolName) {
Toolchain toolchain = toolchainManager.getToolchainFromBuildContext("jdk", mavenSession);
Path toolPath;
if (toolchain != null) {
String toolPathStr = toolchain.findTool(toolName);
if (toolPathStr != null) {
toolPath = Paths.get(toolPathStr);
} else {
throw new RuntimeException("tools not found in tool chain: " + toolchain);
}
} else {
String javaHome = System.getProperty("java.home");
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
toolName = toolName + ".exe";
}
toolPath = Paths.get(javaHome, "bin", toolName);
}
return toolPath;
}
/**
* Returns the directory with the jmod files to be used for creating the image.
* If {@code baseJdk} has been given, the jmod files from the JDK identified that way
* will be used; otherwise the jmod files from the JDK running the current build
* will be used.
*/
private Path getJlinkBaseJdk() throws MojoExecutionException {
if (baseJdk != null) {
List toolChains = toolchainManager.getToolchains(mavenSession, "jdk", getToolChainRequirements(baseJdk));
if (toolChains.isEmpty()) {
throw new MojoExecutionException("Found no tool chain of type 'jdk' and matching requirements '" + baseJdk + "'");
} else if (toolChains.size() > 1) {
throw new MojoExecutionException("Found more than one tool chain of type 'jdk' and matching requirements '" + baseJdk + "'");
} else {
Toolchain toolchain = toolChains.get(0);
String javac = toolchain.findTool("javac");
// #63; when building on Linux / OS X but creating a Windows runtime image
// the tool lookup must be for javac.exe explicitly (as the toolchain mechanism
// itself won't append the suffix if not running this build on Windows
if (javac == null) {
javac = toolchain.findTool("javac.exe");
}
if (javac == null) {
throw new MojoExecutionException("Couldn't locate toolchain directory");
}
return new File(javac)
.toPath()
.getParent()
.getParent();
}
} else {
return Paths.get(System.getProperty("java.home"));
}
}
private Map getToolChainRequirements(JdkSetting baseJdk) {
Map requirements = new HashMap<>();
if (baseJdk.getVendor() != null) {
requirements.put("vendor", baseJdk.getVendor());
}
if (baseJdk.getPlatform() != null) {
requirements.put("platform", baseJdk.getPlatform());
}
if (baseJdk.getVersion() != null) {
requirements.put("version", baseJdk.getVersion());
}
return requirements;
}
private void copyModules() {
Path outputPath = modulesDirectory.toPath();
createDirectories();
Set dependencies = project.getArtifacts();
Path modulesBasePath = project.getBasedir().toPath().resolve("src/main/modules");
for (Artifact artifact : dependencies) {
String scope = artifact.getScope();
if (!scope.equals("compile") && !scope.equals("runtime")) {
continue;
}
String groupId = artifact.getGroupId();
String artifactId = artifact.getArtifactId();
Path inputFile = artifact.getFile().toPath();
Path moduleInfoPath = modulesBasePath.resolve(groupId).resolve(artifactId).resolve("module-info.java");
if (!Files.exists(moduleInfoPath)) {
getLog().debug("copy module " + artifact.getArtifactId());
Path outputJar = modulesDirectory.toPath().resolve(inputFile.getFileName());
try {
Files.copy(inputFile, outputJar, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException("Couldn't copy JAR file", e);
}
continue;
}
getLog().info("add module info to artifact: " + artifact.getArtifactId());
String moduleInfoSource;
try {
moduleInfoSource = new String(Files.readAllBytes(moduleInfoPath), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
AddModuleInfo addModuleInfo = new AddModuleInfo(
moduleInfoSource,
inputFile,
outputPath,
true
);
addModuleInfo.run();
}
Path projectJarPath = projectArtifact.toPath();
Path outputJar = modulesDirectory.toPath().resolve(projectJarPath.getFileName());
try {
Files.copy(projectJarPath, outputJar, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException("Couldn't copy JAR file", e);
}
}
private void runJlink(List modulePaths) throws AssertionError {
Path jlinkPath = getTools("jlink");
List command = new ArrayList<>();
command.add(jlinkPath.toString());
command.add("--add-modules");
command.add(String.join(",", addModules));
if (bindServices) {
command.add("--bind-services");
}
command.add("--module-path");
command.add(modulePaths.stream().map(Path::toString).collect(joining(":")));
command.add("--output");
command.add(output.toString());
if (launchers != null && !launchers.isEmpty()) {
for (JLinkLauncher launcher : launchers) {
command.add("--launcher");
command.add(launcher.getName() + "=" + launcher.getModule());
}
}
if (compress != 0) {
command.add("--compress");
command.add(String.valueOf(compress));
}
if (stripDebug) {
command.add("--strip-debug");
}
if (ignoreSigningInformation) {
command.add("--ignore-signing-information");
}
if (!excludeResources.isEmpty()) {
command.add("--exclude-resources=" + String.join(",", excludeResources));
}
if (!excludeFiles.isEmpty()) {
command.add("--exclude-files=" + String.join(",", excludeFiles));
}
if (noHeaderFiles) {
command.add("--no-header-files");
}
if (noManPages) {
command.add("--no-man-pages");
}
getLog().debug("run jlink: " + Joiner.of(" ").join(command));
ProcessResult result = ProcessUtils.execute(Collections2.toArray(command, String[]::new));
if (result.exitCode() != 0) {
String message = result.stderr().isEmpty() ? result.stdout() : result.stderr();
getLog().error("jlink error: " + message);
}
}
private void createDirectories() {
if (!workingDirectory.exists()) {
workingDirectory.mkdirs();
}
if (!modulesDirectory.exists()) {
modulesDirectory.mkdirs();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy