
io.helidon.build.maven.graal.GraalNativeMojo Maven / Gradle / Ivy
/*
* Copyright (c) 2018, 2020 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.graal;
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 io.helidon.build.util.Strings;
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.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,
requiresProject = true)
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";
/**
* {@code true} if running on WINDOWS.
*/
private static final boolean IS_WINDOWS = File.pathSeparator != ":";
/**
* 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");
/**
* Constant for the file extensions that are executable on windows.
*/
private static final List WINDOWS_EXECUTABLE_EXTENSIONS = List.of("exe", "bin", "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}", readonly = true,
required = true)
private String finalName;
/**
* Show exception stack traces for exceptions during image building.
*/
@Parameter(defaultValue = "true",
property = "native.image.reportExceptionStackTraces")
private boolean reportExceptionStackTraces;
/**
* Do not use image-build server.
*/
@Parameter(defaultValue = "true",
property = "native.image.noServer")
private boolean noServer;
/**
* Indicates if project resources should be added to the image.
*/
@Parameter(defaultValue = "true")
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
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
private List additionalArgs;
/**
* Skip execution for this plugin.
*/
@Parameter(defaultValue = "false", property = "native.image.skip")
private boolean skipNativeImage;
/**
* The {@code native-image} execution process.
*/
private Process process;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
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, mainClass);
context.artifact(project, buildDirectory);
context.validate();
// create the command
List command = new ArrayList<>();
File nativeImageCmd = findNativeImageCmd();
command.add(nativeImageCmd.getAbsolutePath());
addStaticOrShared(command);
String quoteToken = IS_WINDOWS && isWindowsScript(nativeImageCmd) ? "\"" : "";
addNativeImageTarget(command, quoteToken);
addResources(command, quoteToken);
if (reportExceptionStackTraces) {
command.add("-H:+ReportExceptionStackTraces");
}
if (noServer) {
command.add("--no-server");
}
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()) {
command.add("-jar");
command.add(context.artifact().getAbsolutePath());
}
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 void addResources(List command, String quoteToken) {
String resources = getResources();
if (!resources.isEmpty()) {
command.add("-H:IncludeResources=" + quoteToken + resources + quoteToken);
}
}
private void addNativeImageTarget(List command, String quoteToken) {
Path outputPath = buildDirectory.toPath().resolve(finalName);
getLog().info("Building native image :" + outputPath.toAbsolutePath());
// Path is the directory
command.add("-H:Path=" + quoteToken + buildDirectory.getAbsolutePath() + quoteToken);
// Name is the filename
command.add("-H:Name=" + quoteToken + finalName + 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 = (String[]) resource.getIncludes()
.toArray(new String[resource.getIncludes().size()]);
}
scanner.setIncludes(includes);
String[] excludes = null;
if (resource.getExcludes() != null
&& !resource.getExcludes().isEmpty()) {
excludes = (String[]) resource.getExcludes()
.toArray(new String[resource.getExcludes().size()]);
}
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);
}
}
/**
* Find the first command file for the specified name with a known windows executable extension.
*
* @param dir directory
* @param cmd command name
* @return File or {@code null} if no valid command file is found
*/
private static File findWindowsCmd(File dir, String cmd) {
return WINDOWS_EXECUTABLE_EXTENSIONS.stream()
.map((ext) -> new File(dir, cmd + "." + ext))
.filter(File::isFile)
.map(File::getAbsoluteFile)
.findFirst()
.orElse(null);
}
/**
* 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()
.filter(ext -> cmd.getAbsolutePath().endsWith("." + ext))
.count() >= 1;
}
/**
* File a command file.
*
* @param dir directory
* @param cmd command name
* @return File or {@code null} if no valid command file is found
*/
private static File findCmd(File dir, String cmd) {
File cmdFile = new File(dir, cmd);
if (cmdFile.isFile()) {
return cmdFile;
}
return IS_WINDOWS ? findWindowsCmd(dir, cmd) : null;
}
/**
* Find the {@code native-image} command file.
*
* @return File
* @throws MojoExecutionException if unable to find the command file
*/
private File findNativeImageCmd() throws MojoExecutionException {
if (graalVMHome == null
|| !graalVMHome.exists()
|| !graalVMHome.isDirectory()) {
getLog().debug(
"graalvm.home not set,looking in the PATH environment");
String sysPath = System.getenv("PATH");
if (Strings.isNotValid(sysPath)) {
throw new MojoExecutionException(
"PATH environment variable is unset or empty");
}
for (final String p : sysPath.split(File.pathSeparator)) {
File cmd = findCmd(new File(p), NATIVE_IMAGE_CMD);
if (cmd != null) {
getLog().debug("Found " + NATIVE_IMAGE_CMD + ": " + cmd);
return cmd;
}
}
throw new MojoExecutionException(NATIVE_IMAGE_CMD
+ " not found in the PATH environment");
}
getLog().debug(
"graalvm.home set, looking for bin/" + NATIVE_IMAGE_CMD);
File binDir = new File(graalVMHome, "bin");
if (!binDir.isDirectory()) {
throw new MojoExecutionException("Unable to find " + NATIVE_IMAGE_CMD + " command, "
+ binDir.getAbsolutePath() + " is not a valid directory");
}
File cmd = findCmd(binDir, NATIVE_IMAGE_CMD);
if (cmd != null) {
getLog().debug("Found " + NATIVE_IMAGE_CMD + ": " + cmd);
return cmd;
}
throw new MojoExecutionException(NATIVE_IMAGE_CMD
+ " not found in directory: " + binDir.getAbsolutePath());
}
private static final class NativeContext {
private final boolean useJar;
private final boolean useMain;
private final boolean addClasspath;
private final String mainClass;
private File artifact;
private NativeContext(String execMode, String mainClass) throws MojoFailureException {
this.mainClass = mainClass;
switch (execMode) {
case EXEC_MODE_JAR:
useJar = true;
useMain = false;
addClasspath = false;
break;
case EXEC_MODE_JAR_WITH_CP:
useJar = true;
useMain = false;
addClasspath = true;
break;
case EXEC_MODE_MAIN_CLASS:
useJar = false;
useMain = true;
addClasspath = true;
break;
default:
throw new MojoFailureException("Invalid configuration of \"execMode\". Has to be one of: "
+ EXEC_MODE_JAR + ", "
+ EXEC_MODE_JAR_WITH_CP + ", or "
+ EXEC_MODE_MAIN_CLASS);
}
}
boolean useJar() {
return useJar;
}
boolean useMain() {
return useMain;
}
boolean addClasspath() {
return addClasspath;
}
File artifact() {
return artifact;
}
void artifact(MavenProject project, File buildDirectory) throws MojoFailureException {
if (useJar) {
artifact = project.getArtifact().getFile();
if (artifact == null) {
artifact = new File(buildDirectory,
project.getBuild().getFinalName() + ".jar");
}
if (!artifact.exists()) {
throw new MojoFailureException("Artifact does not exist: "
+ artifact.getAbsolutePath());
}
}
}
void validate() throws MojoFailureException {
if ((null == mainClass) && useMain) {
throw new MojoFailureException("Main class not configured and required. Use option \"mainClass\"");
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy