blob: b25d4c2954a34e5a39501819b854274476af2195 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://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>
*
* @since 1.10.6
*/
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;
}
}