org.apache.maven.plugins.jlink.JLinkMojo Maven / Gradle / Ivy
/*
* 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.maven.plugins.jlink;
/*
* 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.
*/
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Optional;
import org.apache.commons.io.FileUtils;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.Artifact;
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.apache.maven.project.MavenProjectHelper;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainPrivate;
import org.apache.maven.toolchain.java.DefaultJavaToolChain;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.zip.ZipArchiver;
import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
import org.codehaus.plexus.languages.java.jpms.LocationManager;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
import org.codehaus.plexus.languages.java.version.JavaVersion;
import static java.util.Collections.singletonMap;
/**
* The JLink goal is intended to create a Java Run Time Image file based on
* https://openjdk.java.net/jeps/282,
* https://openjdk.java.net/jeps/220.
*
* @author Karl Heinz Marbaise [email protected]
*/
@Mojo(name = "jlink", requiresDependencyResolution = ResolutionScope.RUNTIME, defaultPhase = LifecyclePhase.PACKAGE)
public class JLinkMojo extends AbstractJLinkMojo {
@Component
private LocationManager locationManager;
/**
*
* Specify the requirements for this jdk toolchain. This overrules the toolchain selected by the
* maven-toolchain-plugin.
*
* note: requires at least Maven 3.3.1
*/
@Parameter
private Map jdkToolchain;
/**
* This is intended to strip debug information out. The command line equivalent of jlink
is:
* -G, --strip-debug
strip debug information.
*/
@Parameter(defaultValue = "false")
private boolean stripDebug;
/**
* Here you can define the compression of the resources being used. The command line equivalent is:
* -c, --compress=<level>
.
*
* The valid values for the level depend on the JDK:
*
* For JDK 9+:
*
* - 0: No compression. Equivalent to zip-0.
* - 1: Constant String Sharing
* - 2: Equivalent to zip-6.
*
*
* For JDK 21+, those values are deprecated and to be removed in a future version.
* The supported values are:
* {@code zip-[0-9]}, where {@code zip-0} provides no compression,
* and {@code zip-9} provides the best compression.
* Default is {@code zip-6}.
*/
@Parameter
private String compress;
/**
* Should the plugin generate a launcher script by means of jlink? The command line equivalent is:
* --launcher <name>=<module>[/<mainclass>]
. The valid values for the level are:
* <name>=<module>[/<mainclass>]
.
*/
@Parameter
private String launcher;
/**
* These JVM arguments will be appended to the {@code lib/modules} file.
* This parameter requires at least JDK 14.
*
* The command line equivalent is: {@code jlink --add-options="..."}.
*
* Example:
*
*
* <addOptions>
* <addOption>-Xmx256m</addOption>
* <addOption>--enable-preview</addOption>
* <addOption>-Dvar=value</addOption>
* </addOptions>
*
*
* Above example will result in {@code jlink --add-options="-Xmx256m" --enable-preview -Dvar=value"}.
*/
@Parameter
private List addOptions;
/**
* Limit the universe of observable modules. The following gives an example of the configuration which can be used
* in the pom.xml
file.
*
*
* <limitModules>
* <limitModule>mod1</limitModule>
* <limitModule>xyz</limitModule>
* .
* .
* </limitModules>
*
*
* This configuration is the equivalent of the command line option:
* --limit-modules <mod>[,<mod>...]
*/
@Parameter
private List limitModules;
/**
*
* Usually this is not necessary, cause this is handled automatically by the given dependencies.
*
*
* By using the --add-modules you can define the root modules to be resolved. The configuration in
* pom.xml
file can look like this:
*
*
*
* <addModules>
* <addModule>mod1</addModule>
* <addModule>first</addModule>
* .
* .
* </addModules>
*
*
* The command line equivalent for jlink is: --add-modules <mod>[,<mod>...]
.
*/
@Parameter
private List addModules;
/**
* Define the plugin module path to be used. There can be defined multiple entries separated by either {@code ;} or
* {@code :}. The jlink command line equivalent is: --plugin-module-path <modulepath>
*/
@Parameter
private String pluginModulePath;
/**
* The output directory for the resulting Run Time Image. The created Run Time Image is stored in non compressed
* form. This will later being packaged into a zip
file. --output <path>
*
* The {@link #classifier} is appended as a subdirecty if it exists,
* otherwise {@code default} will be used as subdirectory.
* This ensures that multiple executions using classifiers will not overwrite the previous run’s image.
*/
@Parameter(defaultValue = "${project.build.directory}/maven-jlink", required = true, readonly = true)
private File outputDirectoryImage;
@Parameter(defaultValue = "${project.build.directory}", required = true, readonly = true)
private File buildDirectory;
@Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
private File outputDirectory;
/**
* The byte order of the generated Java Run Time image. --endian <little|big>
. If the endian is
* not given the default is: native
.
*/
// TODO: Should we define either little or big as default? or should we left as it.
@Parameter
private String endian;
/**
* Include additional paths on the --module-path
option. Project dependencies and JDK modules are
* automatically added.
*/
@Parameter
private List modulePaths;
/**
* Add the option --bind-services
or not.
*/
@Parameter(defaultValue = "false")
private boolean bindServices;
/**
* You can disable a plugin by using this option. --disable-plugin pluginName
.
*/
@Parameter
private String disablePlugin;
/**
* --ignore-signing-information
*/
@Parameter(defaultValue = "false")
private boolean ignoreSigningInformation;
/**
* This will suppress to have an includes
directory in the resulting Java Run Time Image. The JLink
* command line equivalent is: --no-header-files
*/
@Parameter(defaultValue = "false")
private boolean noHeaderFiles;
/**
* This will suppress to have the man
directory in the resulting Java Run Time Image. The JLink command
* line equivalent is: --no-man-pages
*/
@Parameter(defaultValue = "false")
private boolean noManPages;
/**
* Suggest providers that implement the given service types from the module path.
*
*
* <suggestProviders>
* <suggestProvider>name-a</suggestProvider>
* <suggestProvider>name-b</suggestProvider>
* .
* .
* </suggestProviders>
*
*
* The jlink command linke equivalent: --suggest-providers [<name>,...]
*/
@Parameter
private List suggestProviders;
/**
* Includes the list of locales where langtag is a BCP 47 language tag.
*
* This option supports locale matching as defined in RFC 4647.
* Ensure that you add the module jdk.localedata when using this option.
*
* The command line equivalent is: --include-locales=en,ja,*-IN
.
*
*
* <includeLocales>
* <includeLocale>en</includeLocale>
* <includeLocale>ja</includeLocale>
* <includeLocale>*-IN</includeLocale>
* .
* .
* </includeLocales>
*
*/
@Parameter
private List includeLocales;
/**
* This will turn on verbose mode. The jlink command line equivalent is: --verbose
*/
@Parameter(defaultValue = "false")
private boolean verbose;
/**
* The JAR archiver needed for archiving the environments.
*/
@Component(role = Archiver.class, hint = "zip")
private ZipArchiver zipArchiver;
/**
* Set the JDK location to create a Java custom runtime image.
*/
@Parameter
private File sourceJdkModules;
/**
* Classifier to add to the artifact generated. If given, the artifact will be attached
* as a supplemental artifact.
* If not given this will create the main artifact which is the default behavior.
* If you try to do that a second time without using a classifier the build will fail.
*/
@Parameter
private String classifier;
/**
* Name of the generated ZIP file in the target
directory. This will not change the name of the
* installed/deployed file.
*/
@Parameter(defaultValue = "${project.build.finalName}", readonly = true)
private String finalName;
/**
* Timestamp for reproducible output archive entries, either formatted as ISO 8601
* yyyy-MM-dd'T'HH:mm:ssXXX
or as an int representing seconds since the epoch (like
* SOURCE_DATE_EPOCH).
*
* @since 3.2.0
*/
@Parameter(defaultValue = "${project.build.outputTimestamp}")
private String outputTimestamp;
/**
* Convenience interface for plugins to add or replace artifacts and resources on projects.
*/
@Component
private MavenProjectHelper projectHelper;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
failIfParametersAreNotInTheirValidValueRanges();
setOutputDirectoryImage();
ifOutputDirectoryExistsDelteIt();
JLinkExecutor jLinkExec = getExecutor();
Collection modulesToAdd = new ArrayList<>();
if (addModules != null) {
modulesToAdd.addAll(addModules);
}
jLinkExec.addAllModules(modulesToAdd);
Collection pathsOfModules = new ArrayList<>();
if (modulePaths != null) {
pathsOfModules.addAll(modulePaths);
}
for (Entry item : getModulePathElements().entrySet()) {
getLog().info(" -> module: " + item.getKey() + " ( "
+ item.getValue().getPath() + " )");
// We use the real module name and not the artifact Id...
modulesToAdd.add(item.getKey());
pathsOfModules.add(item.getValue().getPath());
}
// The jmods directory of the JDK
jLinkExec
.getJmodsFolder(this.sourceJdkModules)
.ifPresent(jmodsFolder -> pathsOfModules.add(jmodsFolder.getAbsolutePath()));
jLinkExec.addAllModulePaths(pathsOfModules);
List jlinkArgs = createJlinkArgs(pathsOfModules, modulesToAdd);
try {
jLinkExec.executeJlink(jlinkArgs);
} catch (IllegalStateException e) {
throw new MojoFailureException("Unable to find jlink command: " + e.getMessage(), e);
}
File createZipArchiveFromImage = createZipArchiveFromImage(buildDirectory, outputDirectoryImage);
if (hasClassifier()) {
projectHelper.attachArtifact(getProject(), "jlink", getClassifier(), createZipArchiveFromImage);
} else {
if (projectHasAlreadySetAnArtifact()) {
throw new MojoExecutionException("You have to use a classifier "
+ "to attach supplemental artifacts to the project instead of replacing them.");
}
getProject().getArtifact().setFile(createZipArchiveFromImage);
}
}
private List getCompileClasspathElements(MavenProject project) {
List list = new ArrayList<>(project.getArtifacts().size() + 1);
for (Artifact a : project.getArtifacts()) {
getLog().debug("Artifact: " + a.getGroupId() + ":" + a.getArtifactId() + ":" + a.getVersion());
list.add(a.getFile());
}
return list;
}
private Map getModulePathElements() throws MojoFailureException {
// For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules
// and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
// you cannot depend on this project and so it won't be distributed.
Map modulepathElements = new HashMap<>();
try {
Collection dependencyArtifacts = getCompileClasspathElements(getProject());
ResolvePathsRequest request = ResolvePathsRequest.ofFiles(dependencyArtifacts);
Optional toolchain = getToolchain();
if (toolchain.isPresent()
&& toolchain.orElseThrow(NoSuchElementException::new) instanceof DefaultJavaToolChain) {
Toolchain toolcahin1 = toolchain.orElseThrow(NoSuchElementException::new);
request.setJdkHome(new File(((DefaultJavaToolChain) toolcahin1).getJavaHome()));
}
ResolvePathsResult resolvePathsResult = locationManager.resolvePaths(request);
for (Map.Entry entry :
resolvePathsResult.getPathElements().entrySet()) {
JavaModuleDescriptor descriptor = entry.getValue();
if (descriptor == null) {
String message = "The given dependency " + entry.getKey()
+ " does not have a module-info.java file. So it can't be linked.";
getLog().error(message);
throw new MojoFailureException(message);
}
// Don't warn for automatic modules, let the jlink tool do that
getLog().debug(" module: " + descriptor.name() + " automatic: " + descriptor.isAutomatic());
if (modulepathElements.containsKey(descriptor.name())) {
getLog().warn("The module name " + descriptor.name() + " does already exists.");
}
modulepathElements.put(descriptor.name(), entry.getKey());
}
// This part is for the module in target/classes ? (Hacky..)
// FIXME: Is there a better way to identify that code exists?
if (outputDirectory.exists()) {
List singletonList = Collections.singletonList(outputDirectory);
ResolvePathsRequest singleModuls = ResolvePathsRequest.ofFiles(singletonList);
ResolvePathsResult resolvePaths = locationManager.resolvePaths(singleModuls);
for (Entry entry :
resolvePaths.getPathElements().entrySet()) {
JavaModuleDescriptor descriptor = entry.getValue();
if (descriptor == null) {
String message = "The given project " + entry.getKey()
+ " does not contain a module-info.java file. So it can't be linked.";
getLog().error(message);
throw new MojoFailureException(message);
}
if (modulepathElements.containsKey(descriptor.name())) {
getLog().warn("The module name " + descriptor.name() + " does already exists.");
}
modulepathElements.put(descriptor.name(), entry.getKey());
}
}
} catch (IOException e) {
getLog().error(e.getMessage());
throw new MojoFailureException(e.getMessage());
}
return modulepathElements;
}
private JLinkExecutor getExecutor() {
return getJlinkExecutor();
}
private boolean projectHasAlreadySetAnArtifact() {
if (getProject().getArtifact().getFile() != null) {
return getProject().getArtifact().getFile().isFile();
} else {
return false;
}
}
/**
* @return true in case where the classifier is not {@code null} and contains something else than white spaces.
*/
protected boolean hasClassifier() {
boolean result = false;
if (getClassifier() != null && !getClassifier().isEmpty()) {
result = true;
}
return result;
}
private File createZipArchiveFromImage(File outputDirectory, File outputDirectoryImage)
throws MojoExecutionException {
zipArchiver.addDirectory(outputDirectoryImage);
// configure for Reproducible Builds based on outputTimestamp value
Date lastModified = new MavenArchiver().parseOutputTimestamp(outputTimestamp);
if (lastModified != null) {
zipArchiver.configureReproducible(lastModified);
}
File resultArchive = getArchiveFile(outputDirectory, finalName, getClassifier(), "zip");
zipArchiver.setDestFile(resultArchive);
try {
zipArchiver.createArchive();
} catch (ArchiverException | IOException e) {
getLog().error(e.getMessage(), e);
throw new MojoExecutionException(e.getMessage(), e);
}
return resultArchive;
}
private void failIfParametersAreNotInTheirValidValueRanges() throws MojoFailureException {
if (endian != null && (!"big".equals(endian) && !"little".equals(endian))) {
String message = "The given endian parameter " + endian
+ " does not contain one of the following values: 'little' or 'big'.";
getLog().error(message);
throw new MojoFailureException(message);
}
if (addOptions != null && !addOptions.isEmpty()) {
requireJdk14();
}
}
private void requireJdk14() throws MojoFailureException {
// needs JDK 14+
Optional optToolchain = getToolchain();
String java14reqMsg = "parameter 'addOptions' needs at least a Java 14 runtime or a Java 14 toolchain.";
if (optToolchain.isPresent()) {
Toolchain toolchain = optToolchain.orElseThrow(NoSuchElementException::new);
if (!(toolchain instanceof ToolchainPrivate)) {
getLog().warn("Unable to check toolchain java version.");
return;
}
ToolchainPrivate toolchainPrivate = (ToolchainPrivate) toolchain;
if (!toolchainPrivate.matchesRequirements(singletonMap("jdk", "14"))) {
throw new MojoFailureException(java14reqMsg);
}
} else if (!JavaVersion.JAVA_VERSION.isAtLeast("14")) {
throw new MojoFailureException(java14reqMsg);
}
}
/**
* Use a separate directory for each image.
*
* Rationale: If a user creates multiple jlink artifacts using classifiers,
* the directories should not overwrite themselves for each execution.
*/
private void setOutputDirectoryImage() {
if (hasClassifier()) {
final File classifiersDirectory = new File(outputDirectoryImage, "classifiers");
outputDirectoryImage = new File(classifiersDirectory, classifier);
} else {
outputDirectoryImage = new File(outputDirectoryImage, "default");
}
}
private void ifOutputDirectoryExistsDelteIt() throws MojoExecutionException {
if (outputDirectoryImage.exists()) {
// Delete the output folder of JLink before we start
// otherwise JLink will fail with a message "Error: directory already exists: ..."
try {
getLog().debug("Deleting existing " + outputDirectoryImage.getAbsolutePath());
FileUtils.forceDelete(outputDirectoryImage);
} catch (IOException e) {
getLog().error("IOException", e);
throw new MojoExecutionException(
"Failure during deletion of " + outputDirectoryImage.getAbsolutePath() + " occured.");
}
}
}
protected List createJlinkArgs(Collection pathsOfModules, Collection modulesToAdd) {
List jlinkArgs = new ArrayList<>();
if (stripDebug) {
jlinkArgs.add("--strip-debug");
}
if (bindServices) {
jlinkArgs.add("--bind-services");
}
if (endian != null) {
jlinkArgs.add("--endian");
jlinkArgs.add(endian);
}
if (ignoreSigningInformation) {
jlinkArgs.add("--ignore-signing-information");
}
if (compress != null) {
jlinkArgs.add("--compress");
jlinkArgs.add(compress);
}
if (launcher != null) {
jlinkArgs.add("--launcher");
jlinkArgs.add(launcher);
}
if (addOptions != null && !addOptions.isEmpty()) {
jlinkArgs.add("--add-options");
jlinkArgs.add(String.format("\"%s\"", String.join(" ", addOptions)));
}
if (disablePlugin != null) {
jlinkArgs.add("--disable-plugin");
jlinkArgs.add(disablePlugin);
}
if (pathsOfModules != null && !pathsOfModules.isEmpty()) {
// @formatter:off
jlinkArgs.add("--module-path");
jlinkArgs.add(getPlatformDependSeparateList(pathsOfModules).replace("\\", "\\\\"));
// @formatter:off
}
if (noHeaderFiles) {
jlinkArgs.add("--no-header-files");
}
if (noManPages) {
jlinkArgs.add("--no-man-pages");
}
if (hasSuggestProviders()) {
jlinkArgs.add("--suggest-providers");
String sb = getCommaSeparatedList(suggestProviders);
jlinkArgs.add(sb);
}
if (hasLimitModules()) {
jlinkArgs.add("--limit-modules");
String sb = getCommaSeparatedList(limitModules);
jlinkArgs.add(sb);
}
if (!modulesToAdd.isEmpty()) {
jlinkArgs.add("--add-modules");
// This must be name of the module and *NOT* the name of the
// file! Can we somehow pre check this information to fail early?
String sb = getCommaSeparatedList(modulesToAdd);
jlinkArgs.add(sb.replace("\\", "\\\\"));
}
if (hasIncludeLocales()) {
jlinkArgs.add("--add-modules");
jlinkArgs.add("jdk.localedata");
jlinkArgs.add("--include-locales");
String sb = getCommaSeparatedList(includeLocales);
jlinkArgs.add(sb);
}
if (pluginModulePath != null) {
jlinkArgs.add("--plugin-module-path");
StringBuilder sb = convertSeparatedModulePathToPlatformSeparatedModulePath(pluginModulePath);
jlinkArgs.add(sb.toString().replace("\\", "\\\\"));
}
if (buildDirectory != null) {
jlinkArgs.add("--output");
jlinkArgs.add(outputDirectoryImage.getAbsolutePath());
}
if (verbose) {
jlinkArgs.add("--verbose");
}
return Collections.unmodifiableList(jlinkArgs);
}
private boolean hasIncludeLocales() {
return includeLocales != null && !includeLocales.isEmpty();
}
private boolean hasSuggestProviders() {
return suggestProviders != null && !suggestProviders.isEmpty();
}
private boolean hasLimitModules() {
return limitModules != null && !limitModules.isEmpty();
}
/**
* {@inheritDoc}
*/
protected String getClassifier() {
return classifier;
}
}