You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ant.apache.org by bo...@apache.org on 2018/12/15 16:28:13 UTC
[3/6] ant git commit: Added tasks for JDK's jmod and jlink tools.
http://git-wip-us.apache.org/repos/asf/ant/blob/343dff90/src/main/org/apache/tools/ant/taskdefs/modules/Link.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/modules/Link.java b/src/main/org/apache/tools/ant/taskdefs/modules/Link.java
new file mode 100644
index 0000000..36fd281
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/modules/Link.java
@@ -0,0 +1,2120 @@
+/*
+ * 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.tools.ant.taskdefs.modules;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Reader;
+import java.io.IOException;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import java.nio.file.Files;
+import java.nio.file.FileVisitResult;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.ArrayList;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Properties;
+
+import java.util.Collections;
+import java.util.Objects;
+
+import java.util.spi.ToolProvider;
+
+import java.util.stream.Stream;
+import java.util.stream.Collectors;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.Task;
+
+import org.apache.tools.ant.types.EnumeratedAttribute;
+import org.apache.tools.ant.types.LogLevel;
+import org.apache.tools.ant.types.Path;
+import org.apache.tools.ant.types.Reference;
+import org.apache.tools.ant.types.ResourceCollection;
+
+import org.apache.tools.ant.util.CompositeMapper;
+import org.apache.tools.ant.util.MergingMapper;
+
+import org.apache.tools.ant.util.FileUtils;
+import org.apache.tools.ant.util.ResourceUtils;
+
+/**
+ * Assembles jmod files into an executable image. Equivalent to the
+ * JDK {@code jlink} command.
+ * <p>
+ * Supported attributes:
+ * <dl>
+ * <dt>{@code destDir}
+ * <dd>Root directory of created image. (required)
+ * <dt>{@code modulePath}
+ * <dd>Path of modules. Should be a list of .jmod files. Required, unless
+ * nested module path or modulepathref is present.
+ * <dt>{@code modulePathRef}
+ * <dd>Reference to path of modules. Referenced path should be
+ * a list of .jmod files.
+ * <dt>{@code modules}
+ * <dd>Comma-separated list of modules to assemble. Required, unless
+ * one or more nested {@code <module>} elements are present.
+ * <dt>{@code observableModules}
+ * <dd>Comma-separated list of explicit modules that comprise
+ * "universe" visible to tool while linking.
+ * <dt>{@code launchers}
+ * <dd>Comma-separated list of commands, each of the form
+ * <var>name</var>{@code =}<var>module</var> or
+ * <var>name</var>{@code =}<var>module</var>{@code /}<var>mainclass</var>
+ * <dt>{@code excludeFiles}
+ * <dd>Comma-separated list of patterns specifying files to exclude from
+ * linked image.
+ * Each is either a <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29">standard PathMatcher pattern</a>
+ * or {@code @}<var>filename</var>.
+ * <dt>{@code excludeResources}
+ * <dd>Comma-separated list of patterns specifying resources to exclude from jmods.
+ * Each is either a <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29">standard PathMatcher pattern</a>
+ * or {@code @}<var>filename</var>.
+ * <dt>{@code locales}
+ * <dd>Comma-separated list of extra locales to include,
+ * requires {@code jdk.localedata} module
+ * <dt>{@code resourceOrder}
+ * <dt>Comma-separated list of patterns specifying resource search order.
+ * Each is either a <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29">standard PathMatcher pattern</a>
+ * or {@code @}<var>filename</var>.
+ * <dt>{@code bindServices}
+ * <dd>boolean, whether to link service providers; default is false
+ * <dt>{@code ignoreSigning}
+ * <dd>boolean, whether to allow signed jar files; default is false
+ * <dt>{@code includeHeaders}
+ * <dd>boolean, whether to include header files; default is true
+ * <dt>{@code includeManPages}
+ * <dd>boolean, whether to include man pages; default is true
+ * <dt>{@code includeNativeCommands}
+ * <dd>boolean, whether to include native executables normally generated
+ * for image; default is true
+ * <dt>{@code debug}
+ * <dd>boolean, whether to include debug information; default is true
+ * <dt>{@code verboseLevel}
+ * <dd>If set, jlink will produce verbose output, which will be logged at
+ * the specified Ant log level ({@code DEBUG}, {@code VERBOSE},
+ * {@code INFO}}, {@code WARN}, or {@code ERR}).
+ * <dt>{@code compress}
+ * <dd>compression level, one of:
+ * <dl>
+ * <dt>{@code 0}
+ * <dt>{@code none}
+ * <dd>no compression (default)
+ * <dt>{@code 1}
+ * <dt>{@code strings}
+ * <dd>constant string sharing
+ * <dt>{@code 2}
+ * <dt>{@code zip}
+ * <dd>zip compression
+ * </dl>
+ * <dt>{@code endianness}
+ * <dd>Must be {@code little} or {@code big}, default is native endianness
+ * <dt>{@code checkDuplicateLegal}
+ * <dd>Boolean. When merging legal notices from different modules
+ * because they have the same name, verify that their contents
+ * are identical. Default is false, which means any license files
+ * with the same name are assumed to have the same content, and no
+ * checking is done.
+ * <dt>{@code vmType}
+ * <dd>Hotspot VM in image, one of:
+ * <ul>
+ * <li>{@code client}
+ * <li>{@code server}
+ * <li>{@code minimal}
+ * <li>{@code all} (default)
+ * </ul>
+ * </dl>
+ *
+ * <p>
+ * Supported nested elements
+ * <dl>
+ * <dt>{@code <modulepath>}
+ * <dd>path element
+ * <dt>{@code <module>}
+ * <dd>May be specified multiple times.
+ * Only attribute is required {@code name} attribute.
+ * <dt>{@code <observableModule>}
+ * <dd>May be specified multiple times.
+ * Only attribute is required {@code name} attribute.
+ * <dt>{@code <launcher>}
+ * <dd>May be specified multiple times. Attributes:
+ * <ul>
+ * <li>{@code name} (required)
+ * <li>{@code module} (required)
+ * <li>{@code mainClass} (optional)
+ * </ul>
+ * <dt>{@code <locale>}
+ * <dd>May be specified multiple times.
+ * Only attribute is required {@code name} attribute.
+ * <dt>{@code <resourceOrder>}
+ * <dd>Explicit resource search order in image. May be specified multiple
+ * times. Exactly one of these attributes must be specified:
+ * <dl>
+ * <dt>{@code pattern}
+ * <dd>A <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29">standard PathMatcher pattern</a>
+ * <dt>{@code listFile}
+ * <dd>Text file containing list of resource names (not patterns),
+ * one per line
+ * </dl>
+ * If the {@code resourceOrder} attribute is also present on the task, its
+ * patterns are treated as if they occur before patterns in nested
+ * {@code <resourceOrder>} elements.
+ * <dt>{@code <excludeFiles>}
+ * <dd>Excludes files from linked image tree. May be specified multiple times.
+ * Exactly one of these attributes is required:
+ * <dl>
+ * <dt>{@code pattern}
+ * <dd>A <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29">standard PathMatcher pattern</a>
+ * <dt>{@code listFile}
+ * <dd>Text file containing list of file names (not patterns),
+ * one per line
+ * </dl>
+ * <dt>{@code <excludeResources>}
+ * <dd>Excludes resources from jmods. May be specified multiple times.
+ * Exactly one of these attributes is required:
+ * <dl>
+ * <dt>{@code pattern}
+ * <dd>A <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29">standard PathMatcher pattern</a>
+ * <dt>{@code listFile}
+ * <dd>Text file containing list of resource names (not patterns),
+ * one per line
+ * </dl>
+ * <dt>{@code <compress>}
+ * <dd>Must have {@code level} attribute, whose permitted values are the same
+ * as the {@code compress} task attribute described above.
+ * May also have a {@code files} attribute, which is a comma-separated
+ * list of patterns, and/or nested {@code <files>} elements, each with
+ * either a {@code pattern} attribute or {@code listFile} attribute.
+ * <dt>{@code <releaseInfo>}
+ * <dd>Replaces, augments, or trims the image's release info properties.
+ * This may specify any of the following:
+ * <ul>
+ * <li>A {@code file} attribute, pointing to a Java properties file
+ * containing new release info properties that will entirely replace
+ * the current ones.
+ * <li>A {@code delete} attribute, containing comma-separated property keys
+ * to remove from application's release info, and/or any number of
+ * nested {@code <delete>} elements, each with a required {@code key}
+ * attribute.
+ * <li>One or more nested {@code <add>} elements, containing either
+ * {@code key} and {@code value} attributes, or a {@code file}
+ * attribute and an optional {@code charset} attribute.
+ * </ul>
+ * </dl>
+ *
+ * @see <a href="https://docs.oracle.com/en/java/javase/11/tools/jlink.html"><code>jlink</code> tool reference</a>
+ */
+public class Link
+extends Task {
+ /**
+ * Error message for improperly formatted launcher attribute.
+ */
+ private static final String INVALID_LAUNCHER_STRING =
+ "Launcher command must take the form name=module "
+ + "or name=module/mainclass";
+
+ /** Path of directories containing linkable modules. */
+ private Path modulePath;
+
+ /** Modules to include in linked image. */
+ private final List<ModuleSpec> modules = new ArrayList<>();
+
+ /** If non-empty, list of all modules linker is permitted to know about. */
+ private final List<ModuleSpec> observableModules = new ArrayList<>();
+
+ /**
+ * Additional runnable programs which linker will place in image's
+ * <code>bin</code> directory.
+ */
+ private final List<Launcher> launchers = new ArrayList<>();
+
+ /**
+ * Locales to explicitly include from {@code jdk.localdata} module.
+ * If empty, all locales are included.
+ */
+ private final List<LocaleSpec> locales = new ArrayList<>();
+
+ /** Resource ordering. */
+ private final List<PatternListEntry> ordering = new ArrayList<>();
+
+ /** Files to exclude from linked image. */
+ private final List<PatternListEntry> excludedFiles = new ArrayList<>();
+
+ /**
+ * Resources in linked modules which should be excluded from linked image.
+ */
+ private final List<PatternListEntry> excludedResources = new ArrayList<>();
+
+ /**
+ * Whether to include all service provides in linked image which are
+ * present in the module path and which are needed by modules explicitly
+ * linked.
+ */
+ private boolean bindServices;
+
+ /**
+ * Whether to ignore signed jars (and jmods based on signed jars) when
+ * linking, instead of emitting an error.
+ */
+ private boolean ignoreSigning;
+
+ /** Whether to include header files from linked modules in image. */
+ private boolean includeHeaders = true;
+
+ /** Whether to include man pages from linked modules in image. */
+ private boolean includeManPages = true;
+
+ /** Whether to include native commands from linked modules in image. */
+ private boolean includeNativeCommands = true;
+
+ /** Whether to include classes' debug information or strip it. */
+ private boolean debug = true;
+
+ /**
+ * The Ant logging level at which verbose output of linked should be
+ * emitted. If null, verbose output is disabled.
+ */
+ private LogLevel verboseLevel;
+
+ /** Directory into which linked image will be placed. */
+ private File outputDir;
+
+ /** Endianness of some files (?) in linked image. */
+ private Endianness endianness;
+
+ /**
+ * Simple compression level applied to linked image.
+ * This or {@link #compression} may be set, but not both.
+ */
+ private CompressionLevel compressionLevel;
+
+ /**
+ * Describes which files in image to compress, and how to compress them.
+ * This or {@link #compressionLevel} may be set, but not both.
+ */
+ private Compression compression;
+
+ /**
+ * Whether to check duplicate legal notices from different modules
+ * actually have identical content, not just indentical names,
+ * before merging them.
+ * <a href="https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java#L80">Forced to true as of Java 11.</a>
+ */
+ private boolean checkDuplicateLegal;
+
+ /** Type of VM in linked image. */
+ private VMType vmType;
+
+ /** Changes to linked image's default release info. */
+ private final List<ReleaseInfo> releaseInfo = new ArrayList<>();
+
+ /**
+ * Adds child {@code <modulePath>} element.
+ *
+ * @return new, empty child element
+ *
+ * @see #setModulePath(Path)
+ */
+ public Path createModulePath() {
+ if (modulePath == null) {
+ modulePath = new Path(getProject());
+ }
+ return modulePath.createPath();
+ }
+
+ /**
+ * Attribute containing path of directories containing linkable modules.
+ *
+ * @return current module path, possibly {@code null}
+ *
+ * @see #setModulePath(Path)
+ * @see #createModulePath()
+ */
+ public Path getModulePath() {
+ return modulePath;
+ }
+
+ /**
+ * Sets attribute containing path of directories containing
+ * linkable modules.
+ *
+ * @param path new module path
+ *
+ * @see #getModulePath()
+ * @see #setModulePathRef(Reference)
+ * @see #createModulePath()
+ */
+ public void setModulePath(final Path path) {
+ if (modulePath == null) {
+ this.modulePath = path;
+ } else {
+ modulePath.append(path);
+ }
+ }
+
+ /**
+ * Sets module path as a reference.
+ *
+ * @param ref path reference
+ *
+ * @see #setModulePath(Path)
+ * @see #createModulePath()
+ */
+ public void setModulePathRef(final Reference ref) {
+ createModulePath().setRefid(ref);
+ }
+
+ /**
+ * Adds child {@code <module>} element, specifying a module to link.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setModules(String)
+ */
+ public ModuleSpec createModule() {
+ ModuleSpec module = new ModuleSpec();
+ modules.add(module);
+ return module;
+ }
+
+ /**
+ * Sets attribute containing list of modules to link.
+ *
+ * @param moduleList comma-separated list of module names
+ */
+ public void setModules(final String moduleList) {
+ for (String moduleName : moduleList.split(",")) {
+ modules.add(new ModuleSpec(moduleName));
+ }
+ }
+
+ /**
+ * Creates child {@code <observableModule>} element that represents
+ * one of the modules the linker is permitted to know about.
+ *
+ * @return new, unconfigured child element
+ */
+ public ModuleSpec createObservableModule() {
+ ModuleSpec module = new ModuleSpec();
+ observableModules.add(module);
+ return module;
+ }
+
+ /**
+ * Sets attribute containing modules linker is permitted to know about.
+ *
+ * @param moduleList comma-separated list of module names
+ */
+ public void setObservableModules(final String moduleList) {
+ for (String moduleName : moduleList.split(",")) {
+ observableModules.add(new ModuleSpec(moduleName));
+ }
+ }
+
+ /**
+ * Creates child {@code <launcher>} element that can contain information
+ * on additional executable in the linked image.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setLaunchers(String)
+ */
+ public Launcher createLauncher() {
+ Launcher command = new Launcher();
+ launchers.add(command);
+ return command;
+ }
+
+ /**
+ * Sets attribute containing comma-separated list of information needed for
+ * additional executables in the linked image. Each item must be of the
+ * form * <var>name</var>{@code =}<var>module</var> or
+ * <var>name</var>{@code =}<var>module</var>{@code /}<var>mainclass</var>.
+ *
+ * @param launcherList comma-separated list of launcher data
+ */
+ public void setLaunchers(final String launcherList) {
+ for (String launcherSpec : launcherList.split(",")) {
+ launchers.add(new Launcher(launcherSpec));
+ }
+ }
+
+ /**
+ * Creates child {@code <locale>} element that specifies a Java locale,
+ * or set of locales, to include from the {@code jdk.localedata} module
+ * in the linked image.
+ *
+ * @return new, unconfigured child element
+ */
+ public LocaleSpec createLocale() {
+ LocaleSpec locale = new LocaleSpec();
+ locales.add(locale);
+ return locale;
+ }
+
+ /**
+ * Sets attribute containing a list of locale patterns, to specify
+ * Java locales to include from {@code jdk.localedata} module in
+ * linked image. Asterisks ({@code *}) are permitted for wildcard
+ * matches.
+ *
+ * @param localeList comma-separated list of locale patterns
+ */
+ public void setLocales(final String localeList) {
+ for (String localeName : localeList.split(",")) {
+ locales.add(new LocaleSpec(localeName));
+ }
+ }
+
+ /**
+ * Creates child {@code <excludeFiles>} element that specifies
+ * files to exclude from linked modules when assembling linked image.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setExcludeFiles(String)
+ */
+ public PatternListEntry createExcludeFiles() {
+ PatternListEntry entry = new PatternListEntry();
+ excludedFiles.add(entry);
+ return entry;
+ }
+
+ /**
+ * Sets attribute containing a list of patterns denoting files
+ * to exclude from linked modules when assembling linked image.
+ *
+ * @param patternList comman-separated list of patterns
+ *
+ * @see Link.PatternListEntry
+ */
+ public void setExcludeFiles(String patternList) {
+ for (String pattern : patternList.split(",")) {
+ excludedFiles.add(new PatternListEntry(pattern));
+ }
+ }
+
+ /**
+ * Creates child {@code <excludeResources>} element that specifies
+ * resources in linked modules that will be excluded from linked image.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setExcludeResources(String)
+ */
+ public PatternListEntry createExcludeResources() {
+ PatternListEntry entry = new PatternListEntry();
+ excludedResources.add(entry);
+ return entry;
+ }
+
+ /**
+ * Sets attribute containing a list of patterns denoting resources
+ * to exclude from linked modules in linked image.
+ *
+ * @param patternList comma-separated list of patterns
+ *
+ * @see #createExcludeResources()
+ * @see Link.PatternListEntry
+ */
+ public void setExcludeResources(String patternList) {
+ for (String pattern : patternList.split(",")) {
+ excludedResources.add(new PatternListEntry(pattern));
+ }
+ }
+
+ /**
+ * Creates child {@code <resourceOrder} element that specifies
+ * explicit ordering of resources in linked image.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setResourceOrder(String)
+ */
+ public PatternListEntry createResourceOrder() {
+ PatternListEntry order = new PatternListEntry();
+ ordering.add(order);
+ return order;
+ }
+
+ /**
+ * Sets attribute containing a list of patterns that explicitly
+ * order resources in the linked image. Any patterns specified here
+ * will be placed before any patterns specified as
+ * {@linkplain #createResourceOrder() child elements}.
+ *
+ * @param patternList comma-separated list of patterns
+ *
+ * @see #createResourceOrder()
+ * @see Link.PatternListEntry
+ */
+ public void setResourceOrder(final String patternList) {
+ List<PatternListEntry> orderList = new ArrayList<>();
+
+ for (String pattern : patternList.split(",")) {
+ orderList.add(new PatternListEntry(pattern));
+ }
+
+ // Attribute value comes before nested elements.
+ ordering.addAll(0, orderList);
+ }
+
+ /**
+ * Attribute indicating whether linked image should pull in providers
+ * in the module path of services used by explicitly linked modules.
+ *
+ * @return true if linked will pull in service provides, false if not
+ *
+ * @see #setBindServices(boolean)
+ */
+ public boolean getBindServices() {
+ return bindServices;
+ }
+
+ /**
+ * Sets attribute indicating whether linked image should pull in providers
+ * in the module path of services used by explicitly linked modules.
+ *
+ * @param bind whether to include service providers
+ *
+ * @see #getBindServices()
+ */
+ public void setBindServices(final boolean bind) {
+ this.bindServices = bind;
+ }
+
+ /**
+ * Attribute indicating whether linker should allow modules made from
+ * signed jars.
+ *
+ * @return true if signed jars are allowed, false if modules based on
+ * signed jars cause an error
+ *
+ * @see #setIgnoreSigning(boolean)
+ */
+ public boolean getIgnoreSigning() {
+ return ignoreSigning;
+ }
+
+ /**
+ * Sets attribute indicating whether linker should allow modules made from
+ * signed jars.
+ * <p>
+ * Note: As of Java 11, this attribute is internally forced to true. See
+ * <a href="https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java#L80">the source</a>.
+ *
+ * @param ignore true to have linker allow signed jars,
+ * false to have linker emit an error for signed jars
+ *
+ *
+ * @see #getIgnoreSigning()
+ */
+ public void setIgnoreSigning(final boolean ignore) {
+ this.ignoreSigning = ignore;
+ }
+
+ /**
+ * Attribute indicating whether to include header files from linked modules
+ * in image.
+ *
+ * @return true if header files should be included, false to exclude them
+ *
+ * @see #setIncludeHeaders(boolean)
+ */
+ public boolean getIncludeHeaders() {
+ return includeHeaders;
+ }
+
+ /**
+ * Sets attribute indicating whether to include header files from
+ * linked modules in image.
+ *
+ * @param include true if header files should be included,
+ * false to exclude them
+ *
+ * @see #getIncludeHeaders()
+ */
+ public void setIncludeHeaders(final boolean include) {
+ this.includeHeaders = include;
+ }
+
+ /**
+ * Attribute indicating whether to include man pages from linked modules
+ * in image.
+ *
+ * @return true if man pages should be included, false to exclude them
+ *
+ * @see #setIncludeManPages(boolean)
+ */
+ public boolean getIncludeManPages() {
+ return includeManPages;
+ }
+
+ /**
+ * Sets attribute indicating whether to include man pages from
+ * linked modules in image.
+ *
+ * @param include true if man pages should be included,
+ * false to exclude them
+ *
+ * @see #getIncludeManPages()
+ */
+ public void setIncludeManPages(final boolean include) {
+ this.includeManPages = include;
+ }
+
+ /**
+ * Attribute indicating whether to include generated native commands,
+ * and native commands from linked modules, in image.
+ *
+ * @return true if native commands should be included, false to exclude them
+ *
+ * @see #setIncludeNativeCommands(boolean)
+ */
+ public boolean getIncludeNativeCommands() {
+ return includeNativeCommands;
+ }
+
+ /**
+ * Sets attribute indicating whether to include generated native commands,
+ * and native commands from linked modules, in image.
+ *
+ * @param include true if native commands should be included,
+ * false to exclude them
+ *
+ * @see #getIncludeNativeCommands()
+ */
+ public void setIncludeNativeCommands(final boolean include) {
+ this.includeNativeCommands = include;
+ }
+
+ /**
+ * Attribute indicating whether linker should keep or strip
+ * debug information in classes.
+ *
+ * @return true if debug information will be retained,
+ * false if it will be stripped
+ *
+ * @see #setDebug(boolean)
+ */
+ public boolean getDebug() {
+ return debug;
+ }
+
+ /**
+ * Sets attribute indicating whether linker should keep or strip
+ * debug information in classes.
+ *
+ * @param debug true if debug information should be retained,
+ * false if it should be stripped
+ *
+ * @see #getDebug()
+ */
+ public void setDebug(final boolean debug) {
+ this.debug = debug;
+ }
+
+ /**
+ * Attribute indicating whether linker should produce verbose output,
+ * and at what logging level that output should be shown.
+ *
+ * @return logging level at which to show linker's verbose output,
+ * or {@code null} to disable verbose output
+ *
+ * @see #setVerboseLevel(LogLevel)
+ */
+ public LogLevel getVerboseLevel() {
+ return verboseLevel;
+ }
+
+ /**
+ * Sets attribute indicating whether linker should produce verbose output,
+ * and at what logging level that output should be shown.
+ *
+ * @param level level logging level at which to show linker's
+ * verbose output, or {@code null} to disable verbose output
+ *
+ * @see #getVerboseLevel()
+ */
+ public void setVerboseLevel(final LogLevel level) {
+ this.verboseLevel = level;
+ }
+
+ /**
+ * Required attribute containing directory where linked image will be
+ * created.
+ *
+ * @return directory where linked image will reside
+ *
+ * @see #setDestDir(File)
+ */
+ public File getDestDir() {
+ return outputDir;
+ }
+
+ /**
+ * Sets attribute indicating directory where linked image will be created.
+ *
+ * @param dir directory in which image will be created by linker
+ *
+ * @see #getDestDir()
+ */
+ public void setDestDir(final File dir) {
+ this.outputDir = dir;
+ }
+
+ /**
+ * Attribute indicating level of compression linker will apply to image.
+ * This is exclusive with regard to {@link #createCompress()}: only one
+ * of the two may be specified.
+ *
+ * @return compression level to apply, or {@code null} for none
+ *
+ * @see #setCompress(Link.CompressionLevel)
+ * @see #createCompress()
+ */
+ public CompressionLevel getCompress() {
+ return compressionLevel;
+ }
+
+ /**
+ * Sets attribute indicating level of compression linker will apply
+ * to image. This is exclusive with regard to {@link #createCompress()}:
+ * only one of the two may be specified.
+ *
+ * @param level compression level to apply, or {@code null} for none
+ *
+ * @see #getCompress()
+ * @see #createCompress()
+ */
+ public void setCompress(final CompressionLevel level) {
+ this.compressionLevel = level;
+ }
+
+ /**
+ * Creates child {@code <compress>} element that specifies the level of
+ * compression the linker will apply, and optionally, which files in the
+ * image will be compressed. This is exclusive with regard to the
+ * {@link #setCompress compress} attribute: only one of the two may be
+ * specified.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setCompress(Link.CompressionLevel)
+ */
+ public Compression createCompress() {
+ if (compression != null) {
+ throw new BuildException(
+ "Only one nested compression element is permitted.",
+ getLocation());
+ }
+ compression = new Compression();
+ return compression;
+ }
+
+ /**
+ * Attribute which indicates whether certain files in the linked image
+ * will be big-endian or little-endian. If {@code null}, the underlying
+ * platform's endianness is used.
+ *
+ * @return endianness to apply, or {@code null} to platform default
+ *
+ * @see #setEndianness(Link.Endianness)
+ */
+ public Endianness getEndianness() {
+ return endianness;
+ }
+
+ /**
+ * Sets attribute which indicates whether certain files in the linked image
+ * will be big-endian or little-endian. If {@code null}, the underlying
+ * platform's endianness is used.
+ *
+ * @param endianness endianness to apply, or {@code null} to use
+ * platform default
+ *
+ * @see #getEndianness()
+ */
+ public void setEndianness(final Endianness endianness) {
+ this.endianness = endianness;
+ }
+
+ /**
+ * Attribute indicating whether linker should check legal notices with
+ * duplicate names, and refuse to merge them (usually using symbolic links)
+ * if their respective content is not identical.
+ *
+ * @return true if legal notice files with same name should be checked
+ * for identical content, false to suppress check
+ *
+ * @see #setCheckDuplicateLegal(boolean)
+ */
+ public boolean getCheckDuplicateLegal() {
+ return checkDuplicateLegal;
+ }
+
+ /**
+ * Sets attribute indicating whether linker should check legal notices with
+ * duplicate names, and refuse to merge them (usually using symbolic links)
+ * if their respective content is not identical.
+ *
+ * @param check true if legal notice files with same name should be checked
+ * for identical content, false to suppress check
+ *
+ * @see #getCheckDuplicateLegal()
+ */
+ public void setCheckDuplicateLegal(final boolean check) {
+ this.checkDuplicateLegal = check;
+ }
+
+ /**
+ * Attribute indicating what type of JVM the linked image should have.
+ * If {@code null}, all JVM types are included.
+ *
+ * @return type of JVM linked image will have
+ *
+ * @see #setVmType(Link.VMType)
+ */
+ public VMType getVmType() {
+ return vmType;
+ }
+
+ /**
+ * Set attribute indicating what type of JVM the linked image should have.
+ * If {@code null}, all JVM types are included.
+ *
+ * @param type type of JVM linked image will have
+ *
+ * @see #getVmType()
+ */
+ public void setVmType(final VMType type) {
+ this.vmType = type;
+ }
+
+ /**
+ * Creates child {@code <releaseInfo>} element that modifies the default
+ * release properties of the linked image.
+ *
+ * @return new, unconfigured child element
+ */
+ public ReleaseInfo createReleaseInfo() {
+ ReleaseInfo info = new ReleaseInfo();
+ releaseInfo.add(info);
+ return info;
+ }
+
+ /**
+ * Child element that explicitly names a Java module.
+ */
+ public class ModuleSpec {
+ /** Module's name. Required. */
+ private String name;
+
+ /** Creates an unconfigured element. */
+ public ModuleSpec() {
+ // Deliberately empty.
+ }
+
+ /**
+ * Creates an element with the given module name.
+ *
+ * @param name module's name
+ */
+ public ModuleSpec(final String name) {
+ setName(name);
+ }
+
+ /**
+ * Attribute containing name of module this element represents.
+ *
+ * @return name of module
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets attribute representing the name of this module this element
+ * represents.
+ *
+ * @param name module's name
+ */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Verifies this element's state.
+ *
+ * @throws BuildException if name is not set
+ */
+ public void validate() {
+ if (name == null) {
+ throw new BuildException("name is required for module.",
+ getLocation());
+ }
+ }
+ }
+
+ /**
+ * Child element that contains a pattern matching Java locales.
+ */
+ public class LocaleSpec {
+ /** Pattern of locale names to match. */
+ private String name;
+
+ /** Creates an unconfigured element. */
+ public LocaleSpec() {
+ // Deliberately empty.
+ }
+
+ /**
+ * Creates an element with the given name pattern.
+ *
+ * @param name pattern of locale names to match
+ */
+ public LocaleSpec(final String name) {
+ setName(name);
+ }
+
+ /**
+ * Attribute containing a pattern which matches Java locale names.
+ * May be an explicit Java locale, or may contain an asterisk
+ * ({@code *)} for wildcard matching.
+ *
+ * @return this element's locale name pattern
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets attribute containing a pattern which matches Java locale names.
+ * May be an explicit Java locale, or may contain an asterisk
+ * ({@code *)} for wildcard matching.
+ *
+ * @param name new locale name or pattern matching locale names
+ */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Verifies this element's state.
+ *
+ * @throws BuildException if name is not set
+ */
+ public void validate() {
+ if (name == null) {
+ throw new BuildException("name is required for locale.",
+ getLocation());
+ }
+ }
+ }
+
+ /**
+ * Child element type which specifies a jlink files pattern. Each
+ * instance may specify a string
+ * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29">PathMatcher pattern</a>
+ * or a text file containing a list of such patterns, one per line.
+ */
+ public class PatternListEntry {
+ /** PathMatcher pattern of files to match. */
+ private String pattern;
+
+ /** Plain text list file with one PathMatcher pattern per line. */
+ private File file;
+
+ /** Creates an unconfigured element. */
+ public PatternListEntry() {
+ // Deliberately empty.
+ }
+
+ /**
+ * Creates a new element from either a pattern or listing file.
+ * If the argument starts with "{@code @}", the remainder of it
+ * is assumed to be a listing file; otherwise, it is treated as
+ * a PathMatcher pattern.
+ *
+ * @param pattern a PathMatcher pattern or {@code @}-filename
+ */
+ public PatternListEntry(final String pattern) {
+ if (pattern.startsWith("@")) {
+ setListFile(new File(pattern.substring(1)));
+ } else {
+ setPattern(pattern);
+ }
+ }
+
+ /**
+ * Returns this element's PathMatcher pattern attribute, if set.
+ *
+ * @return this element's files pattern
+ */
+ public String getPattern() {
+ return pattern;
+ }
+
+ /**
+ * Sets this element's
+ * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29">PathMatcher pattern</a>
+ * attribute for matching files.
+ *
+ * @param pattern new files pattern
+ */
+ public void setPattern(final String pattern) {
+ this.pattern = pattern;
+ }
+
+ /**
+ * Returns this element's list file attribute, if set.
+ *
+ * @return this element's list file
+ *
+ * @see #setListFile(File)
+ */
+ public File getListFile() {
+ return file;
+ }
+
+ /**
+ * Sets this element's list file attribute. The file must be a
+ * plain text file with one PathMatcher pattern per line.
+ *
+ * @param file list file containing patterns
+ *
+ * @see #getListFile()
+ */
+ public void setListFile(final File file) {
+ this.file = file;
+ }
+
+ /**
+ * Verifies this element's state.
+ *
+ * @throws BuildException if both pattern and file are set
+ * @throws BuildException if neither pattern nor file is set
+ */
+ public void validate() {
+ if ((pattern == null && file == null)
+ || (pattern != null && file != null)) {
+
+ throw new BuildException(
+ "Each entry in a pattern list must specify "
+ + "exactly one of pattern or file.", getLocation());
+ }
+ }
+
+ /**
+ * Converts this element to a jlink command line attribute,
+ * either this element's bare pattern, or its list file
+ * preceded by "{@code @}".
+ *
+ * @return this element's information converted to a command line value
+ */
+ public String toOptionValue() {
+ return pattern != null ? pattern : ("@" + file);
+ }
+ }
+
+ /**
+ * Child element representing a custom launcher command in a linked image.
+ * A launcher has a name, which is typically used as a file name for an
+ * executable file, a Java module name, and optionally a class within
+ * that module which can act as a standard Java main class.
+ */
+ public class Launcher {
+ /** This launcher's name, usually used to create an executable file. */
+ private String name;
+
+ /** The name of the Java module this launcher launches. */
+ private String module;
+
+ /**
+ * The class within this element's {@link #module} to run.
+ * Optional if the Java module specifies its own main class.
+ */
+ private String mainClass;
+
+ /** Creates a new, unconfigured element. */
+ public Launcher() {
+ // Deliberately empty.
+ }
+
+ /**
+ * Creates a new element from a {@code jlink}-compatible string
+ * specifier, which must take the form
+ * <var>name</var>{@code =}<var>module</var> or
+ * <var>name</var>{@code =}<var>module</var>{@code /}<var>mainclass</var>.
+ *
+ * @param textSpec name, module, and optional main class, as described
+ * above
+ *
+ * @throws NullPointerException if argument is {@code null}
+ * @throws BuildException if argument does not conform to above
+ * requirements
+ */
+ public Launcher(final String textSpec) {
+ Objects.requireNonNull(textSpec, "Text cannot be null");
+
+ int equals = textSpec.lastIndexOf('=');
+ if (equals < 1) {
+ throw new BuildException(INVALID_LAUNCHER_STRING);
+ }
+
+ setName(textSpec.substring(0, equals));
+
+ int slash = textSpec.indexOf('/', equals);
+ if (slash < 0) {
+ setModule(textSpec.substring(equals + 1));
+ } else if (slash > equals + 1 && slash < textSpec.length() - 1) {
+ setModule(textSpec.substring(equals + 1, slash));
+ setMainClass(textSpec.substring(slash + 1));
+ } else {
+ throw new BuildException(INVALID_LAUNCHER_STRING);
+ }
+ }
+
+ /**
+ * Returns this element's name attribute, typically used as the basis
+ * of an executable file name.
+ *
+ * @return this element's name
+ *
+ * @see #setName(String)
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets this element's name attribute, which is typically used by the
+ * linker to create an executable file with a similar name. Thus,
+ * the name should contain only characters safe for file names.
+ *
+ * @param name name of launcher
+ */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the attribute of this element which contains the
+ * name of the Java module to execute.
+ *
+ * @return this element's module name
+ */
+ public String getModule() {
+ return module;
+ }
+
+ /**
+ * Sets the attribute of this element which contains the name of
+ * a Java module to execute.
+ *
+ * @param module name of module to execute
+ */
+ public void setModule(final String module) {
+ this.module = module;
+ }
+
+ /**
+ * Returns the attribute of this element which contains the main class
+ * to execute in this element's {@linkplain #getModule() module}, if
+ * that module doesn't define its main class.
+ *
+ * @return name of main class to execute
+ */
+ public String getMainClass() {
+ return mainClass;
+ }
+
+ /**
+ * Sets the attribute which contains the main class to execute in
+ * this element's {@linkplain #getModule() module}, if that module
+ * doesn't define its main class.
+ *
+ * @param className name of class to execute
+ */
+ public void setMainClass(final String className) {
+ this.mainClass = className;
+ }
+
+ /**
+ * Verifies this element's state.
+ *
+ * @throws BuildException if name or module is not set
+ */
+ public void validate() {
+ if (name == null || name.isEmpty()) {
+ throw new BuildException("Launcher must have a name",
+ getLocation());
+ }
+ if (module == null || module.isEmpty()) {
+ throw new BuildException("Launcher must have specify a module",
+ getLocation());
+ }
+ }
+
+ /**
+ * Returns this element's information in jlink launcher format:
+ * <var>name</var>{@code =}<var>module</var> or
+ * <var>name</var>{@code =}<var>module</var>{@code /}<var>mainclass</var>.
+ *
+ * @return name, module and optional main class in jlink format
+ */
+ @Override
+ public String toString() {
+ if (mainClass != null) {
+ return name + "=" + module + "/" + mainClass;
+ } else {
+ return name + "=" + module;
+ }
+ }
+ }
+
+ /**
+ * Possible values for linked image endianness:
+ * {@code little} and {@code big}.
+ */
+ public static class Endianness
+ extends EnumeratedAttribute {
+ @Override
+ public String[] getValues() {
+ return new String[] {
+ "little", "big"
+ };
+ }
+ }
+
+ /**
+ * Possible values for JVM type in linked image:
+ * {@code client}, {@code server}, {@code minimal}, or {@code all}.
+ */
+ public static class VMType
+ extends EnumeratedAttribute {
+ @Override
+ public String[] getValues() {
+ return new String[] {
+ "client", "server", "minimal", "all"
+ };
+ }
+ }
+
+ /**
+ * Possible attribute values for compression level of a linked image:
+ * <dl>
+ * <dt>{@code 0}
+ * <dt>{@code none}
+ * <dd>no compression (default)
+ * <dt>{@code 1}
+ * <dt>{@code strings}
+ * <dd>constant string sharing
+ * <dt>{@code 2}
+ * <dt>{@code zip}
+ * <dd>zip compression
+ * </dl>
+ */
+ public static class CompressionLevel
+ extends EnumeratedAttribute {
+ private static final Map<String, String> KEYWORDS;
+
+ static {
+ Map<String, String> map = new LinkedHashMap<>();
+ map.put("0", "0");
+ map.put("1", "1");
+ map.put("2", "2");
+ map.put("none", "0");
+ map.put("strings", "1");
+ map.put("zip", "2");
+
+ KEYWORDS = Collections.unmodifiableMap(map);
+ }
+
+ @Override
+ public String[] getValues() {
+ return KEYWORDS.keySet().toArray(new String[0]);
+ }
+
+ /**
+ * Converts this value to a string suitable for use in a
+ * jlink command.
+ *
+ * @return jlink keyword corresponding to this value
+ */
+ String toCommandLineOption() {
+ return KEYWORDS.get(getValue());
+ }
+ }
+
+ /**
+ * Child element fully describing compression of a linked image.
+ * This includes the level, and optionally, the names of files to compress.
+ */
+ public class Compression {
+ /** Compression level. Required attribute. */
+ private CompressionLevel level;
+
+ /**
+ * Patterns specifying files to compress. If empty, all files are
+ * compressed.
+ */
+ private final List<PatternListEntry> patterns = new ArrayList<>();
+
+ /**
+ * Required attribute containing level of compression.
+ *
+ * @return compression level
+ */
+ public CompressionLevel getLevel() {
+ return level;
+ }
+
+ /**
+ * Sets attribute indicating level of compression.
+ *
+ * @param level type of compression to apply to linked image
+ */
+ public void setLevel(final CompressionLevel level) {
+ this.level = level;
+ }
+
+ /**
+ * Creates a nested element which can specify a pattern of files
+ * to compress.
+ *
+ * @return new, unconfigured child element
+ */
+ public PatternListEntry createFiles() {
+ PatternListEntry pattern = new PatternListEntry();
+ patterns.add(pattern);
+ return pattern;
+ }
+
+ /**
+ * Sets an attribute that represents a list of file patterns to
+ * compress in the linked image, as a comma-separated list of
+ * PathMatcher patterns or pattern list files.
+ *
+ * @param patternList comma-separated list of patterns and/or file names
+ *
+ * @see Link.PatternListEntry
+ */
+ public void setFiles(final String patternList) {
+ patterns.clear();
+ for (String pattern : patternList.split(",")) {
+ patterns.add(new PatternListEntry(pattern));
+ }
+ }
+
+ /**
+ * Verifies this element's state.
+ *
+ * @throws BuildException if compression level is not set
+ * @throws BuildException if any nested patterns are invalid
+ */
+ public void validate() {
+ if (level == null) {
+ throw new BuildException("Compression level must be specified.",
+ getLocation());
+ }
+ patterns.forEach(PatternListEntry::validate);
+ }
+
+ /**
+ * Converts this element to a single jlink option value.
+ *
+ * @return command line option representing this element's state
+ */
+ public String toCommandLineOption() {
+ StringBuilder option =
+ new StringBuilder(level.toCommandLineOption());
+
+ if (!patterns.isEmpty()) {
+ String separator = ":filter=";
+ for (PatternListEntry entry : patterns) {
+ option.append(separator).append(entry.toOptionValue());
+ separator = ",";
+ }
+ }
+
+ return option.toString();
+ }
+ }
+
+ /**
+ * Grandchild element representing deletable key in a linked image's
+ * release properties.
+ */
+ public class ReleaseInfoKey {
+ /** Required attribute holding property key to delete. */
+ private String key;
+
+ /** Creates a new, unconfigured element. */
+ public ReleaseInfoKey() {
+ // Deliberately empty.
+ }
+
+ /**
+ * Creates a new element with the specified key.
+ *
+ * @param key property key to delete from release info
+ */
+ public ReleaseInfoKey(final String key) {
+ setKey(key);
+ }
+
+ /**
+ * Attribute holding the release info property key to delete.
+ *
+ * @return property key to be deleted
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Sets attribute containing property key to delete from
+ * linked image's release info.
+ *
+ * @param key propert key to be deleted
+ */
+ public void setKey(final String key) {
+ this.key = key;
+ }
+
+ /**
+ * Verifies this element's state is valid.
+ *
+ * @throws BuildException if key is not set
+ */
+ public void validate() {
+ if (key == null) {
+ throw new BuildException(
+ "Release info key must define a 'key' attribute.",
+ getLocation());
+ }
+ }
+ }
+
+ /**
+ * Grandchild element describing additional release info properties for a
+ * linked image. To be valid, an instance must have either a non-null
+ * key and value, or a non-null file.
+ */
+ public class ReleaseInfoEntry {
+ /** New release property's key. */
+ private String key;
+
+ /** New release property's value. */
+ private String value;
+
+ /** File containing additional release properties. */
+ private File file;
+
+ /** Charset of {@link #file}. */
+ private String charset = StandardCharsets.ISO_8859_1.name();
+
+ /** Creates a new, unconfigured element. */
+ public ReleaseInfoEntry() {
+ // Deliberately empty.
+ }
+
+ /**
+ * Creates a new element which specifies a single additional property.
+ *
+ * @param key new property's key
+ * @param value new property's value
+ */
+ public ReleaseInfoEntry(final String key,
+ final String value) {
+ setKey(key);
+ setValue(value);
+ }
+
+ /**
+ * Attribute containing the key of this element's additional property.
+ *
+ * @return additional property's key
+ *
+ * @see #getValue()
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Sets attribute containing the key of this element's
+ * additional property.
+ *
+ * @param key additional property's key
+ *
+ * @see #setValue(String)
+ */
+ public void setKey(final String key) {
+ this.key = key;
+ }
+
+ /**
+ * Attribute containing the value of this element's additional property.
+ *
+ * @return additional property's value
+ *
+ * @see #getKey()
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Sets attributes containing the value of this element's
+ * additional property.
+ *
+ * @param value additional property's value
+ *
+ * @see #setKey(String)
+ */
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ /**
+ * Attribute containing a Java properties file which contains
+ * additional release info properties. This is exclusive with
+ * respect to the {@linkplain #getKey() key} and
+ * {@linkplain #getValue() value} of this instance: either the
+ * file must be set, or the key and value must be set.
+ *
+ * @return this element's properties file
+ */
+ public File getFile() {
+ return file;
+ }
+
+ /**
+ * Sets attribute containing a Java properties file which contains
+ * additional release info properties. This is exclusive with
+ * respect to the {@linkplain #setKey(String) key} and
+ * {@linkplain #setValue(String) value} of this instance: either the
+ * file must be set, or the key and value must be set.
+ *
+ * @param file this element's properties file
+ */
+ public void setFile(final File file) {
+ this.file = file;
+ }
+
+ /**
+ * Attribute containing the character set of this object's
+ * {@linkplain #getFile() file}. This is {@code ISO_8859_1}
+ * by default, in accordance with the java.util.Properties default.
+ *
+ * @return character set of this element's file
+ */
+ public String getCharset() {
+ return charset;
+ }
+
+ /**
+ * Sets attribute containing the character set of this object's
+ * {@linkplain #setFile(File) file}. If not set, this is
+ * {@code ISO_8859_1} by default, in accordance with the
+ * java.util.Properties default.
+ *
+ * @param charset character set of this element's file
+ */
+ public void setCharset(final String charset) {
+ this.charset = charset;
+ }
+
+ /**
+ * Verifies the state of this element.
+ *
+ * @throws BuildException if file is set, and key and/or value are set
+ * @throws BuildException if file is not set, and key and value are not both set
+ * @throws BuildException if charset is not a valid Java Charset name
+ */
+ public void validate() {
+ if (file == null && (key == null || value == null)) {
+ throw new BuildException(
+ "Release info must define 'key' and 'value' attributes, "
+ + "or a 'file' attribute.", getLocation());
+ }
+ if (file != null && (key != null || value != null)) {
+ throw new BuildException(
+ "Release info cannot define both a file attribute and "
+ + "key/value attributes.", getLocation());
+ }
+
+ // This can't happen from a build file, but can theoretically
+ // happen if called from Java code.
+ if (charset == null) {
+ throw new BuildException("Charset cannot be null.",
+ getLocation());
+ }
+
+ try {
+ Charset.forName(charset);
+ } catch (IllegalArgumentException e) {
+ throw new BuildException(e, getLocation());
+ }
+ }
+
+ /**
+ * Converts this element to a Java properties object containing
+ * the additional properties this element represents. If this
+ * element's file is set, it is read; otherwise, a Properties
+ * object containing just one property, consisting of this element's
+ * key and value, is returned.
+ *
+ * @return new Properties object obtained from this element's file or
+ * its key and value
+ *
+ * @throws BuildException if file is set, but cannot be read
+ */
+ public Properties toProperties() {
+ Properties props = new Properties();
+ if (file != null) {
+ try (Reader reader = Files.newBufferedReader(
+ file.toPath(), Charset.forName(charset))) {
+
+ props.load(reader);
+ } catch (IOException e) {
+ throw new BuildException(
+ "Cannot read release info file \"" + file + "\": " + e,
+ e, getLocation());
+ }
+ } else {
+ props.setProperty(key, value);
+ }
+
+ return props;
+ }
+ }
+
+ /**
+ * Child element describing changes to the default release properties
+ * of a linked image.
+ */
+ public class ReleaseInfo {
+ /**
+ * File that contains replacement release properties for linked image.
+ */
+ private File file;
+
+ /**
+ * Properties to add to default release properties of linked image.
+ */
+ private final List<ReleaseInfoEntry> propertiesToAdd = new ArrayList<>();
+
+ /**
+ * Property keys to remove from release properties of linked image.
+ */
+ private final List<ReleaseInfoKey> propertiesToDelete = new ArrayList<>();
+
+ /**
+ * Attribute specifying Java properties file which will replace the
+ * default release info properties for the linked image.
+ *
+ * @return release properties file
+ */
+ public File getFile() {
+ return file;
+ }
+
+ /**
+ * Sets attribute specifying Java properties file which will replace
+ * the default release info properties for the linked image.
+ *
+ * @param file replacement release properties file
+ */
+ public void setFile(final File file) {
+ this.file = file;
+ }
+
+ /**
+ * Creates an uninitialized child element which can represent properties
+ * to add to the default release properties of a linked image.
+ *
+ * @return new, unconfigured child element
+ */
+ public ReleaseInfoEntry createAdd() {
+ ReleaseInfoEntry property = new ReleaseInfoEntry();
+ propertiesToAdd.add(property);
+ return property;
+ }
+
+ /**
+ * Creates an uninitialized child element which can represent
+ * a property key to delete from the release properties of
+ * a linked image.
+ *
+ * @return new, unconfigured child element
+ */
+ public ReleaseInfoKey createDelete() {
+ ReleaseInfoKey key = new ReleaseInfoKey();
+ propertiesToDelete.add(key);
+ return key;
+ }
+
+ /**
+ * Sets attribute which contains a comma-separated list of
+ * property keys to delete from the release properties of
+ * a linked image.
+ *
+ * @param keyList comma-separated list of property keys
+ *
+ * @see #createDelete()
+ */
+ public void setDelete(final String keyList) {
+ for (String key : keyList.split(",")) {
+ propertiesToDelete.add(new ReleaseInfoKey(key));
+ }
+ }
+
+ /**
+ * Verifies the state of this element.
+ *
+ * @throws BuildException if any child element is invalid
+ *
+ * @see Link.ReleaseInfoEntry#validate()
+ * @see Link.ReleaseInfoKey#validate()
+ */
+ public void validate() {
+ propertiesToAdd.forEach(ReleaseInfoEntry::validate);
+ propertiesToDelete.forEach(ReleaseInfoKey::validate);
+ }
+
+ /**
+ * Converts all of this element's state to a series of
+ * <code>jlink</code> options.
+ *
+ * @return new collection of jlink options based on this element's
+ * attributes and child elements
+ */
+ public Collection<String> toCommandLineOptions() {
+ Collection<String> options = new ArrayList<>();
+
+ if (file != null) {
+ options.add("--release-info=" + file);
+ }
+ if (!propertiesToAdd.isEmpty()) {
+ StringBuilder option = new StringBuilder("--release-info=add");
+
+ for (ReleaseInfoEntry entry : propertiesToAdd) {
+ Properties props = entry.toProperties();
+ for (String key : props.stringPropertyNames()) {
+ option.append(":").append(key).append("=");
+ option.append(props.getProperty(key));
+ }
+ }
+
+ options.add(option.toString());
+ }
+ if (!propertiesToDelete.isEmpty()) {
+ StringBuilder option =
+ new StringBuilder("--release-info=del:keys=");
+
+ String separator = "";
+ for (ReleaseInfoKey key : propertiesToDelete) {
+ option.append(separator).append(key.getKey());
+ // jlink docs aren't clear on whether property keys
+ // to delete should be separated by commas or colons.
+ separator = ",";
+ }
+
+ options.add(option.toString());
+ }
+
+ return options;
+ }
+ }
+
+ /**
+ * Invokes the jlink tool to create a new linked image, unless the
+ * output directory exists and all of its files are files are newer
+ * than all files in the module path.
+ *
+ * @throws BuildException if destDir is not set
+ * @throws BuildException if module path is unset or empty
+ * @throws BuildException if module list is empty
+ * @throws BuildException if compressionLevel attribute and compression
+ * child element are both specified
+ */
+ @Override
+ public void execute()
+ throws BuildException {
+ if (outputDir == null) {
+ throw new BuildException("Destination directory is required.",
+ getLocation());
+ }
+
+ if (modulePath == null || modulePath.isEmpty()) {
+ throw new BuildException("Module path is required.", getLocation());
+ }
+
+ if (modules.isEmpty()) {
+ throw new BuildException("At least one module must be specified.",
+ getLocation());
+ }
+
+ if (outputDir.exists()) {
+ CompositeMapper imageMapper = new CompositeMapper();
+ try (Stream<java.nio.file.Path> imageTree =
+ Files.walk(outputDir.toPath())) {
+
+ /*
+ * Is this sufficient? What if part of the image tree was
+ * deleted or altered? Should we check for standard
+ * files and directories, like 'bin', 'lib', 'conf', 'legal',
+ * and 'release'? (Some, like 'include', may not be present,
+ * if the image was previously built with options that
+ * omitted them.)
+ */
+ imageTree.forEach(
+ p -> imageMapper.add(new MergingMapper(p.toString())));
+
+ ResourceCollection outOfDate =
+ ResourceUtils.selectOutOfDateSources(this, modulePath,
+ imageMapper, getProject(),
+ FileUtils.getFileUtils().getFileTimestampGranularity());
+ if (outOfDate.isEmpty()) {
+ log("Skipping image creation, since "
+ + "\"" + outputDir + "\" is already newer than "
+ + "all constituent modules.", Project.MSG_VERBOSE);
+ return;
+ }
+ } catch (IOException e) {
+ throw new BuildException(
+ "Could not scan \"" + outputDir + "\" "
+ + "for being up-to-date: " + e, e, getLocation());
+ }
+ }
+
+ modules.forEach(ModuleSpec::validate);
+ observableModules.forEach(ModuleSpec::validate);
+ launchers.forEach(Launcher::validate);
+ locales.forEach(LocaleSpec::validate);
+ ordering.forEach(PatternListEntry::validate);
+ excludedFiles.forEach(PatternListEntry::validate);
+ excludedResources.forEach(PatternListEntry::validate);
+
+ Collection<String> args = buildJlinkArgs();
+
+ ToolProvider jlink = ToolProvider.findFirst("jlink").orElseThrow(
+ () -> new BuildException("jlink tool not found in JDK.",
+ getLocation()));
+
+ if (outputDir.exists()) {
+ log("Deleting existing " + outputDir, Project.MSG_VERBOSE);
+ deleteTree(outputDir.toPath());
+ }
+
+ log("Executing: jlink " + String.join(" ", args), Project.MSG_VERBOSE);
+
+ ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+ int exitCode;
+ try (PrintStream out = new PrintStream(stdout);
+ PrintStream err = new PrintStream(stderr)) {
+
+ exitCode = jlink.run(out, err, args.toArray(new String[0]));
+ }
+
+ if (exitCode != 0) {
+ StringBuilder message = new StringBuilder();
+ message.append("jlink failed (exit code ").append(exitCode).append(")");
+ if (stdout.size() > 0) {
+ message.append(", output is: ").append(stdout);
+ }
+ if (stderr.size() > 0) {
+ message.append(", error output is: ").append(stderr);
+ }
+
+ throw new BuildException(message.toString(), getLocation());
+ }
+
+ if (verboseLevel != null) {
+ int level = verboseLevel.getLevel();
+
+ if (stdout.size() > 0) {
+ log(stdout.toString(), level);
+ }
+ if (stderr.size() > 0) {
+ log(stderr.toString(), level);
+ }
+ }
+
+ log("Created " + outputDir.getAbsolutePath(), Project.MSG_INFO);
+ }
+
+ /**
+ * Recursively deletes a file tree.
+ *
+ * @param dir root of tree to delete
+ *
+ * @throws BuildException if deletion fails
+ */
+ private void deleteTree(java.nio.file.Path dir) {
+ try {
+ Files.walkFileTree(dir, new SimpleFileVisitor<java.nio.file.Path>() {
+ @Override
+ public FileVisitResult visitFile(final java.nio.file.Path file,
+ final BasicFileAttributes attr)
+ throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(final java.nio.file.Path dir,
+ IOException e)
+ throws IOException {
+ if (e == null) {
+ Files.delete(dir);
+ }
+ return super.postVisitDirectory(dir, e);
+ }
+ });
+ } catch (IOException e) {
+ throw new BuildException(
+ "Could not delete \"" + dir + "\": " + e, e, getLocation());
+ }
+ }
+
+ /**
+ * Creates list of arguments to <code>jlink</code> tool, based on this
+ * instance's current state.
+ *
+ * @return new list of <code>jlink</code> arguments
+ *
+ * @throws BuildException if any inconsistencies attributes/elements
+ * is found
+ */
+ private Collection<String> buildJlinkArgs() {
+ Collection<String> args = new ArrayList<>();
+
+ args.add("--output");
+ args.add(outputDir.toString());
+
+ args.add("--module-path");
+ args.add(modulePath.toString());
+
+ args.add("--add-modules");
+ args.add(modules.stream().map(ModuleSpec::getName).collect(
+ Collectors.joining(",")));
+
+ if (!observableModules.isEmpty()) {
+ args.add("--limit-modules");
+ args.add(observableModules.stream().map(ModuleSpec::getName).collect(
+ Collectors.joining(",")));
+ }
+
+ if (!locales.isEmpty()) {
+ args.add("--include-locales="
+ + locales.stream().map(LocaleSpec::getName).collect(
+ Collectors.joining(",")));
+ }
+
+ for (Launcher launcher : launchers) {
+ args.add("--launcher");
+ args.add(launcher.toString());
+ }
+
+ if (!ordering.isEmpty()) {
+ args.add("--order-resources="
+ + ordering.stream().map(PatternListEntry::toOptionValue).collect(
+ Collectors.joining(",")));
+ }
+ if (!excludedFiles.isEmpty()) {
+ args.add("--exclude-files="
+ + excludedFiles.stream().map(PatternListEntry::toOptionValue).collect(
+ Collectors.joining(",")));
+ }
+ if (!excludedResources.isEmpty()) {
+ args.add("--exclude-resources="
+ + excludedResources.stream().map(PatternListEntry::toOptionValue).collect(
+ Collectors.joining(",")));
+ }
+
+ if (bindServices) {
+ args.add("--bind-services");
+ }
+ if (ignoreSigning) {
+ args.add("--ignore-signing-information");
+ }
+ if (!includeHeaders) {
+ args.add("--no-header-files");
+ }
+ if (!includeManPages) {
+ args.add("--no-man-pages");
+ }
+ if (!includeNativeCommands) {
+ args.add("--strip-native-commands");
+ }
+ if (!debug) {
+ args.add("--strip-debug");
+ }
+ if (verboseLevel != null) {
+ args.add("--verbose");
+ }
+
+ if (endianness != null) {
+ args.add("--endian");
+ args.add(endianness.getValue());
+ }
+
+ if (compressionLevel != null) {
+ if (compression != null) {
+ throw new BuildException("compressionLevel attribute "
+ + "and <compression> child element cannot both be present.",
+ getLocation());
+ }
+ args.add("--compress=" + compressionLevel.toCommandLineOption());
+ }
+ if (compression != null) {
+ compression.validate();
+ args.add("--compress=" + compression.toCommandLineOption());
+ }
+ if (vmType != null) {
+ args.add("--vm=" + vmType.getValue());
+ }
+ if (checkDuplicateLegal) {
+ args.add("--dedup-legal-notices=error-if-not-same-content");
+ }
+ for (ReleaseInfo info : releaseInfo) {
+ info.validate();
+ args.addAll(info.toCommandLineOptions());
+ }
+
+ return args;
+ }
+}
http://git-wip-us.apache.org/repos/asf/ant/blob/343dff90/src/main/org/apache/tools/ant/taskdefs/modules/package-info.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/modules/package-info.java b/src/main/org/apache/tools/ant/taskdefs/modules/package-info.java
new file mode 100644
index 0000000..99b11e5
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/modules/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ *
+ */
+
+/**
+ * Tasks for dealing with Java modules, which are supported starting with
+ * Java 9.
+ */
+package org.apache.tools.ant.taskdefs.modules;
http://git-wip-us.apache.org/repos/asf/ant/blob/343dff90/src/main/org/apache/tools/ant/taskdefs/optional/jlink/JlinkTask.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/jlink/JlinkTask.java b/src/main/org/apache/tools/ant/taskdefs/optional/jlink/JlinkTask.java
index db8b3a3..be27284 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/jlink/JlinkTask.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/jlink/JlinkTask.java
@@ -25,8 +25,10 @@ import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.Path;
/**
- * This class defines objects that can link together various jar and
- * zip files.
+ * This task defines objects that can link together various jar and
+ * zip files. It is not related to the {@code jlink} tool present in
+ * Java 9 and later; for that, see
+ * {@link org.apache.tools.ant.taskdefs.modules.Link}.
*
* <p>It is basically a wrapper for the jlink code written originally
* by <a href="mailto:beard@netscape.com">Patrick Beard</a>. The
http://git-wip-us.apache.org/repos/asf/ant/blob/343dff90/src/main/org/apache/tools/ant/types/ModuleVersion.java
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/types/ModuleVersion.java b/src/main/org/apache/tools/ant/types/ModuleVersion.java
new file mode 100644
index 0000000..0a43420
--- /dev/null
+++ b/src/main/org/apache/tools/ant/types/ModuleVersion.java
@@ -0,0 +1,147 @@
+/*
+ * 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.tools.ant.types;
+
+import java.util.Objects;
+
+/**
+ * Element describing the parts of a Java
+ * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">module version</a>.
+ * The version number is required; all other parts are optional.
+ */
+public class ModuleVersion {
+ /** Module version's required <em>version number</em>. */
+ private String number;
+
+ /** Module version's optional <em>pre-release version</em>. */
+ private String preRelease;
+
+ /** Module version's optional <em>build version</em>. */
+ private String build;
+
+ /**
+ * Returns this element's version number.
+ *
+ * @return version number
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Sets this element's required version number. This cannot contain
+ * an ASCII hyphen ({@code -}) or plus ({@code +}), as those characters
+ * are used as delimiters in a complete module version string.
+ *
+ * @throws NullPointerException if argument is {@code null}
+ * @throws IllegalArgumentException if argument contains {@code '-'}
+ * or {@code '+'}
+ */
+ public void setNumber(final String number) {
+ Objects.requireNonNull(number, "Version number cannot be null.");
+ if (number.indexOf('-') >= 0 || number.indexOf('+') >= 0) {
+ throw new IllegalArgumentException(
+ "Version number cannot contain '-' or '+'.");
+ }
+ this.number = number;
+ }
+
+ /**
+ * Returns this element's pre-release version, if set.
+ *
+ * @return pre-release value, or {@code null}
+ */
+ public String getPreRelease() {
+ return preRelease;
+ }
+
+ /**
+ * Sets this element's pre-release version. This can be any value
+ * which doesn't contain an ASCII plus ({@code +}).
+ *
+ * @param pre pre-release version, or {@code null}
+ *
+ * @throws IllegalArgumentException if argument contains "{@code +}"
+ */
+ public void setPreRelease(final String pre) {
+ if (pre != null && pre.indexOf('+') >= 0) {
+ throw new IllegalArgumentException(
+ "Version's pre-release cannot contain '+'.");
+ }
+ this.preRelease = pre;
+ }
+
+ /**
+ * Returns this element's build version, if set.
+ *
+ * @return build value, or {@code null}
+ */
+ public String getBuild() {
+ return build;
+ }
+
+ /**
+ * Sets this element's build version. This can be any value, including
+ * {@code null}.
+ *
+ * @param build build version, or {@code null}
+ */
+ public void setBuild(final String build) {
+ this.build = build;
+ }
+
+ /**
+ * Snapshots this element's state and converts it to a string compliant
+ * with {@code ModuleDescriptor.Version}.
+ *
+ * @return Java module version string built from this object's properties
+ *
+ * @throws IllegalStateException if {@linkplain #getNumber() number}
+ * is {@code null}
+ */
+ public String toModuleVersionString() {
+ if (number == null) {
+ throw new IllegalStateException("Version number cannot be null.");
+ }
+
+ StringBuilder version = new StringBuilder(number);
+ if (preRelease != null || build != null) {
+ version.append('-').append(Objects.toString(preRelease, ""));
+ }
+ if (build != null) {
+ version.append('+').append(build);
+ }
+
+ return version.toString();
+ }
+
+ /**
+ * Returns a summary of this object's state, suitable for debugging.
+ *
+ * @return string form of this instance
+ */
+ @Override
+ public String toString() {
+ return getClass().getName() +
+ "[number=" + number +
+ ", preRelease=" + preRelease +
+ ", build=" + build +
+ "]";
+ }
+}