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:[email protected]">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 + + "]"; + } +}
