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

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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy