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

org.graalvm.buildtools.maven.AbstractNativeImageMojo Maven / Gradle / Ivy

Go to download

Plugin that provides support for building and testing of GraalVM native images (ahead-of-time compiled Java code)

The newest version!
/*
 * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package org.graalvm.buildtools.maven;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.toolchain.ToolchainManager;
import org.graalvm.buildtools.maven.config.ExcludeConfigConfiguration;
import org.graalvm.buildtools.utils.NativeImageConfigurationUtils;
import org.graalvm.buildtools.utils.NativeImageUtils;
import org.graalvm.buildtools.utils.SharedConstants;

import javax.inject.Inject;
import java.io.File;
import java.io.InputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Sebastien Deleuze
 */
public abstract class AbstractNativeImageMojo extends AbstractNativeMojo {
    protected static final String NATIVE_IMAGE_META_INF = "META-INF/native-image";
    protected static final String NATIVE_IMAGE_PROPERTIES_FILENAME = "native-image.properties";
    protected static final String NATIVE_IMAGE_DRY_RUN = "nativeDryRun";

    @Parameter(defaultValue = "${plugin}", readonly = true) // Maven 3 only
    protected PluginDescriptor plugin;

    @Parameter(defaultValue = "${session}", readonly = true)
    protected MavenSession session;

    @Parameter(defaultValue = "${mojoExecution}")
    protected MojoExecution mojoExecution;

    @Parameter(property = "plugin.artifacts", required = true, readonly = true)
    protected List pluginArtifacts;

    @Parameter(defaultValue = "${project.build.directory}", property = "outputDir", required = true)
    protected File outputDirectory;

    @Parameter(property = "mainClass")
    protected String mainClass;

    @Parameter(property = "imageName", defaultValue = "${project.artifactId}")
    protected String imageName;

    @Parameter(property = "classpath")
    protected List classpath;

    @Parameter(property = "classesDirectory")
    protected File classesDirectory;

    @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true)
    protected File defaultClassesDirectory;

    protected final List imageClasspath;

    @Parameter(property = "debug", defaultValue = "false")
    protected boolean debug;

    @Parameter(property = "fallback", defaultValue = "false")
    protected boolean fallback;

    @Parameter(property = "verbose", defaultValue = "false")
    protected boolean verbose;

    @Parameter(property = "sharedLibrary", defaultValue = "false")
    protected boolean sharedLibrary;

    @Parameter(property = "quickBuild", defaultValue = "false")
    protected boolean quickBuild;

    @Parameter(property = "useArgFile")
    protected Boolean useArgFile;

    @Parameter(property = "buildArgs")
    protected List buildArgs;

    @Parameter(defaultValue = "${project.build.directory}/native/generated", property = "resourcesConfigDirectory", required = true)
    protected File resourcesConfigDirectory;

    @Parameter(property = "agentResourceDirectory")
    protected File agentResourceDirectory;

    @Parameter(property = "excludeConfig")
    protected List excludeConfig;

    @Parameter(property = "environmentVariables")
    protected Map environment;

    @Parameter(property = "systemPropertyVariables")
    protected Map systemProperties;

    @Parameter(property = "configurationFileDirectories")
    protected List configFiles;

    @Parameter(property = "jvmArgs")
    protected List jvmArgs;

    @Parameter(property = NATIVE_IMAGE_DRY_RUN, defaultValue = "false")
    protected boolean dryRun;

    @Parameter(property = "requiredVersion")
    protected String requiredVersion;

    @Component
    protected ToolchainManager toolchainManager;

    @Inject
    protected AbstractNativeImageMojo() {
        imageClasspath = new ArrayList<>();
        useArgFile = SharedConstants.IS_WINDOWS;
    }

    protected List getBuildArgs() throws MojoExecutionException {
        final List cliArgs = new ArrayList<>();

        if (excludeConfig != null) {
            excludeConfig.forEach(entry -> {
                cliArgs.add("--exclude-config");
                cliArgs.add(entry.getJarPath());
                cliArgs.add(String.format("\"%s\"", entry.getResourcePattern()));
            });
        }

        cliArgs.add("-cp");
        cliArgs.add(getClasspath());

        if (debug) {
            cliArgs.add("-g");
        }
        if (!fallback) {
            cliArgs.add("--no-fallback");
        }
        if (verbose) {
            cliArgs.add("--verbose");
        }
        if (sharedLibrary) {
            cliArgs.add("--shared");
        }

        // Let's allow user to specify environment option to toggle quick build.
        String quickBuildEnv = System.getenv("GRAALVM_QUICK_BUILD");
        if (quickBuildEnv != null) {
            logger.warn("Quick build environment variable is set.");
            quickBuild = quickBuildEnv.isEmpty() || Boolean.parseBoolean(quickBuildEnv);
        }

        if (quickBuild) {
            cliArgs.add("-Ob");
        }

        cliArgs.add("-o");
        cliArgs.add(outputDirectory.toPath().toAbsolutePath() + File.separator + imageName);

        if (systemProperties != null) {
            for (Map.Entry entry : systemProperties.entrySet()) {
                cliArgs.add("-D" + entry.getKey() + "=" + entry.getValue());
            }
        }

        if (jvmArgs != null) {
            jvmArgs.forEach(jvmArg -> cliArgs.add("-J" + jvmArg));
        }

        maybeAddGeneratedResourcesConfig(buildArgs);
        maybeAddReachabilityMetadata(configFiles);

        if (configFiles != null && !configFiles.isEmpty()) {
            cliArgs.add("-H:ConfigurationFileDirectories=" +
                    configFiles.stream()
                    .map(Paths::get)
                    .map(Path::toAbsolutePath)
                    .map(Path::toString)
                    .collect(Collectors.joining(","))
            );
        }

        if (buildArgs != null && !buildArgs.isEmpty()) {
            cliArgs.addAll(processBuildArgs(buildArgs));
        }

        List actualCliArgs;
        if (useArgFile) {
            Path tmpDir = Paths.get("target", "tmp");
            actualCliArgs = new ArrayList<>(NativeImageUtils.convertToArgsFile(cliArgs, tmpDir));
        } else {
            actualCliArgs = cliArgs;
        }

        /* Main class comes last. It is kept outside argument files as GraalVM releases before JDK 21 fail to detect the mainClass in these files. */
        if (mainClass != null && !mainClass.equals(".")) {
            actualCliArgs.add(mainClass);
        }
        return Collections.unmodifiableList(actualCliArgs);
    }

    static List processBuildArgs(List buildArgs) {
        var result = new ArrayList();
        for (String buildArg : buildArgs) {
            if(buildArg.startsWith("\\Q") || buildArg.startsWith("-H:ConfigurationFileDirectories")) {
                result.add(buildArg);
            } else {
                result.addAll(Arrays.asList(buildArg.split("\\s+", 2)));
            }
        }
        return result;
    }

    protected Path processSupportedArtifacts(Artifact artifact) throws MojoExecutionException {
        return processArtifact(artifact, "jar", "test-jar", "war");
    }

    protected Path processArtifact(Artifact artifact, String... artifactTypes) throws MojoExecutionException {
        File artifactFile = artifact.getFile();

        if (artifactFile == null) {
            logger.debug("Missing artifact file for artifact " + artifact + " (type: " + artifact.getType() + ")");
            return null;
        }

        if (Arrays.stream(artifactTypes).noneMatch(a -> a.equals(artifact.getType()))) {
            logger.warn("Ignoring ImageClasspath Entry '" + artifact + "' with unsupported type '" + artifact.getType() + "'");
            return null;
        }
        if (!artifactFile.exists()) {
            throw new MojoExecutionException("Missing jar-file for " + artifact + ". " +
                    "Ensure that " + plugin.getArtifactId() + " runs in package phase.");
        }

        Path jarFilePath = artifactFile.toPath();
        logger.debug("ImageClasspath Entry: " + artifact + " (" + jarFilePath.toUri() + ")");

        warnIfWrongMetaInfLayout(jarFilePath, artifact);
        return jarFilePath;
    }

    protected void addArtifactToClasspath(Artifact artifact) throws MojoExecutionException {
        Optional.ofNullable(processSupportedArtifacts(artifact)).ifPresent(imageClasspath::add);
    }

    private static FileSystem openFileSystem(URI uri) throws IOException {
      FileSystem fs;
      try {
        fs = FileSystems.newFileSystem(uri, Collections.emptyMap());
      } catch (FileSystemAlreadyExistsException e) {
        fs = FileSystems.getFileSystem(uri);
      }
      return fs;
    }

    protected void warnIfWrongMetaInfLayout(Path jarFilePath, Artifact artifact) throws MojoExecutionException {
        if (jarFilePath.toFile().isDirectory()) {
            logger.debug("Artifact `" + jarFilePath + "` is a directory.");
            return;
        }
        URI jarFileURI = URI.create("jar:" + jarFilePath.toUri());
        try (FileSystem jarFS = openFileSystem(jarFileURI)) {
            Path nativeImageMetaInfBase = jarFS.getPath("/" + NATIVE_IMAGE_META_INF);
            if (Files.isDirectory(nativeImageMetaInfBase)) {
                try (Stream stream = Files.walk(nativeImageMetaInfBase)) {
                    List nativeImageProperties = stream
                            .filter(p -> p.endsWith(NATIVE_IMAGE_PROPERTIES_FILENAME)).collect(Collectors.toList());
                    for (Path nativeImageProperty : nativeImageProperties) {
                        Path relativeSubDir = nativeImageMetaInfBase.relativize(nativeImageProperty).getParent();
                        boolean valid = relativeSubDir != null && (relativeSubDir.getNameCount() == 2);
                        valid = valid && relativeSubDir.getName(0).toString().equals(artifact.getGroupId());
                        valid = valid && relativeSubDir.getName(1).toString().equals(artifact.getArtifactId());
                        if (!valid) {
                            String example = NATIVE_IMAGE_META_INF + "/%s/%s/" + NATIVE_IMAGE_PROPERTIES_FILENAME;
                            example = String.format(example, artifact.getGroupId(), artifact.getArtifactId());
                            logger.warn("Properties file at '" + nativeImageProperty.toUri() + "' does not match the recommended '" + example + "' layout.");
                        }
                    }
                }
            }
        } catch (IOException e) {
            throw new MojoExecutionException("Artifact " + artifact + "cannot be added to image classpath", e);
        }
    }

    protected abstract List getDependencyScopes();

    protected void addDependenciesToClasspath() throws MojoExecutionException {
        configureMetadataRepository();
        Set collected = new HashSet<>();
        // Must keep classpath order is the same with surefire test
        for (Artifact dependency : project.getArtifacts()) {
            if (getDependencyScopes().contains(dependency.getScope()) && collected.add(dependency)) {
                addArtifactToClasspath(dependency);
                maybeAddDependencyMetadata(dependency, file -> {
                    buildArgs.add("--exclude-config");
                    buildArgs.add(Pattern.quote(dependency.getFile().getAbsolutePath()));
                    buildArgs.add("^/META-INF/native-image/");
                });
            }
        }
    }

    /**
     * Returns path to where application classes are stored, or jar artifact if it is produced.
     * @return Path to application classes
     * @throws MojoExecutionException failed getting main build path
     */
    protected Path getMainBuildPath() throws MojoExecutionException {
        if (classesDirectory != null) {
            return classesDirectory.toPath();
        } else {
            Path artifactPath = processArtifact(project.getArtifact(), project.getPackaging());
            if (artifactPath != null) {
                return artifactPath;
            } else {
                return defaultClassesDirectory.toPath();
            }
        }
    }

    protected void populateApplicationClasspath() throws MojoExecutionException {
        imageClasspath.add(getMainBuildPath());
    }

    protected void populateClasspath() throws MojoExecutionException {
        if (classpath != null && !classpath.isEmpty()) {
            imageClasspath.addAll(classpath.stream()
                    .map(Paths::get)
                    .map(Path::toAbsolutePath)
                    .collect(Collectors.toSet())
            );
        } else {
            populateApplicationClasspath();
            addDependenciesToClasspath();
        }
        imageClasspath.removeIf(entry -> !entry.toFile().exists());
    }

    protected String getClasspath() throws MojoExecutionException {
        populateClasspath();
        if (imageClasspath.isEmpty()) {
            throw new MojoExecutionException("Image classpath is empty. " +
                    "Check if your classpath configuration is correct.");
        }
        return imageClasspath.stream()
                .map(Path::toString)
                .collect(Collectors.joining(File.pathSeparator));
    }

    protected void buildImage() throws MojoExecutionException {
        checkRequiredVersionIfNeeded();
        Path nativeImageExecutable = NativeImageConfigurationUtils.getNativeImage(logger);

        try {
            ProcessBuilder processBuilder = new ProcessBuilder(nativeImageExecutable.toString());
            processBuilder.command().addAll(getBuildArgs());

            if (environment != null) {
                processBuilder.environment().putAll(environment);
            }

            if (!outputDirectory.exists() && !outputDirectory.mkdirs()) {
                throw new MojoExecutionException("Failed creating output directory");
            }
            processBuilder.inheritIO();

            String commandString = String.join(" ", processBuilder.command());
            logger.info("Executing: " + commandString);

            if (dryRun) {
                logger.warn("Skipped native-image building due to `" + NATIVE_IMAGE_DRY_RUN + "` being specified.");
                return;
            }

            Process imageBuildProcess = processBuilder.start();
            if (imageBuildProcess.waitFor() != 0) {
                throw new MojoExecutionException("Execution of " + commandString + " returned non-zero result");
            }
        } catch (IOException | InterruptedException e) {
            throw new MojoExecutionException("Building image with " + nativeImageExecutable + " failed", e);
        }
    }

    protected void checkRequiredVersionIfNeeded() throws MojoExecutionException {
        if (requiredVersion == null) {
            return;
        }
        Path nativeImageExecutable = NativeImageConfigurationUtils.getNativeImage(logger);
        try {
            ProcessBuilder processBuilder = new ProcessBuilder(nativeImageExecutable.toString());
            processBuilder.command().add("--version");
            Process versionCheckProcess = processBuilder.start();
            if (versionCheckProcess.waitFor() != 0) {
                String commandString = String.join(" ", processBuilder.command());
                throw new MojoExecutionException("Execution of " + commandString + " returned non-zero result");
            }
            InputStream inputStream = versionCheckProcess.getInputStream();
            String versionToCheck = new BufferedReader(
                    new InputStreamReader(inputStream, StandardCharsets.UTF_8))
                    .lines()
                    .collect(Collectors.joining("\n"));
            NativeImageUtils.checkVersion(requiredVersion, versionToCheck);

        } catch (IOException | InterruptedException e) {
            throw new MojoExecutionException("Checking GraalVM version with " + nativeImageExecutable + " failed", e);
        }
    }

    protected void maybeAddGeneratedResourcesConfig(List into) {
        if (resourcesConfigDirectory.exists() || agentResourceDirectory != null) {
            File[] dirs = resourcesConfigDirectory.listFiles();
            Stream configDirs =
                    Stream.concat(dirs == null ? Stream.empty() : Arrays.stream(dirs),
                            agentResourceDirectory == null ? Stream.empty() : Stream.of(agentResourceDirectory).filter(File::isDirectory));

            String value = configDirs.map(File::getAbsolutePath).collect(Collectors.joining(","));
            if (!value.isEmpty()) {
                into.add("-H:ConfigurationFileDirectories=" + value);
            }
        }
    }



    protected void maybeAddReachabilityMetadata(List configDirs) {
        if (isMetadataRepositoryEnabled() && !metadataRepositoryConfigurations.isEmpty()) {
            metadataRepositoryConfigurations.stream()
                    .map(configuration -> configuration.getDirectory().toAbsolutePath())
                    .map(Path::toFile)
                    .map(File::getAbsolutePath)
                    .forEach(configDirs::add);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy