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

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