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

io.helidon.build.maven.GraalNativeMojo Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2018, 2023 Oracle and/or its affiliates.
 *
 * 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 io.helidon.build.maven;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import io.helidon.build.common.PathFinder;
import io.helidon.build.common.SourcePath;
import io.helidon.build.maven.component.PathComponent;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
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.MavenProject;
import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
import org.codehaus.plexus.languages.java.jpms.LocationManager;
import org.codehaus.plexus.languages.java.jpms.ResolvePathRequest;
import org.codehaus.plexus.languages.java.jpms.ResolvePathResult;
import org.codehaus.plexus.util.Scanner;
import org.sonatype.plexus.build.incremental.BuildContext;

/**
 * Maven goal to invoke GraalVM {@code native-image}.
 */
@Mojo(name = "native-image",
      defaultPhase = LifecyclePhase.PACKAGE,
      requiresDependencyResolution = ResolutionScope.RUNTIME)
public class GraalNativeMojo extends AbstractMojo {
    private static final String EXEC_MODE_MAIN_CLASS = "main";
    private static final String EXEC_MODE_JAR = "jar";
    private static final String EXEC_MODE_JAR_WITH_CP = "jar-cp";
    private static final String EXEC_MODE_MODULE = "module";
    private static final String EXEC_MODE_NONE = "none";

    private static final String PATH_ENV_VAR = "PATH";
    private static final String JAVA_HOME_ENV_VAR = "JAVA_HOME";
    private static final String GRAALVM_HOME_ENV_VAR = "GRAALVM_HOME";

    /**
     * {@code true} if running on WINDOWS.
     */
    private static final boolean IS_WINDOWS = File.pathSeparatorChar != ':';

    /**
     * Constant for the {@code native-image} command file name.
     */
    private static final String NATIVE_IMAGE_CMD = "native-image";

    /**
     * Constant for the file extensions of windows executable scripts.
     */
    private static final List WINDOWS_SCRIPT_EXTENSIONS = List.of("bat", "cmd", "ps1");

    /**
     * Plexus build context used to get the scanner for scanning resources.
     */
    @Component
    private BuildContext buildContext;

    /**
     * The Maven project this mojo executes on.
     */
    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    private MavenProject project;

    /**
     * The project build output directory. (e.g. {@code target/})
     */
    @Parameter(defaultValue = "${project.build.directory}",
            readonly = true, required = true)
    private File buildDirectory;

    @Parameter(defaultValue = "${project.basedir}", required = true, property = "native.image.currentDir")
    private File currentDir;

    /**
     * GraalVM home.
     */
    @Parameter(defaultValue = "${env.GRAALVM_HOME}")
    private File graalVMHome;

    /**
     * Name of the output file to be generated.
     */
    @Parameter(defaultValue = "${project.build.finalName}", required = true, property = "native.image.finalName")
    private String finalName;

    /**
     * Project JAR file.
     */
    @Parameter(property = "native.image.jarFile")
    private File jarFile;

    /**
     * Show exception stack traces for exceptions during image building.
     */
    @Parameter(defaultValue = "true",
            property = "native.image.reportExceptionStackTraces")
    private boolean reportExceptionStackTraces;

    /**
     * Indicates if project resources should be added to the image.
     */
    @Parameter(defaultValue = "true", property = "native.image.addProjectResources")
    private boolean addProjectResources;

    @Parameter(defaultValue = EXEC_MODE_JAR,
               property = "native.image.execMode")
    private String execMode;

    @Parameter(defaultValue = "${mainClass}",
               property = "native.image.mainClass")
    private String mainClass;

    /**
     * List of regexp matching names of resources to be included in the image.
     */
    @Parameter(property = "native.image.includeResources")
    private List includeResources;

    /**
     * Build shared library.
     */
    @Parameter(defaultValue = "false", property = "native.image.buildShared")
    private boolean buildShared;

    /**
     * Build statically linked executable (requires static {@code libc} and
     * {@code zlib}).
     */
    @Parameter(defaultValue = "false", property = "native.image.buildStatic")
    private boolean buildStatic;

    /**
     * Additional command line arguments.
     */
    @Parameter(property = "native.image.additionalArgs")
    private List additionalArgs;

    /**
     * Skip execution for this plugin.
     */
    @Parameter(defaultValue = "false", property = "native.image.skip")
    private boolean skipNativeImage;

    /**
     * Module name for {@code --module} argument.
     */
    @Parameter(property = "native.image.module")
    private String module;

    /**
     * Custom class-path in module execution mode only.
     */
    @Parameter
    private PathComponent classPath;

    /**
     * Custom module-path in module execution mode only.
     */
    @Parameter
    private PathComponent modulePath;

    /**
     * The {@code native-image} execution process.
     */
    private Process process;

    /**
     * The {@link LocationManager} parsing artifacts.
     */
    private final LocationManager locationManager = new LocationManager();

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        Path outputPath = buildDirectory.toPath().resolve(finalName);
        getLog().info("Building native image :" + outputPath.toAbsolutePath());

        getLog().debug("Skip: " + skipNativeImage);
        getLog().debug("Type: " + execMode);
        getLog().debug("Main class: " + mainClass);

        if (skipNativeImage) {
            getLog().info("Skipping execution.");
            return;
        }

        NativeContext context = new NativeContext(execMode);
        if (context.useMain() && mainClass == null) {
            throw new MojoFailureException("Main class not configured and required. Use option \"mainClass\"");
        }

        // create the command
        List command = new ArrayList<>();

        Path nativeImageCmd = findNativeImage();
        if (nativeImageCmd == null || !nativeImageCmd.toFile().exists()) {
            throw new MojoExecutionException(NATIVE_IMAGE_CMD
                    + " not found from environment variable directory "
                    + GRAALVM_HOME_ENV_VAR + ","
                    + PATH_ENV_VAR + " and "
                    + JAVA_HOME_ENV_VAR);
        }
        File nativeImageFile = nativeImageCmd.toFile();
        command.add(nativeImageFile.getAbsolutePath());
        addStaticOrShared(command);

        String quoteToken = IS_WINDOWS && isWindowsScript(nativeImageFile) ? "\"" : "";

        // Path is the directory
        command.add("-H:Path=" + quoteToken + buildDirectory.getAbsolutePath() + quoteToken);

        addResources(command, quoteToken);

        if (reportExceptionStackTraces) {
            command.add("-H:+ReportExceptionStackTraces");
        }

        if (context.addClasspath()) {
            command.add("-classpath");
            command.add(getClasspath(context));
        }

        if (additionalArgs != null) {
            command.addAll(additionalArgs);
        }

        /*
         * when using a main class, the following two lines must not be used
         * when using a jar, the jar itself and whole `Class-Path` from manifest are added to the
         * classpath automatically.
         */
        if (context.useJar()) {
            resolveJarFile();
            command.add("-jar");
            command.add(jarFile.getAbsolutePath());
        }

        if (context.useModule()) {
            resolveJarFile();
            command.add("--module");
            if (Objects.isNull(module) || module.isBlank()) {
                module = extractModuleNameFromJar();
            }
            if (Objects.nonNull(mainClass)) {
                module = module.endsWith("/")
                        ? module.substring(0, module.length() - 1)
                        : module;
                module = String.join("/", module, mainClass);
            }
            getLog().debug("Module: " + module);
            command.add(module);
            addModuleAndClassPath(command);
        }

        if (context.useNone()) {
            if (Objects.isNull(additionalArgs) || additionalArgs.isEmpty()) {
                throw new MojoExecutionException("\"additionalArgs\" must be specified when using \"none\" execution mode.");
            }
        }

        // -H:Name must be after -jar
        command.add("-H:Name=" + quoteToken + finalName + quoteToken);

        if (context.useMain()) {
            command.add(mainClass);
        }

        getLog().debug("Executing command: " + command);

        // execute the command process
        ProcessBuilder pb = new ProcessBuilder(command.toArray(new String[0]));
        pb.directory(currentDir);
        Thread stdoutReader = new Thread(this::logStdout);
        Thread stderrReader = new Thread(this::logStderr);
        try {
            process = pb.start();
            stdoutReader.start();
            stderrReader.start();
            int exitCode = process.waitFor();
            stdoutReader.join();
            stderrReader.join();
            if (exitCode != 0) {
                throw new MojoFailureException("Image generation failed, "
                        + "exit code: " + exitCode);
            }
        } catch (IOException | InterruptedException ex) {
            throw new MojoExecutionException("Image generation error", ex);
        }
    }

    private Path findNativeImage() {
        return PathFinder.find(NATIVE_IMAGE_CMD,
                        List.of(Optional.ofNullable(graalVMHome)
                                .map(File::toPath)
                                .map(p -> p.resolve("bin"))),
                        List.of(Optional.ofNullable(System.getenv(JAVA_HOME_ENV_VAR))
                                .map(Path::of)
                                .map(p -> p.resolve("bin"))))
                .orElseThrow(() -> new IllegalStateException("Unable to find " + NATIVE_IMAGE_CMD));
    }

    private String extractModuleNameFromJar() throws MojoExecutionException {
        Objects.requireNonNull(jarFile);
        try {
            ResolvePathResult result = locationManager.resolvePath(ResolvePathRequest.ofFile(jarFile));
            JavaModuleDescriptor descriptor = result.getModuleDescriptor();
            if (!descriptor.isAutomatic()) {
                return descriptor.name();
            }
            throw new MojoExecutionException(String.format("Jar file %s does not contain module descriptor", jarFile.getName()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void resolveJarFile() throws MojoFailureException {
        if (jarFile == null) {
            File artifact = project.getArtifact().getFile();
            if (artifact == null) {
                artifact = new File(buildDirectory, finalName + ".jar");
            }
            jarFile = artifact;
        }
        if (!jarFile.exists()) {
            throw new MojoFailureException("Artifact does not exist: " + jarFile.getAbsolutePath());
        }
    }

    private void addResources(List command, String quoteToken) {
        String resources = getResources();
        if (!resources.isEmpty()) {
            command.add("-H:IncludeResources=" + quoteToken + resources + quoteToken);
        }
    }

    private void addStaticOrShared(List command) throws MojoExecutionException {
        if (buildShared || buildStatic) {
            if (buildShared && buildStatic) {
                throw new MojoExecutionException(
                        "static and shared option cannot be used together");
            }
            if (buildShared) {
                getLog().info("Building a shared library");
                command.add("--shared");
            }
            if (buildStatic) {
                getLog().info("Building a statically linked executable");
                command.add("--static");
            }
        }
    }

    /**
     * Log the process standard output.
     */
    private void logStdout() {
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
        String line;
        try {
            line = reader.readLine();
            while (line != null) {
                getLog().info(line);
                line = reader.readLine();
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Log the process standard error.
     */
    private void logStderr() {
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getErrorStream()));
        String line;
        try {
            line = reader.readLine();
            while (line != null) {
                getLog().warn(line);
                line = reader.readLine();
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Scan for project resources and produce a comma separated list of include
     * resources.
     * @return String as comma separated list
     */
    private String getResources() {
        // scan all resources
        getLog().debug("Building resources string");
        List resources = new ArrayList<>();

        if (addProjectResources) {
            getLog().debug("Scanning project resources");
            for (Resource resource : project.getResources()) {
                File resourcesDir = new File(resource.getDirectory());
                Scanner scanner = buildContext.newScanner(resourcesDir);
                String[] includes = null;
                if (resource.getIncludes() != null
                        && !resource.getIncludes().isEmpty()) {
                    includes = resource.getIncludes().toArray(new String[0]);
                }
                scanner.setIncludes(includes);
                String[] excludes = null;
                if (resource.getExcludes() != null
                        && !resource.getExcludes().isEmpty()) {
                    excludes = resource.getExcludes().toArray(new String[0]);
                }
                scanner.setExcludes(excludes);
                scanner.scan();
                for (String included : scanner.getIncludedFiles()) {
                    getLog().debug("Found resource: " + included);
                    resources.add(included.replaceAll("\\\\", "/"));
                }
            }
        }

        // add additional resources
        if (includeResources != null) {
            getLog().debug("Adding provided resources: " + includeResources);
            resources.addAll(includeResources);
        }

        // comma separated list
        StringBuilder sb = new StringBuilder();
        Iterator it = resources.iterator();
        while (it.hasNext()) {
            sb.append(it.next());
            if (it.hasNext()) {
                sb.append("|");
            }
        }
        String resourcesStr = sb.toString();
        getLog().debug("Built resources string: " + resourcesStr);
        return resourcesStr;
    }

    /**
     * Get the project run-time class-path.
     *
     * @return String represented the java class-path
     * @throws MojoExecutionException if an
     * {@link DependencyResolutionRequiredException} occurs
     * @param context configuration context
     */
    private String getClasspath(NativeContext context) throws MojoExecutionException {
        getLog().debug("Building class-path string");
        try {
            List runtimeClasspathElements = project.getRuntimeClasspathElements();
            File targetClasses = new File(buildDirectory, "classes");

            List classpathElements = new LinkedList<>();

            if (context.useJar()) {
                // Adding the classpath once more causes issues with libraries
                // doing classpath scanning (slf4j, CDI) - ergo we must exclude the target when running
                // from jar
                for (String element : runtimeClasspathElements) {
                    File elementFile = new File(element);
                    if (!targetClasses.equals(elementFile)) {
                        classpathElements.add(element);
                    }
                }
            } else {
                // when running using main class, we need the whole classpath
                classpathElements.addAll(runtimeClasspathElements);
            }

            String classpath = String.join(File.pathSeparator, classpathElements);
            getLog().debug("Built class-path: " + classpath);
            return classpath;
        } catch (DependencyResolutionRequiredException ex) {
            throw new MojoExecutionException(
                    "Unable to get compile class-path", ex);
        }
    }

    /**
     * Build module-path, class-path, and add them to the provided list.
     *
     * @param command where the module-path and/or class-path will be added
     */
    private void addModuleAndClassPath(List command) {
        getLog().debug("Building module-path string");
        List modules = new LinkedList<>();
        List cp = new LinkedList<>();
        File jarFile = new File(buildDirectory, finalName + ".jar");

        if (jarFile.exists()) {
            if (getProjectModuleDescriptor().isPresent()) {
                modules.add(jarFile.getAbsolutePath());
            } else {
                cp.add(jarFile.getAbsolutePath());
            }
        } else {
            getLog().warn(String.format("Jar file %s does not exist, won't be present on module/class path", jarFile.getName()));
        }

        for (Artifact artifact : project.getArtifacts()) {
            File file = artifact.getFile();
            try {
                ResolvePathResult result = locationManager.resolvePath(ResolvePathRequest.ofFile(file));
                if (!result.getModuleDescriptor().isAutomatic()) {
                    modules.add(file.getPath());
                    continue;
                }
                addRuntimeClasspathArtifact(artifact, cp);
            } catch (IOException e) {
                addRuntimeClasspathArtifact(artifact, cp);
            }
        }

        cp = filter(cp, classPath);
        modules = filter(modules, modulePath);
        String modulePath = String.join(File.pathSeparator, modules);
        String classPath = String.join(File.pathSeparator, cp);
        getLog().debug("Built module-path: " + modulePath);
        getLog().debug("Built class-path: " + classPath);
        if (!modulePath.isEmpty()) {
            command.add("--module-path");
            command.add(modulePath);
        }
        if (!classPath.isEmpty()) {
            command.add("--class-path");
            command.add(classPath);
        }
    }

    private List filter(List list, PathComponent filter) {
        return Objects.isNull(filter) ? list : filter.filter(list);
    }

    private Optional getProjectModuleDescriptor() {
        return SourcePath.scan(Path.of(project.getBuild().getSourceDirectory()).toFile())
                .stream()
                .filter(p -> p.matches("module-info.java"))
                .findAny();
    }

    private void addRuntimeClasspathArtifact(Artifact artifact, List list) {
        if (artifact.getArtifactHandler().isAddedToClasspath()
                && (Artifact.SCOPE_COMPILE.equals(artifact.getScope())
                || Artifact.SCOPE_RUNTIME.equals(artifact.getScope()))) {

            File file = artifact.getFile();
            if (Objects.nonNull(file)) {
                list.add(file.getPath());
            }
        }
    }

    /**
     * Test if the given command file is a windows script.
     * @param cmd command file
     * @return {@code true} if a windows script, {@code false} otherwise
     */
    private static boolean isWindowsScript(File cmd) {
        return WINDOWS_SCRIPT_EXTENSIONS.stream().anyMatch(ext -> cmd.getAbsolutePath().endsWith("." + ext));
    }

    private static final class NativeContext {

        private final boolean useJar;
        private final boolean useMain;
        private final boolean addClasspath;
        private final boolean useModule;
        private final boolean none;

        private NativeContext(String execMode) throws MojoFailureException {
            switch (execMode) {
            case EXEC_MODE_JAR:
                useJar = true;
                useMain = false;
                addClasspath = false;
                useModule = false;
                none = false;
                break;
            case EXEC_MODE_JAR_WITH_CP:
                useJar = true;
                useMain = false;
                addClasspath = true;
                useModule = false;
                none = false;
                break;
            case EXEC_MODE_MAIN_CLASS:
                useJar = false;
                useMain = true;
                addClasspath = true;
                useModule = false;
                none = false;
                break;
            case EXEC_MODE_MODULE:
                useJar = false;
                useMain = false;
                addClasspath = false;
                useModule = true;
                none = false;
                break;
            case EXEC_MODE_NONE:
                useJar = false;
                useMain = false;
                addClasspath = false;
                useModule = false;
                none = true;
                break;
            default:
                throw new MojoFailureException("Invalid configuration of \"execMode\". Has to be one of: "
                                                       + EXEC_MODE_JAR + ", "
                                                       + EXEC_MODE_JAR_WITH_CP + ", "
                                                       + EXEC_MODE_NONE + ", "
                                                       + EXEC_MODE_MODULE + ", or "
                                                       + EXEC_MODE_MAIN_CLASS);
            }
        }

        boolean useJar() {
            return useJar;
        }

        boolean useMain() {
            return useMain;
        }

        boolean useModule() {
            return useModule;
        }

        boolean useNone() {
            return none;
        }

        boolean addClasspath() {
            return addClasspath;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy