blob: c36c2aa2fee0f4d9ea2869c9097e91029a06e5e6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.geronimo.arthur.maven.mojo;
import static java.lang.ClassLoader.getSystemClassLoader;
import static java.util.Collections.emptyMap;
import static java.util.Locale.ROOT;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static org.apache.maven.plugins.annotations.LifecyclePhase.PACKAGE;
import static org.apache.maven.plugins.annotations.ResolutionScope.TEST;
import static org.apache.xbean.finder.archive.ClasspathArchive.archive;
import java.io.File;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;
import javax.json.bind.config.PropertyOrderStrategy;
import org.apache.geronimo.arthur.impl.nativeimage.ArthurNativeImageConfiguration;
import org.apache.geronimo.arthur.impl.nativeimage.ArthurNativeImageExecutor;
import org.apache.geronimo.arthur.maven.extension.MavenArthurExtension;
import org.apache.geronimo.arthur.maven.installer.SdkmanGraalVMInstaller;
import org.apache.geronimo.arthur.spi.model.ClassReflectionModel;
import org.apache.geronimo.arthur.spi.model.DynamicProxyModel;
import org.apache.geronimo.arthur.spi.model.ResourceBundleModel;
import org.apache.geronimo.arthur.spi.model.ResourceModel;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.xbean.finder.AnnotationFinder;
import org.apache.xbean.finder.archive.CompositeArchive;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.installation.InstallRequest;
import org.eclipse.aether.installation.InstallResult;
import org.eclipse.aether.installation.InstallationException;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
/**
* Generates a native binary from current project.
*/
@Mojo(name = "native-image", defaultPhase = PACKAGE, requiresDependencyResolution = TEST, threadSafe = true)
public class NativeImageMojo extends ArthurMojo {
//
// ArthurNativeImageConfiguration
//
/**
* native-image binary to use, if not set it will install graal in the local repository.
*/
@Parameter(property = "arthur.nativeImage")
private String nativeImage;
/**
* custom native-image arguments.
*/
@Parameter(property = "arthur.customOptions")
private List<String> customOptions;
/**
* custom pre-built classpath, if not set it defaults on the project dependencies.
*/
@Parameter(property = "arthur.classpath")
private List<String> classpath;
/**
* JSON java.lang.reflect.Proxy configuration.
*/
@Parameter(property = "arthur.dynamicProxyConfigurationFiles")
private List<String> dynamicProxyConfigurationFiles;
/**
* JSON reflection configuration.
*/
@Parameter(property = "arthur.reflectionConfigurationFiles")
private List<String> reflectionConfigurationFiles;
/**
* JSON resources configuration.
*/
@Parameter(property = "arthur.resourcesConfigurationFiles")
private List<String> resourcesConfigurationFiles;
/**
* resource bundle qualified names to include.
*/
@Parameter(property = "arthur.includeResourceBundles")
private List<String> includeResourceBundles;
/**
* Classes to intiialize at run time.
*/
@Parameter(property = "arthur.initializeAtRunTime")
private List<String> initializeAtRunTime;
/**
* Classes to initialize at build time.
*/
@Parameter(property = "arthur.initializeAtBuildTime")
private List<String> initializeAtBuildTime;
/**
* Limit the number of compilable methods.
*/
@Parameter(property = "arthur.maxRuntimeCompileMethods", defaultValue = "1000")
private int maxRuntimeCompileMethods;
/**
* Enforce {@link #maxRuntimeCompileMethods}.
*/
@Parameter(property = "arthur.enforceMaxRuntimeCompileMethods", defaultValue = "true")
private boolean enforceMaxRuntimeCompileMethods;
/**
* Should all charsets be added.
*/
@Parameter(property = "arthur.addAllCharsets", defaultValue = "true")
private boolean addAllCharsets;
/**
* Should exception stacks be reported.
*/
@Parameter(property = "arthur.reportExceptionStackTraces", defaultValue = "true")
private boolean reportExceptionStackTraces;
/**
* Should class initialition be tracked.
*/
@Parameter(property = "arthur.traceClassInitialization", defaultValue = "true")
private boolean traceClassInitialization;
/**
* Should initialiation of classes be printed - mainly for debug purposes.
*/
@Parameter(property = "arthur.printClassInitialization", defaultValue = "false")
private boolean printClassInitialization;
/**
* Behavior when native compilation fails, it is recommended to keep it to "no".
*/
@Parameter(property = "arthur.fallbackMode", defaultValue = "no")
private ArthurNativeImageConfiguration.FallbackMode fallbackMode;
/**
* Should the image be static or dynamic (jvm part).
*/
@Parameter(property = "arthur.buildStaticImage", defaultValue = "true")
private boolean buildStaticImage;
/**
* Should incomplete classpath be tolerated.
*/
@Parameter(property = "arthur.allowIncompleteClasspath", defaultValue = "true")
private boolean allowIncompleteClasspath;
/**
* Should unsupported element be reported at runtime or not. It is not a recommended option but it is often needed.
*/
@Parameter(property = "arthur.reportUnsupportedElementsAtRuntime", defaultValue = "true")
private boolean reportUnsupportedElementsAtRuntime;
/**
* Should security services be included.
*/
@Parameter(property = "arthur.enableAllSecurityServices", defaultValue = "true")
private boolean enableAllSecurityServices;
/**
* Which main to compile.
*/
@Parameter(property = "arthur.main", required = true)
private String main;
/**
* Where to put the output binary.
*/
@Parameter(property = "arthur.output", defaultValue = "${project.build.directory}/${project.artifactId}.graal.bin")
private String output;
/**
* The execution will fork native-image process, should IO be inherited from maven process (recommended).
*/
@Parameter(property = "arthur.inheritIO", defaultValue = "true")
private boolean inheritIO;
/**
* Should graal build server be used (a bit like gradle daemon), it is very discouraged to be used cause invalidation is not yet well handled.
*/
@Parameter(property = "arthur.noServer", defaultValue = "true")
private boolean noServer;
//
// Installer parameters
//
/**
* In case Graal must be downloaded to get native-image, where to take it from.
*/
@Parameter(property = "arthur.graalDownloadUrl",
defaultValue = "https://api.sdkman.io/2/broker/download/java/${graalVersion}-grl/${platform}")
private String graalDownloadUrl;
/**
* In case Graal must be downloaded to get native-image, which version to download.
*/
@Parameter(property = "arthur.graalVersion", defaultValue = "19.2.1")
private String graalVersion;
/**
* In case Graal must be downloaded to get native-image, which platform to download, auto will handle it for you.
*/
@Parameter(property = "arthur.graalPlatform", defaultValue = "auto")
private String graalPlatform;
/**
* In case Graal must be downloaded to get native-image, it will be cached in the local repository with this gav.
*/
@Parameter(property = "arthur.graalCacheGav", defaultValue = "org.apache.geronimo.arthur.cache:graal")
private String graalCacheGav; // groupId:artifactId
//
// Other maven injections
//
/**
* Inline configuration model (appended to {@link #reflectionConfigurationFiles}.
*/
@Parameter
private List<ClassReflectionModel> reflections;
/**
* Inline resource bundle model (appended to {@link #reflectionConfigurationFiles}.
*/
@Parameter
private List<ResourceBundleModel> bundles;
/**
* Inline resources model (appended to {@link #resourcesConfigurationFiles}.
*/
@Parameter
private List<ResourceModel> resources;
/**
* Inline dynamic proxy configuration (appended to {@link #dynamicProxyConfigurationFiles}).
*/
@Parameter
private List<DynamicProxyModel> dynamicProxies;
/**
* Should this mojo be skipped.
*/
@Parameter(property = "arthur.skip")
private boolean skip;
/**
* Should the build be done with test dependencies (and binaries).
*/
@Parameter(property = "arthur.supportTestArtifacts", defaultValue = "false")
private boolean supportTestArtifacts;
/**
* By default arthur runs the extension with a dedicated classloader built from the project having as parent the JVM,
* this enables to use the mojo as parent instead).
*/
@Parameter(property = "arthur.useTcclAsScanningParentClassLoader", defaultValue = "false")
private boolean useTcclAsScanningParentClassLoader;
/**
* Where the temporary files are created.
*/
@Parameter(defaultValue = "${project.build.directory}/arthur_workdir")
private File workdir;
/**
* groupId:artifactId list of ignored artifact during the pre-build phase.
*/
@Parameter(property = "arthur.excludedArtifacts")
private List<String> excludedArtifacts;
/**
* {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>} list of artifacts appended to graal build.
*/
@Parameter(property = "arthur.graalExtensions")
private List<String> graalExtensions;
/**
* List of types used in the build classpath, typically enables to ignore tar.gz/natives for example.
*/
@Parameter(property = "arthur.supportedTypes", defaultValue = "jar,zip")
private List<String> supportedTypes;
/**
* Should jar be used instead of exploded folder (target/classes).
* Note this option disable the support of module test classes.
*/
@Parameter(property = "project.usePackagedArtifact", defaultValue = "false")
private boolean usePackagedArtifact;
@Parameter(defaultValue = "${project.packaging}", readonly = true)
private String packaging;
@Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}.${project.packaging}")
private File jar;
@Parameter(defaultValue = "${project.build.outputDirectory}")
private File classes;
@Parameter(defaultValue = "${project.build.testOutputDirectory}")
private File testClasses;
@Parameter(defaultValue = "${repositorySystemSession}")
protected RepositorySystemSession repositorySystemSession;
@Parameter(defaultValue = "${project.remoteProjectRepositories}")
private List<RemoteRepository> remoteRepositories;
@Component
private RepositorySystem repositorySystem;
@Override
public void execute() {
if (skip) {
getLog().info("Skipping execution as requested");
return;
}
if ("pom".equals(packaging)) {
getLog().info("Skipping packaging pom");
return;
}
final List<File> classpathFiles = findClasspathFiles().collect(toList());
final ArthurNativeImageConfiguration configuration = getConfiguration(classpathFiles);
if (nativeImage == null) {
final String graalPlatform = buildPlatform();
final SdkmanGraalVMInstaller graalInstaller = new SdkmanGraalVMInstaller(
offline, inheritIO,
buildDownloadUrl(graalPlatform),
graalVersion,
graalPlatform,
buildCacheGav(graalPlatform),
workdir.toPath(),
getLog(),
gav -> resolve(toArtifact(gav)).getFile().toPath(),
(gav, file) -> install(file.toFile(), toArtifact(gav)));
final Path graalHome = graalInstaller.install();
getLog().info("Using GRAAL: " + graalHome);
configuration.setNativeImage(graalInstaller.installNativeImage().toAbsolutePath().toString());
}
final URL[] urls = classpathFiles.stream().map(it -> {
try {
return it.toURI().toURL();
} catch (final MalformedURLException e) {
throw new IllegalStateException(e);
}
}).toArray(URL[]::new);
final Thread thread = Thread.currentThread();
final ClassLoader parentLoader = useTcclAsScanningParentClassLoader ?
thread.getContextClassLoader() : getSystemClassLoader();
final ClassLoader oldLoader = thread.getContextClassLoader();
try (final URLClassLoader loader = new URLClassLoader(urls, parentLoader);
final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
.setProperty("johnzon.cdi.activated", false)
.withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL))) {
thread.setContextClassLoader(loader);
final AnnotationFinder finder = new AnnotationFinder(new CompositeArchive(Stream.of(urls)
.map(it -> archive(loader, it))
.collect(toList())));
MavenArthurExtension.with(
reflections, resources, bundles, dynamicProxies,
() -> new ArthurNativeImageExecutor(
ArthurNativeImageExecutor.ExecutorConfiguration.builder()
.jsonSerializer(jsonb::toJson)
.finder(finder::findAnnotatedClasses)
.configuration(configuration)
.workingDirectory(workdir.toPath().resolve("generated_configuration"))
.build())
.run());
} catch (final Exception e) {
throw new IllegalStateException(e);
} finally {
thread.setContextClassLoader(oldLoader);
}
if (propertiesPrefix != null) {
project.getProperties().setProperty(propertiesPrefix + "binary.path", output);
}
}
private String buildCacheGav(final String graalPlatform) {
if (!graalPlatform.toLowerCase(ROOT).startsWith("linux")) { // cygwin
return graalCacheGav + ":zip:" + graalPlatform + ':' + graalVersion;
}
return graalCacheGav + ":tar.gz:" + graalPlatform + ':' + graalVersion;
}
private String buildDownloadUrl(final String graalPlatform) {
return graalDownloadUrl
.replace("${graalVersion}", graalVersion)
.replace("${platform}", graalPlatform);
}
private String buildPlatform() {
if (!"auto".equals(graalPlatform)) {
return graalPlatform;
}
return (System.getProperty("os.name", "linux") +
ofNullable(System.getProperty("sun.arch.data.model"))
.orElseGet(() -> System.getProperty("os.arch", "64").replace("amd", "")))
.toLowerCase(ROOT);
}
private Stream<File> findClasspathFiles() {
return Stream.concat(Stream.concat(
usePackagedArtifact ?
Stream.of(jar) :
Stream.concat(
Stream.of(classes),
supportTestArtifacts ? Stream.of(testClasses) : Stream.empty()),
project.getArtifacts().stream()
.filter(a -> !excludedArtifacts.contains(a.getGroupId() + ':' + a.getArtifactId()))
.filter(this::handleTestInclusion)
.filter(this::isNotSvm)
.filter(a -> supportedTypes.contains(a.getType()))
.map(Artifact::getFile)),
resolveExtension())
.filter(File::exists);
}
private boolean handleTestInclusion(final Artifact a) {
return !"test".equals(a.getScope()) || supportTestArtifacts;
}
private boolean isNotSvm(final Artifact artifact) {
return !"com.oracle.substratevm".equals(artifact.getGroupId());
}
private Stream<File> resolveExtension() {
return ofNullable(graalExtensions)
.map(e -> e.stream()
.map(this::toArtifact)
.map(this::resolve)
.map(org.eclipse.aether.artifact.Artifact::getFile))
.orElseGet(Stream::empty);
}
private org.eclipse.aether.artifact.Artifact toArtifact(final String s) {
return new DefaultArtifact(s);
}
private Path install(final File file, final org.eclipse.aether.artifact.Artifact art) {
final org.eclipse.aether.artifact.Artifact artifact = new DefaultArtifact(
art.getGroupId(),
art.getArtifactId(),
art.getClassifier(),
art.getExtension(),
art.getVersion(),
emptyMap(),
file);
try {
final InstallResult result = repositorySystem.install(
repositorySystemSession,
new InstallRequest().addArtifact(artifact));
if (result.getArtifacts().isEmpty()) {
throw new IllegalStateException("Can't install " + art);
}
return resolve(art).getFile().toPath();
} catch (final InstallationException e) {
throw new IllegalStateException(e);
}
}
private org.eclipse.aether.artifact.Artifact resolve(final org.eclipse.aether.artifact.Artifact art) {
final ArtifactRequest artifactRequest =
new ArtifactRequest().setArtifact(art).setRepositories(remoteRepositories);
try {
final ArtifactResult result = repositorySystem.resolveArtifact(repositorySystemSession, artifactRequest);
if (result.isMissing()) {
throw new IllegalStateException("Can't find " + art);
}
return result.getArtifact();
} catch (final ArtifactResolutionException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
private ArthurNativeImageConfiguration getConfiguration(final Collection<File> classpathFiles) {
final ArthurNativeImageConfiguration configuration = new ArthurNativeImageConfiguration();
Stream.of(ArthurNativeImageConfiguration.class.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(ArthurNativeImageConfiguration.GraalCommandPart.class))
.map(this::asAccessible)
.forEach(field -> {
try {
final Field mojoField = asAccessible(NativeImageMojo.class.getDeclaredField(field.getName()));
field.set(configuration, mojoField.get(NativeImageMojo.this));
} catch (final NoSuchFieldException | IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
});
if (configuration.getClasspath() == null || configuration.getClasspath().isEmpty()) {
configuration.setClasspath(classpathFiles.stream().map(File::getAbsolutePath).collect(toList()));
}
configuration.setInheritIO(inheritIO);
return configuration;
}
private Field asAccessible(final Field field) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
}
}