For a task based on the JDK's jlink tool, see
+Link. This task is for something else
+entirely.
+
Links entries from sub-builds and libraries.
The jlink task can be used to build jar and zip files, similar to
diff --git a/manual/Tasks/jmod.html b/manual/Tasks/jmod.html
new file mode 100644
index 000000000..0540918b0
--- /dev/null
+++ b/manual/Tasks/jmod.html
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+Jmod Task
+
+
+
+
+
Jmod
+
Description
+
Creates a linkable jmod file from a modular jar file, and optionally from
+other application files such as native libraries and license documents.
+Equivalent to the JDK's
+jmod
+tool.
+
Requires Java 9 or later.
+
+
Parameters
+
+
+
Attribute
+
Description
+
Required
+
+
+
destFile
+
jmod file to create.
+
Yes
+
+
+
classpath
+
Files to be placed in the jmod file. Usually a single module.
+
One of these is required, unless a nested
+ <classpath> is present.
+
+
+
classpathref
+
Files to be placed in the jmod file, given as a
+ reference
+ to a path defined elsewhere.
+
+
+
modulepath
+
Locations of modules on which classpath modules depend.
+
No
+
+
+
modulepathref
+
Locations of modules on which classpath modules depend,
+ given as a reference
+ to a path defined elsewhere.
+
No
+
+
+
commandpath
+
Directories containing native commands to include in jmod.
+
No
+
+
+
commandpathref
+
Directories containing native commands to include in jmod,
+ given as a reference
+ to a path defined elsewhere.
+
No
+
+
+
headerpath
+
Directories containing header files to include in jmod.
+
No
+
+
+
headerpathref
+
Directories containing header files to include in jmod,
+ given as a reference
+ to a path defined elsewhere.
+
No
+
+
+
configpath
+
Directories containing user-editable configuration files
+ to include in jmod.
+
No
+
+
+
configpathref
+
Directories containing user-editable configuration files
+ to include in jmod,
+ given as a reference
+ to a path defined elsewhere.
+
No
+
+
+
legalpath
+
Directories containing legal licenses and notices to include in jmod.
+
No
+
+
+
legalpathref
+
Directories containing legal licenses and notices to include in jmod,
+ given as a reference
+ to a path defined elsewhere.
+
No
+
+
+
nativelibpath
+
Directories containing native libraries to include in jmod.
+
No
+
+
+
nativelibpathref
+
Directories containing native libraries to include in jmod,
+ given as a reference
+ to a path defined elsewhere.
+
No
+
+
+
manpath
+
Directories containing man pages to include in jmod.
+
No
+
+
+
manpathref
+
Directories containing man pages to include in jmod,
+ given as a reference
+ to a path defined elsewhere.
Class that acts as executable entry point of module.
+
No
+
+
+
platform
+
The target platform for the jmod. Typically takes the form
+ OS-architecture. A particular JDK's
+ platform can be seen by running a command like
+ jmod describe $JDK_HOME/jmods/java.base.jmod | grep -i platform
+
No
+
+
+
hashModulesPattern
+
Regular expression for names of modules in the module path
+ which depend on the jmod being created, and which should have
+ hashes generated for them and included in the new jmod.
+
No
+
+
+
resolveByDefault
+
Boolean indicating whether the jmod should be one of
+ the default resolved modules when it is in a module path
+ searched by tools and applications.
+
No. Default is true.
+
+
+
moduleWarnings
+
Whether to emit warnings when resolving modules which are
+ not recommended for use. Comma-separated list of one of more of
+ the following:
+
+
deprecated
+
Warn if module is deprecated
+
leaving
+
Warn if module is deprecated for removal
+
incubating
+
Warn if module is an incubating (not yet official) module
Like the moduleWarnings attribute, but only specifies a single
+basis for emitting warnings. This child element may appear multiple times,
+to specify multiple conditions under which warnings should be emitted by the
+jmod tool.
+
+
Attributes:
+
+
+
Attribute
+
Description
+
Required
+
+
+
reason
+
Condition which will cause jmod tool to emit warnings. One of:
+
+
deprecated
+
Warn if module is deprecated
+
leaving
+
Warn if module is deprecated for removal
+
incubating
+
Warn if module is an incubating (not yet official) module
Assembles jmod files into an executable image. Equivalent to the JDK's
+jlink
+tool.
+
+
Requires Java 9 or later.
+
+
Parameters
+
+
+
Attribute
+
Description
+
Required
+
+
+
destDir
+
Root directory of created image.
+
Yes
+
+
+
modulepath
+
Path-like sequence of jmod files to link in order to create image.
+
One of these is required, unless a nested
+ <modulepath> is present.
+
+
+
modulepathref
+
Path-like sequence of jmod files to link in order to
+ create image, given as a reference
+ to a path defined elsewhere.
+
+
+
modules
+
Comma-separated list of modules to place in the linked image.
+
Yes, unless one or more nested <module> elements
+ are present.
+
+
+
observableModules
+
Comma-separated list of explicit modules that comprise
+ "universe" visible to link tool while linking.
+
No
+
+
+
launchers
+
Comma-separated list of commands, each of the form
+ name=module or
+ name=module/mainclass
+
No
+
+
+
locales
+
Comma-separated list of extra locales, or wildcard patterns matching
+ multiple locale names, to include.
+ Requires jdk.localedata module.
+
No
+
+
+
excludeResources
+
Comma-separated list of patterns specifying resources to exclude
+ from source jmods. Each is either a
+ standard PathMatcher pattern
+ or @filename, indicating a text file with
+ one resource name per line.
+
No
+
+
+
excludeFiles
+
Comma-separated list of patterns specifying files to exclude
+ from linked image. Each is either a
+ standard PathMatcher pattern
+ or @filename, indicating a text file with
+ one file name per line.
+
No
+
+
+
resourceOrder
+
Comma-separated list of patterns specifying resource search order.
+ Each is either a
+ standard PathMatcher pattern
+ or @filename, indicating a text file with
+ one resource name per line.
+
No
+
+
+
bindServices
+
Boolean, whether to include in linked image any service providers
+ found in module path corresponding to service provider interfaces
+ used by explicitly linked modules.
+
No, default is false
+
+
+
ignoreSigning
+
Boolean, whether to allow signed jar files.
+ (Note: As of Java 11, this is ignored and is always treated as true.)
+
No, default is false
+
+
+
includeHeaders
+
Boolean, whether to include header files in linked image.
+
No, default is true
+
+
+
includeManPages
+
Boolean, whether to include man pages in linked image.
+
No, default is true
+
+
+
includeNativeCommands
+
Boolean, whether to include native executables in linked image.
+
No, default is true
+
+
+
debug
+
Boolean, whether to include debug information.
+
No, default is true
+
+
+
verboseLevel
+
If set, the linker will produce verbose output, which will be logged at
+ the specified Ant log level (DEBUG, VERBOSE,
+ INFO, WARN, or ERR).
+
No, default is no verbose output
+
+
+
compress
+
Compression level of linked image. One of:
+
+
0 or
+ none
+
no compression (default)
+
1 or
+ strings
+
constant string sharing
+
2 or
+ zip
+
zip compression
+
+
+
No, default is no compression
+
+
+
endianness
+
Byte order of linked image, must be little or big
+
No, default is native byte order
+
+
+
checkDuplicateLegal
+
Boolean. When merging legal notices from different modules
+ because they have the same name, verify that their contents
+ are identical.
+
No, default is false, which means any license files
+ with the same name are assumed to have the same content, and no
+ checking is done.
Text file containing list of file names (not patterns),
+ one per line
+
+
+
+
compress
+
Describes how image should be compressed. Attributes:
+
+
+
Attribute
+
Description
+
Required
+
+
+
level
+
Compression level of linked image. One of:
+
+
0 or
+ none
+
no compression (default)
+
1 or
+ strings
+
constant string sharing
+
2 or
+ zip
+
zip compression
+
+
+
Yes
+
+
+
files
+
Comma-separated list of patterns matching files to compress.
+ Each pattern either a
+ standard PathMatcher pattern
+ or @filename, indicating a text file with
+ one file name per line.
+
No
+
+
+
+
<compress> can also have any number of nested
+<files> elements, with these attributes:
Download the JDK for that platform, and expand the archive manually into
+a directory of your choice. (Downloading a zip or tar.gz version of a JDK
+instead of an installer will make this easier.)
+
Determine the foreign JDK's platform string. This can be done with
+a command that examines the JDK's jmods/java.base.jmod file:
+
The target platform for the jmod. A particular JDK's platform
+ * can be seen by running
+ * jmod describe $JDK_HOME/jmods/java.base.jmod | grep -i platform.
+ *
{@code hashModulesPattern}
+ *
Regular expression for names of modules in the module path
+ * which depend on the jmod being created, and which should have
+ * hashes generated for them and included in the new jmod.
+ *
{@code resolveByDefault}
+ *
Boolean indicating whether the jmod should be one of
+ * the default resolved modules in an application. Default is true.
+ *
{@code moduleWarnings}
+ *
Whether to emit warnings when resolving modules which are
+ * not recommended for use. Comma-separated list of one of more of
+ * the following:
+ *
+ *
{@code deprecated}
+ *
Warn if module is deprecated
+ *
{@code leaving}
+ *
Warn if module is deprecated for removal
+ *
{@code incubating}
+ *
Warn if module is an incubating (not yet official) module
+ *
+ *
+ *
+ *
+ * Supported nested elements:
+ *
+ *
{@code }
+ *
Path indicating where to locate files to be placed in the jmod file.
+ *
{@code }
+ *
Path indicating where to locate dependencies.
+ *
{@code }
+ *
Path of directories containing native commands to include in jmod.
+ *
{@code }
+ *
Path of directories containing header files to include in jmod.
+ *
{@code }
+ *
Path of directories containing user-editable configuration files
+ * to include in jmod.
+ *
{@code }
+ *
Path of directories containing legal notices to include in jmod.
+ *
{@code }
+ *
Path of directories containing native libraries to include in jmod.
+ *
{@code }
+ *
Path of directories containing man pages to include in jmod.
+ *
{@code }
+ *
Module version of jmod.
+ * Must have a required {@code number} attribute. May also have optional
+ * {@code preRelease} and {@code build} attributes.
+ *
{@code }
+ *
Has one required attribute, {@code reason}. See {@code moduleWarnings}
+ * attribute above. This element may be specified multiple times.
+ *
+ *
+ * destFile and classpath are required data.
+ */
+public class Jmod
+extends Task {
+ /** Location of jmod file to be created. */
+ private File jmodFile;
+
+ /**
+ * Path of files (usually jar files or directories containing
+ * compiled classes) from which to create jmod.
+ */
+ private Path classpath;
+
+ /**
+ * Path of directories containing modules on which the modules
+ * in the classpath depend.
+ */
+ private Path modulePath;
+
+ /**
+ * Path of directories containing executable files to bundle in the
+ * created jmod.
+ */
+ private Path commandPath;
+
+ /**
+ * Path of directories containing configuration files to bundle in the
+ * created jmod.
+ */
+ private Path configPath;
+
+ /**
+ * Path of directories containing includable header files (such as for
+ * other languages) to bundle in the created jmod.
+ */
+ private Path headerPath;
+
+ /**
+ * Path of directories containing legal license files to bundle
+ * in the created jmod.
+ */
+ private Path legalPath;
+
+ /**
+ * Path of directories containing native libraries needed by classes
+ * in the modules comprising the created jmod.
+ */
+ private Path nativeLibPath;
+
+ /**
+ * Path of directories containing manual pages to bundle
+ * in the created jmod.
+ */
+ private Path manPath;
+
+ /**
+ * Module version of jmod. Either this or {@link #moduleVersion}
+ * may be set.
+ */
+ private String version;
+
+ /** Module version of jmod. Either this or {@link #version} may be set. */
+ private ModuleVersion moduleVersion;
+
+ /**
+ * Main class to execute, if Java attempts to execute jmod's module
+ * without specifying a main class explicitly.
+ */
+ private String mainClass;
+
+ /**
+ * Target platform of created jmod. Examples are {@code windows-amd64}
+ * and {@code linux-amd64}. Target platform is an attribute
+ * of each JDK, which can be seen by executing
+ * jmod describe $JDK_HOME/jmods/java.base.jmod and
+ * searching the output for a line starting with {@code platform}.
+ */
+ private String platform;
+
+ /**
+ * Regular expression matching names of modules which depend on the
+ * the created jmod's module, for which hashes should be added to the
+ * created jmod.
+ */
+ private String hashModulesPattern;
+
+ /**
+ * Whether the created jmod should be seen by Java when present in a
+ * module path, even if not explicitly named. Normally true.
+ */
+ private boolean resolveByDefault = true;
+
+ /**
+ * Reasons why module resolution during jmod creation may emit warnings.
+ */
+ private final List moduleWarnings =
+ new ArrayList<>();
+
+ /**
+ * Attribute containing the location of the jmod file to create.
+ *
+ * @return location of jmod file
+ *
+ * @see #setDestFile(File)
+ */
+ public File getDestFile() {
+ return jmodFile;
+ }
+
+ /**
+ * Sets attribute containing the location of the jmod file to create.
+ * This value is required.
+ *
+ * @param file location where jmod file will be created.
+ */
+ public void setDestFile(final File file) {
+ this.jmodFile = file;
+ }
+
+ /**
+ * Adds an unconfigured {@code } child element which can
+ * specify the files which will comprise the created jmod.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setClasspath(Path)
+ */
+ public Path createClasspath() {
+ if (classpath == null) {
+ classpath = new Path(getProject());
+ }
+ return classpath.createPath();
+ }
+
+ /**
+ * Attribute which specifies the files (usually modular .jar files)
+ * which will comprise the created jmod file.
+ *
+ * @return path of constituent files
+ *
+ * @see #setClasspath(Path)
+ */
+ public Path getClasspath() {
+ return classpath;
+ }
+
+ /**
+ * Sets attribute specifying the files that will comprise the created jmod
+ * file. Usually this contains a single modular .jar file.
+ *
+ * The classpath is required and must not be empty.
+ *
+ * @param path path of files that will comprise jmod
+ *
+ * @see #createClasspath()
+ */
+ public void setClasspath(final Path path) {
+ if (classpath == null) {
+ this.classpath = path;
+ } else {
+ classpath.append(path);
+ }
+ }
+
+ /**
+ * Sets {@linkplain #setClasspath(Path) classpath attribute} from a
+ * path reference.
+ *
+ * @param ref reference to path which will act as classpath
+ */
+ public void setClasspathRef(final Reference ref) {
+ createClasspath().setRefid(ref);
+ }
+
+ /**
+ * Creates a child {@code } element which can contain a
+ * path of directories containing modules upon which modules in the
+ * {@linkplain #setClasspath(Path) classpath} depend.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setModulePath(Path)
+ */
+ public Path createModulePath() {
+ if (modulePath == null) {
+ modulePath = new Path(getProject());
+ }
+ return modulePath.createPath();
+ }
+
+ /**
+ * Attribute containing path of directories which contain modules on which
+ * the created jmod's {@linkplain #setClasspath(Path) constituent modules}
+ * depend.
+ *
+ * @return path of directories containing modules needed by
+ * classpath modules
+ *
+ * @see #setModulePath(Path)
+ */
+ public Path getModulePath() {
+ return modulePath;
+ }
+
+ /**
+ * Sets attribute containing path of directories which contain modules
+ * on which the created jmod's
+ * {@linkplain #setClasspath(Path) constituent modules} depend.
+ *
+ * @param path path of directories containing modules needed by
+ * classpath modules
+ *
+ * @see #createModulePath()
+ */
+ public void setModulePath(final Path path) {
+ if (modulePath == null) {
+ this.modulePath = path;
+ } else {
+ modulePath.append(path);
+ }
+ }
+
+ /**
+ * Sets {@linkplain #setModulePath(Path) module path}
+ * from a path reference.
+ *
+ * @param ref reference to path which will act as module path
+ */
+ public void setModulePathRef(final Reference ref) {
+ createModulePath().setRefid(ref);
+ }
+
+ /**
+ * Creates a child element which can contain a list of directories
+ * containing native executable files to include in the created jmod.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setCommandPath(Path)
+ */
+ public Path createCommandPath() {
+ if (commandPath == null) {
+ commandPath = new Path(getProject());
+ }
+ return commandPath.createPath();
+ }
+
+ /**
+ * Attribute containing path of directories which contain native
+ * executable files to include in the created jmod.
+ *
+ * @return list of directories containing native executables
+ *
+ * @see #setCommandPath(Path)
+ */
+ public Path getCommandPath() {
+ return commandPath;
+ }
+
+ /**
+ * Sets attribute containing path of directories which contain native
+ * executable files to include in the created jmod.
+ *
+ * @param path list of directories containing native executables
+ *
+ * @see #createCommandPath()
+ */
+ public void setCommandPath(final Path path) {
+ if (commandPath == null) {
+ this.commandPath = path;
+ } else {
+ commandPath.append(path);
+ }
+ }
+
+ /**
+ * Sets {@linkplain #setCommandPath(Path) command path}
+ * from a path reference.
+ *
+ * @param ref reference to path which will act as command path
+ */
+ public void setCommandPathRef(final Reference ref) {
+ createCommandPath().setRefid(ref);
+ }
+
+ /**
+ * Creates a child element which can contain a list of directories
+ * containing user configuration files to include in the created jmod.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setConfigPath(Path)
+ */
+ public Path createConfigPath() {
+ if (configPath == null) {
+ configPath = new Path(getProject());
+ }
+ return configPath.createPath();
+ }
+
+ /**
+ * Attribute containing list of directories which contain
+ * user configuration files.
+ *
+ * @return list of directories containing user configuration files
+ *
+ * @see #setConfigPath(Path)
+ */
+ public Path getConfigPath() {
+ return configPath;
+ }
+
+ /**
+ * Sets attribute containing list of directories which contain
+ * user configuration files.
+ *
+ * @param path list of directories containing user configuration files
+ *
+ * @see #createConfigPath()
+ */
+ public void setConfigPath(final Path path) {
+ if (configPath == null) {
+ this.configPath = path;
+ } else {
+ configPath.append(path);
+ }
+ }
+
+ /**
+ * Sets {@linkplain #setConfigPath(Path) configuration file path}
+ * from a path reference.
+ *
+ * @param ref reference to path which will act as configuration file path
+ */
+ public void setConfigPathRef(final Reference ref) {
+ createConfigPath().setRefid(ref);
+ }
+
+ /**
+ * Creates a child element which can contain a list of directories
+ * containing compile-time header files for third party use, to include
+ * in the created jmod.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setHeaderPath(Path)
+ */
+ public Path createHeaderPath() {
+ if (headerPath == null) {
+ headerPath = new Path(getProject());
+ }
+ return headerPath.createPath();
+ }
+
+ /**
+ * Attribute containing a path of directories which hold compile-time
+ * header files for third party use, all of which will be included in the
+ * created jmod.
+ *
+ * @return path of directories containing header files
+ */
+ public Path getHeaderPath() {
+ return headerPath;
+ }
+
+ /**
+ * Sets attribute containing a path of directories which hold compile-time
+ * header files for third party use, all of which will be included in the
+ * created jmod.
+ *
+ * @param path path of directories containing header files
+ *
+ * @see #createHeaderPath()
+ */
+ public void setHeaderPath(final Path path) {
+ if (headerPath == null) {
+ this.headerPath = path;
+ } else {
+ headerPath.append(path);
+ }
+ }
+
+ /**
+ * Sets {@linkplain #setHeaderPath(Path) header path}
+ * from a path reference.
+ *
+ * @param ref reference to path which will act as header path
+ */
+ public void setHeaderPathRef(final Reference ref) {
+ createHeaderPath().setRefid(ref);
+ }
+
+ /**
+ * Creates a child element which can contain a list of directories
+ * containing license files to include in the created jmod.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setLegalPath(Path)
+ */
+ public Path createLegalPath() {
+ if (legalPath == null) {
+ legalPath = new Path(getProject());
+ }
+ return legalPath.createPath();
+ }
+
+ /**
+ * Attribute containing list of directories which hold license files
+ * to include in the created jmod.
+ *
+ * @return path containing directories which hold license files
+ */
+ public Path getLegalPath() {
+ return legalPath;
+ }
+
+ /**
+ * Sets attribute containing list of directories which hold license files
+ * to include in the created jmod.
+ *
+ * @param path path containing directories which hold license files
+ *
+ * @see #createLegalPath()
+ */
+ public void setLegalPath(final Path path) {
+ if (legalPath == null) {
+ this.legalPath = path;
+ } else {
+ legalPath.append(path);
+ }
+ }
+
+ /**
+ * Sets {@linkplain #setLegalPath(Path) legal licenses path}
+ * from a path reference.
+ *
+ * @param ref reference to path which will act as legal path
+ */
+ public void setLegalPathRef(final Reference ref) {
+ createLegalPath().setRefid(ref);
+ }
+
+ /**
+ * Creates a child element which can contain a list of directories
+ * containing native libraries to include in the created jmod.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setNativeLibPath(Path)
+ */
+ public Path createNativeLibPath() {
+ if (nativeLibPath == null) {
+ nativeLibPath = new Path(getProject());
+ }
+ return nativeLibPath.createPath();
+ }
+
+ /**
+ * Attribute containing list of directories which hold native libraries
+ * to include in the created jmod.
+ *
+ * @return path of directories containing native libraries
+ */
+ public Path getNativeLibPath() {
+ return nativeLibPath;
+ }
+
+ /**
+ * Sets attribute containing list of directories which hold native libraries
+ * to include in the created jmod.
+ *
+ * @param path path of directories containing native libraries
+ *
+ * @see #createNativeLibPath()
+ */
+ public void setNativeLibPath(final Path path) {
+ if (nativeLibPath == null) {
+ this.nativeLibPath = path;
+ } else {
+ nativeLibPath.append(path);
+ }
+ }
+
+ /**
+ * Sets {@linkplain #setNativeLibPath(Path) native library path}
+ * from a path reference.
+ *
+ * @param ref reference to path which will act as native library path
+ */
+ public void setNativeLibPathRef(final Reference ref) {
+ createNativeLibPath().setRefid(ref);
+ }
+
+ /**
+ * Creates a child element which can contain a list of directories
+ * containing man pages (program manuals, typically in troff format)
+ * to include in the created jmod.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setManPath(Path)
+ */
+ public Path createManPath() {
+ if (manPath == null) {
+ manPath = new Path(getProject());
+ }
+ return manPath.createPath();
+ }
+
+ /**
+ * Attribute containing list of directories containing man pages
+ * to include in created jmod. Man pages are textual program manuals,
+ * typically in troff format.
+ *
+ * @return path containing directories which hold man pages to include
+ * in jmod
+ */
+ public Path getManPath() {
+ return manPath;
+ }
+
+ /**
+ * Sets attribute containing list of directories containing man pages
+ * to include in created jmod. Man pages are textual program manuals,
+ * typically in troff format.
+ *
+ * @param path path containing directories which hold man pages to include
+ * in jmod
+ *
+ * @see #createManPath()
+ */
+ public void setManPath(final Path path) {
+ if (manPath == null) {
+ this.manPath = path;
+ } else {
+ manPath.append(path);
+ }
+ }
+
+ /**
+ * Sets {@linkplain #setManPath(Path) man pages path}
+ * from a path reference.
+ *
+ * @param ref reference to path which will act as module path
+ */
+ public void setManPathRef(final Reference ref) {
+ createManPath().setRefid(ref);
+ }
+
+ /**
+ * Creates an uninitialized child element representing the version of
+ * the module represented by the created jmod.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setVersion(String)
+ */
+ public ModuleVersion createVersion() {
+ if (moduleVersion != null) {
+ throw new BuildException(
+ "No more than one element is allowed.",
+ getLocation());
+ }
+ moduleVersion = new ModuleVersion();
+ return moduleVersion;
+ }
+
+ /**
+ * Attribute which specifies
+ * a module version
+ * for created jmod.
+ *
+ * @return module version for created jmod
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * Sets the module version
+ * for the created jmod.
+ *
+ * @param version module version of created jmod
+ *
+ * @see #createVersion()
+ */
+ public void setVersion(final String version) {
+ this.version = version;
+ }
+
+ /**
+ * Attribute containing the class that acts as the executable entry point
+ * of the created jmod.
+ *
+ * @return fully-qualified name of jmod's main class
+ */
+ public String getMainClass() {
+ return mainClass;
+ }
+
+ /**
+ * Sets attribute containing the class that acts as the
+ * executable entry point of the created jmod.
+ *
+ * @param className fully-qualified name of jmod's main class
+ */
+ public void setMainClass(final String className) {
+ this.mainClass = className;
+ }
+
+ /**
+ * Attribute containing the platform for which the jmod
+ * will be built. Platform values are defined in the
+ * {@code java.base.jmod} of JDKs, and usually take the form
+ * OS{@code -}architecture. If unset,
+ * current platform is used.
+ *
+ * @return OS and architecture for which jmod will be built, or {@code null}
+ */
+ public String getPlatform() {
+ return platform;
+ }
+
+ /**
+ * Sets attribute containing the platform for which the jmod
+ * will be built. Platform values are defined in the
+ * {@code java.base.jmod} of JDKs, and usually take the form
+ * OS{@code -}architecture. If unset,
+ * current platform is used.
+ *
+ * A JDK's platform can be viewed with a command like:
+ * jmod describe $JDK_HOME/jmods/java.base.jmod | grep -i platform.
+o *
+ * @param platform platform for which jmod will be created, or {@code null}
+ */
+ public void setPlatform(final String platform) {
+ this.platform = platform;
+ }
+
+ /**
+ * Attribute containing a regular expression which specifies which
+ * of the modules that depend on the jmod being created should have
+ * hashes generated and added to the jmod.
+ *
+ * @return regex specifying which dependent modules should have
+ * their generated hashes included
+ */
+ public String getHashModulesPattern() {
+ return hashModulesPattern;
+ }
+
+ /**
+ * Sets attribute containing a regular expression which specifies which
+ * of the modules that depend on the jmod being created should have
+ * hashes generated and added to the jmod.
+ *
+ * @param pattern regex specifying which dependent modules should have
+ * their generated hashes included
+ */
+ public void setHashModulesPattern(final String pattern) {
+ this.hashModulesPattern = pattern;
+ }
+
+ /**
+ * Attribute indicating whether the created jmod should be visible
+ * in a module path, even when not specified explicitly. True by default.
+ *
+ * @return whether jmod should be visible in module paths
+ */
+ public boolean getResolveByDefault() {
+ return resolveByDefault;
+ }
+
+ /**
+ * Sets attribute indicating whether the created jmod should be visible
+ * in a module path, even when not specified explicitly. True by default.
+ *
+ * @param resolve whether jmod should be visible in module paths
+ */
+ public void setResolveByDefault(final boolean resolve) {
+ this.resolveByDefault = resolve;
+ }
+
+ /**
+ * Creates a child element which can specify the circumstances
+ * under which jmod creation emits warnings.
+ *
+ * @return new, unconfigured child element
+ *
+ * @see #setModuleWarnings(String)
+ */
+ public ResolutionWarningSpec createModuleWarning() {
+ ResolutionWarningSpec warningSpec = new ResolutionWarningSpec();
+ moduleWarnings.add(warningSpec);
+ return warningSpec;
+ }
+
+ /**
+ * Sets attribute containing a comma-separated list of reasons for
+ * jmod creation to emit warnings. Valid values in list are:
+ * {@code deprecated}, {@code leaving}, {@code incubating}.
+ *
+ * @param warningList list containing one or more of the above values,
+ * separated by commas
+ *
+ * @see #createModuleWarning()
+ * @see Jmod.ResolutionWarningReason
+ */
+ public void setModuleWarnings(final String warningList) {
+ for (String warning : warningList.split(",")) {
+ moduleWarnings.add(new ResolutionWarningSpec(warning));
+ }
+ }
+
+ /**
+ * Permissible reasons for jmod creation to emit warnings.
+ */
+ public static class ResolutionWarningReason
+ extends EnumeratedAttribute {
+ /**
+ * String value indicating warnings are emitted for modules
+ * marked as deprecated (but not deprecated for removal).
+ */
+ public static final String DEPRECATED = "deprecated";
+
+ /**
+ * String value indicating warnings are emitted for modules
+ * marked as deprecated for removal.
+ */
+ public static final String LEAVING = "leaving";
+
+ /**
+ * String value indicating warnings are emitted for modules
+ * designated as "incubating" in the JDK.
+ */
+ public static final String INCUBATING = "incubating";
+
+ /** Maps Ant task values to jmod option values. */
+ private static final Map VALUES_TO_OPTIONS;
+
+ static {
+ Map map = new LinkedHashMap<>();
+ map.put(DEPRECATED, "deprecated");
+ map.put(LEAVING, "deprecated-for-removal");
+ map.put(INCUBATING, "incubating");
+
+ VALUES_TO_OPTIONS = Collections.unmodifiableMap(map);
+ }
+
+ @Override
+ public String[] getValues() {
+ return VALUES_TO_OPTIONS.keySet().toArray(new String[0]);
+ }
+
+ /**
+ * Converts this object's current value to a jmod tool
+ * option value.
+ *
+ * @return jmod option value
+ */
+ String toCommandLineOption() {
+ return VALUES_TO_OPTIONS.get(getValue());
+ }
+
+ /**
+ * Converts a string to a {@code ResolutionWarningReason} instance.
+ *
+ * @param s string to convert
+ *
+ * @return {@code ResolutionWarningReason} instance corresponding to
+ * string argument
+ *
+ * @throws BuildException if argument is not a valid
+ * {@code ResolutionWarningReason} value
+ */
+ public static ResolutionWarningReason valueOf(String s) {
+ return (ResolutionWarningReason)
+ getInstance(ResolutionWarningReason.class, s);
+ }
+ }
+
+ /**
+ * Child element which enables jmod tool warnings. 'reason' attribute
+ * is required.
+ */
+ public class ResolutionWarningSpec {
+ /** Condition which should trigger jmod warning output. */
+ private ResolutionWarningReason reason;
+
+ /**
+ * Creates an uninitialized element.
+ */
+ public ResolutionWarningSpec() {
+ // Deliberately empty.
+ }
+
+ /**
+ * Creates an element with the given reason attribute.
+ *
+ * @param reason non{@code null} {@link Jmod.ResolutionWarningReason}
+ * value
+ *
+ * @throws BuildException if argument is not a valid
+ * {@code ResolutionWarningReason}
+ */
+ public ResolutionWarningSpec(String reason) {
+ setReason(ResolutionWarningReason.valueOf(reason));
+ }
+
+ /**
+ * Required attribute containing reason for emitting jmod warnings.
+ *
+ * @return condition which triggers jmod warnings
+ */
+ public ResolutionWarningReason getReason() {
+ return reason;
+ }
+
+ /**
+ * Sets attribute containing reason for emitting jmod warnings.
+ *
+ * @param reason condition which triggers jmod warnings
+ */
+ public void setReason(ResolutionWarningReason reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * Verifies this object's state.
+ *
+ * @throws BuildException if this object's reason is {@code null}
+ */
+ public void validate() {
+ if (reason == null) {
+ throw new BuildException("reason attribute is required",
+ getLocation());
+ }
+ }
+ }
+
+ /**
+ * Checks whether a resource is a directory. Used for checking validity
+ * of jmod path arguments which have to be directories.
+ *
+ * @param resource resource to check
+ *
+ * @return true if resource exists and is not a directory,
+ * false if it is a directory or does not exist
+ */
+ private static boolean isRegularFile(Resource resource) {
+ return resource.isExists() && !resource.isDirectory();
+ }
+
+ /**
+ * Checks that all paths which are required to be directories only,
+ * refer only to directories.
+ *
+ * @throws BuildException if any path has an existing file
+ * which is a non-directory
+ */
+ private void checkDirPaths() {
+ if (modulePath != null
+ && modulePath.stream().anyMatch(Jmod::isRegularFile)) {
+
+ throw new BuildException(
+ "ModulePath must contain only directories.", getLocation());
+ }
+ if (commandPath != null
+ && commandPath.stream().anyMatch(Jmod::isRegularFile)) {
+
+ throw new BuildException(
+ "CommandPath must contain only directories.", getLocation());
+ }
+ if (configPath != null
+ && configPath.stream().anyMatch(Jmod::isRegularFile)) {
+
+ throw new BuildException(
+ "ConfigPath must contain only directories.", getLocation());
+ }
+ if (headerPath != null
+ && headerPath.stream().anyMatch(Jmod::isRegularFile)) {
+
+ throw new BuildException(
+ "HeaderPath must contain only directories.", getLocation());
+ }
+ if (legalPath != null
+ && legalPath.stream().anyMatch(Jmod::isRegularFile)) {
+
+ throw new BuildException(
+ "LegalPath must contain only directories.", getLocation());
+ }
+ if (nativeLibPath != null
+ && nativeLibPath.stream().anyMatch(Jmod::isRegularFile)) {
+
+ throw new BuildException(
+ "NativeLibPath must contain only directories.", getLocation());
+ }
+ if (manPath != null
+ && manPath.stream().anyMatch(Jmod::isRegularFile)) {
+
+ throw new BuildException(
+ "ManPath must contain only directories.", getLocation());
+ }
+ }
+
+ /**
+ * Creates a jmod file according to this task's properties
+ * and child elements.
+ *
+ * @throws BuildException if destFile is not set
+ * @throws BuildException if classpath is not set or is empty
+ * @throws BuildException if any path other than classpath refers to an
+ * existing file which is not a directory
+ * @throws BuildException if both {@code version} attribute and
+ * {@code } child element are present
+ * @throws BuildException if {@code hashModulesPattern} is set, but
+ * module path is not defined
+ */
+ @Override
+ public void execute()
+ throws BuildException {
+
+ if (jmodFile == null) {
+ throw new BuildException("Destination file is required.",
+ getLocation());
+ }
+
+ if (classpath == null) {
+ throw new BuildException("Classpath is required.",
+ getLocation());
+ }
+
+ if (classpath.stream().noneMatch(Resource::isExists)) {
+ throw new BuildException(
+ "Classpath must contain at least one entry which exists.",
+ getLocation());
+ }
+
+ if (version != null && moduleVersion != null) {
+ throw new BuildException(
+ "version attribute and nested element "
+ + "cannot both be present.",
+ getLocation());
+ }
+
+ if (hashModulesPattern != null && !hashModulesPattern.isEmpty()
+ && modulePath == null) {
+
+ throw new BuildException(
+ "hashModulesPattern requires a module path, since "
+ + "it will generate hashes of the other modules which depend "
+ + "on the module being created.",
+ getLocation());
+ }
+
+ checkDirPaths();
+
+ Path[] dependentPaths = {
+ classpath,
+ modulePath,
+ commandPath,
+ configPath,
+ headerPath,
+ legalPath,
+ nativeLibPath,
+ manPath,
+ };
+ Union allResources = new Union(getProject());
+ for (Path path : dependentPaths) {
+ if (path != null) {
+ for (String entry : path.list()) {
+ File entryFile = new File(entry);
+ if (entryFile.isDirectory()) {
+ log("Will compare timestamp of all files in "
+ + "\"" + entryFile + "\" with timestamp of "
+ + jmodFile, Project.MSG_VERBOSE);
+ FileSet fileSet = new FileSet();
+ fileSet.setDir(entryFile);
+ allResources.add(fileSet);
+ } else {
+ log("Will compare timestamp of \"" + entryFile + "\" "
+ + "with timestamp of " + jmodFile,
+ Project.MSG_VERBOSE);
+ allResources.add(new FileResource(entryFile));
+ }
+ }
+ }
+ }
+
+ ResourceCollection outOfDate =
+ ResourceUtils.selectOutOfDateSources(this, allResources,
+ new MergingMapper(jmodFile.toString()),
+ getProject(),
+ FileUtils.getFileUtils().getFileTimestampGranularity());
+
+ if (outOfDate.isEmpty()) {
+ log("Skipping jmod creation, since \"" + jmodFile + "\" "
+ + "is already newer than all files in paths.",
+ Project.MSG_VERBOSE);
+ return;
+ }
+
+ Collection args = buildJmodArgs();
+
+ try {
+ log("Deleting " + jmodFile + " if it exists.", Project.MSG_VERBOSE);
+ Files.deleteIfExists(jmodFile.toPath());
+ } catch (IOException e) {
+ throw new BuildException(
+ "Could not remove old file \"" + jmodFile + "\": " + e, e,
+ getLocation());
+ }
+
+ ToolProvider jmod = ToolProvider.findFirst("jmod").orElseThrow(
+ () -> new BuildException("jmod tool not found in JDK.",
+ getLocation()));
+
+ log("Executing: jmod " + 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 = jmod.run(out, err, args.toArray(new String[0]));
+ }
+
+ if (exitCode != 0) {
+ StringBuilder message = new StringBuilder();
+ message.append("jmod 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());
+ }
+
+ log("Created " + jmodFile.getAbsolutePath(), Project.MSG_INFO);
+ }
+
+ /**
+ * Creates list of arguments to jmod tool, based on this
+ * instance's current state.
+ *
+ * @return new list of jmod arguments
+ */
+ private Collection buildJmodArgs() {
+ Collection args = new ArrayList<>();
+
+ args.add("create");
+
+ args.add("--class-path");
+ args.add(classpath.toString());
+
+ // Paths
+
+ if (modulePath != null && !modulePath.isEmpty()) {
+ args.add("--module-path");
+ args.add(modulePath.toString());
+ }
+ if (commandPath != null && !commandPath.isEmpty()) {
+ args.add("--cmds");
+ args.add(commandPath.toString());
+ }
+ if (configPath != null && !configPath.isEmpty()) {
+ args.add("--config");
+ args.add(configPath.toString());
+ }
+ if (headerPath != null && !headerPath.isEmpty()) {
+ args.add("--header-files");
+ args.add(headerPath.toString());
+ }
+ if (legalPath != null && !legalPath.isEmpty()) {
+ args.add("--legal-notices");
+ args.add(legalPath.toString());
+ }
+ if (nativeLibPath != null && !nativeLibPath.isEmpty()) {
+ args.add("--libs");
+ args.add(nativeLibPath.toString());
+ }
+ if (manPath != null && !manPath.isEmpty()) {
+ args.add("--man-pages");
+ args.add(manPath.toString());
+ }
+
+ // Strings
+
+ String versionStr =
+ (moduleVersion != null ? moduleVersion.toModuleVersionString() : version);
+ if (versionStr != null && !versionStr.isEmpty()) {
+ args.add("--module-version");
+ args.add(versionStr);
+ }
+
+ if (mainClass != null && !mainClass.isEmpty()) {
+ args.add("--main-class");
+ args.add(mainClass);
+ }
+ if (platform != null && !platform.isEmpty()) {
+ args.add("--target-platform");
+ args.add(platform);
+ }
+ if (hashModulesPattern != null && !hashModulesPattern.isEmpty()) {
+ args.add("--hash-modules");
+ args.add(hashModulesPattern);
+ }
+
+ // booleans
+
+ if (!resolveByDefault) {
+ args.add("--do-not-resolve-by-default");
+ }
+ for (ResolutionWarningSpec moduleWarning : moduleWarnings) {
+ moduleWarning.validate();
+ args.add("--warn-if-resolved");
+ args.add(moduleWarning.getReason().toCommandLineOption());
+ }
+
+ // Destination file
+
+ args.add(jmodFile.toString());
+
+ return args;
+ }
+}
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 000000000..36fd28130
--- /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.
+ *
+ * Supported attributes:
+ *
+ *
{@code destDir}
+ *
Root directory of created image. (required)
+ *
{@code modulePath}
+ *
Path of modules. Should be a list of .jmod files. Required, unless
+ * nested module path or modulepathref is present.
+ *
{@code modulePathRef}
+ *
Reference to path of modules. Referenced path should be
+ * a list of .jmod files.
+ *
{@code modules}
+ *
Comma-separated list of modules to assemble. Required, unless
+ * one or more nested {@code } elements are present.
+ *
{@code observableModules}
+ *
Comma-separated list of explicit modules that comprise
+ * "universe" visible to tool while linking.
+ *
{@code launchers}
+ *
Comma-separated list of commands, each of the form
+ * name{@code =}module or
+ * name{@code =}module{@code /}mainclass
+ *
{@code excludeFiles}
+ *
Comma-separated list of patterns specifying files to exclude from
+ * linked image.
+ * Each is either a standard PathMatcher pattern
+ * or {@code @}filename.
+ *
{@code excludeResources}
+ *
Comma-separated list of patterns specifying resources to exclude from jmods.
+ * Each is either a standard PathMatcher pattern
+ * or {@code @}filename.
+ *
{@code locales}
+ *
Comma-separated list of extra locales to include,
+ * requires {@code jdk.localedata} module
+ *
{@code resourceOrder}
+ *
Comma-separated list of patterns specifying resource search order.
+ * Each is either a standard PathMatcher pattern
+ * or {@code @}filename.
+ *
{@code bindServices}
+ *
boolean, whether to link service providers; default is false
+ *
{@code ignoreSigning}
+ *
boolean, whether to allow signed jar files; default is false
+ *
{@code includeHeaders}
+ *
boolean, whether to include header files; default is true
+ *
{@code includeManPages}
+ *
boolean, whether to include man pages; default is true
+ *
{@code includeNativeCommands}
+ *
boolean, whether to include native executables normally generated
+ * for image; default is true
+ *
{@code debug}
+ *
boolean, whether to include debug information; default is true
+ *
{@code verboseLevel}
+ *
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}).
+ *
{@code compress}
+ *
compression level, one of:
+ *
+ *
{@code 0}
+ *
{@code none}
+ *
no compression (default)
+ *
{@code 1}
+ *
{@code strings}
+ *
constant string sharing
+ *
{@code 2}
+ *
{@code zip}
+ *
zip compression
+ *
+ *
{@code endianness}
+ *
Must be {@code little} or {@code big}, default is native endianness
+ *
{@code checkDuplicateLegal}
+ *
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.
+ *
{@code vmType}
+ *
Hotspot VM in image, one of:
+ *
+ *
{@code client}
+ *
{@code server}
+ *
{@code minimal}
+ *
{@code all} (default)
+ *
+ *
+ *
+ *
+ * Supported nested elements
+ *
+ *
{@code }
+ *
path element
+ *
{@code }
+ *
May be specified multiple times.
+ * Only attribute is required {@code name} attribute.
+ *
{@code }
+ *
May be specified multiple times.
+ * Only attribute is required {@code name} attribute.
+ *
{@code }
+ *
May be specified multiple times. Attributes:
+ *
+ *
{@code name} (required)
+ *
{@code module} (required)
+ *
{@code mainClass} (optional)
+ *
+ *
{@code }
+ *
May be specified multiple times.
+ * Only attribute is required {@code name} attribute.
+ *
{@code }
+ *
Explicit resource search order in image. May be specified multiple
+ * times. Exactly one of these attributes must be specified:
+ *
Text file containing list of resource names (not patterns),
+ * one per line
+ *
+ * If the {@code resourceOrder} attribute is also present on the task, its
+ * patterns are treated as if they occur before patterns in nested
+ * {@code } elements.
+ *
{@code }
+ *
Excludes files from linked image tree. May be specified multiple times.
+ * Exactly one of these attributes is required:
+ *
Text file containing list of resource names (not patterns),
+ * one per line
+ *
+ *
{@code }
+ *
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 } elements, each with
+ * either a {@code pattern} attribute or {@code listFile} attribute.
+ *
{@code }
+ *
Replaces, augments, or trims the image's release info properties.
+ * This may specify any of the following:
+ *
+ *
A {@code file} attribute, pointing to a Java properties file
+ * containing new release info properties that will entirely replace
+ * the current ones.
+ *
A {@code delete} attribute, containing comma-separated property keys
+ * to remove from application's release info, and/or any number of
+ * nested {@code } elements, each with a required {@code key}
+ * attribute.
+ *
One or more nested {@code } elements, containing either
+ * {@code key} and {@code value} attributes, or a {@code file}
+ * attribute and an optional {@code charset} attribute.
+ *
+ *
+ *
+ * @see jlink tool reference
+ */
+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 modules = new ArrayList<>();
+
+ /** If non-empty, list of all modules linker is permitted to know about. */
+ private final List observableModules = new ArrayList<>();
+
+ /**
+ * Additional runnable programs which linker will place in image's
+ * bin directory.
+ */
+ private final List launchers = new ArrayList<>();
+
+ /**
+ * Locales to explicitly include from {@code jdk.localdata} module.
+ * If empty, all locales are included.
+ */
+ private final List locales = new ArrayList<>();
+
+ /** Resource ordering. */
+ private final List ordering = new ArrayList<>();
+
+ /** Files to exclude from linked image. */
+ private final List excludedFiles = new ArrayList<>();
+
+ /**
+ * Resources in linked modules which should be excluded from linked image.
+ */
+ private final List 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.
+ * Forced to true as of Java 11.
+ */
+ private boolean checkDuplicateLegal;
+
+ /** Type of VM in linked image. */
+ private VMType vmType;
+
+ /** Changes to linked image's default release info. */
+ private final List releaseInfo = new ArrayList<>();
+
+ /**
+ * Adds child {@code } 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 } 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 } 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 } 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 * name{@code =}module or
+ * name{@code =}module{@code /}mainclass.
+ *
+ * @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 } 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 } 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 } 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 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.
+ *
+ * Note: As of Java 11, this attribute is internally forced to true. See
+ * the source.
+ *
+ * @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 } 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 } 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
+ * PathMatcher pattern
+ * 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
+ * PathMatcher pattern
+ * 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
+ * name{@code =}module or
+ * name{@code =}module{@code /}mainclass.
+ *
+ * @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:
+ * name{@code =}module or
+ * name{@code =}module{@code /}mainclass.
+ *
+ * @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:
+ *
+ *
{@code 0}
+ *
{@code none}
+ *
no compression (default)
+ *
{@code 1}
+ *
{@code strings}
+ *
constant string sharing
+ *
{@code 2}
+ *
{@code zip}
+ *
zip compression
+ *
+ */
+ public static class CompressionLevel
+ extends EnumeratedAttribute {
+ private static final Map KEYWORDS;
+
+ static {
+ Map 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 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 propertiesToAdd = new ArrayList<>();
+
+ /**
+ * Property keys to remove from release properties of linked image.
+ */
+ private final List 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
+ * jlink options.
+ *
+ * @return new collection of jlink options based on this element's
+ * attributes and child elements
+ */
+ public Collection toCommandLineOptions() {
+ Collection 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 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 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() {
+ @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 jlink tool, based on this
+ * instance's current state.
+ *
+ * @return new list of jlink arguments
+ *
+ * @throws BuildException if any inconsistencies attributes/elements
+ * is found
+ */
+ private Collection buildJlinkArgs() {
+ Collection 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 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;
+ }
+}
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 000000000..99b11e536
--- /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;
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 db8b3a333..be2728463 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}.
*
*
It is basically a wrapper for the jlink code written originally
* by Patrick Beard. The
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 000000000..0a43420bc
--- /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
+ * module version.
+ * The version number is required; all other parts are optional.
+ */
+public class ModuleVersion {
+ /** Module version's required version number. */
+ private String number;
+
+ /** Module version's optional pre-release version. */
+ private String preRelease;
+
+ /** Module version's optional build version. */
+ 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 +
+ "]";
+ }
+}
diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/modules/JmodTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/modules/JmodTest.java
new file mode 100644
index 000000000..b8cecb9c8
--- /dev/null
+++ b/src/tests/junit/org/apache/tools/ant/taskdefs/modules/JmodTest.java
@@ -0,0 +1,690 @@
+/*
+ * 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.BufferedReader;
+import java.io.StringReader;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import java.io.File;
+import java.io.IOException;
+
+import java.nio.file.Files;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+import java.util.spi.ToolProvider;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.BuildFileRule;
+
+/**
+ * Tests the {@link Jmod} task.
+ */
+public class JmodTest {
+ @Rule
+ public final BuildFileRule buildRule = new BuildFileRule();
+
+ @Rule
+ public final ExpectedException expected = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ buildRule.configureProject("src/etc/testcases/taskdefs/jmod.xml");
+ buildRule.executeTarget("setUp");
+ }
+
+ @Test
+ public void testDestAndClasspathNoJmod() {
+ buildRule.executeTarget("destAndClasspathNoJmod");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+ }
+
+ @Test
+ public void testDestAndNestedClasspath() {
+ buildRule.executeTarget("classpath-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+ }
+
+ @Test
+ public void testDestAndClasspathOlderThanJmod()
+ throws IOException {
+ buildRule.executeTarget("destAndClasspathOlderThanJmod");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ File jar = new File(buildRule.getProject().getProperty("hello.jar"));
+ Assert.assertTrue("Checking that newer jmod was not written "
+ + "when source files are older.",
+ Files.getLastModifiedTime(jmod.toPath()).toInstant().isAfter(
+ Instant.now().plus(30, ChronoUnit.MINUTES)));
+ }
+
+ @Test
+ public void testNoDestFile() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("noDestFile");
+ }
+
+ @Test
+ public void testNoClasspath() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("noClasspath");
+ }
+
+ @Test
+ public void testEmptyClasspath() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("emptyClasspath");
+ }
+
+ @Test
+ public void testClasspathEntirelyNonexistent() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("nonexistentClasspath");
+ }
+
+ @Test
+ public void testClasspathref() {
+ buildRule.executeTarget("classpathref");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+ }
+
+ @Test
+ public void testClasspathAttributeAndChildElement() {
+ buildRule.executeTarget("classpath-both");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+ }
+
+ @Test
+ public void testModulepath() {
+ buildRule.executeTarget("modulepath");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+ }
+
+ @Test
+ public void testModulepathref() {
+ buildRule.executeTarget("modulepathref");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+ }
+
+ @Test
+ public void testModulepathNested() {
+ buildRule.executeTarget("modulepath-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+ }
+
+ @Test
+ public void testModulepathNonDir() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("modulepathnondir");
+ }
+
+ @Test
+ public void testModulepathAttributeAndChildElement() {
+ buildRule.executeTarget("modulepath-both");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+ }
+
+ @Test
+ public void testCommandPath() {
+ buildRule.executeTarget("commandpath");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains command.",
+ containsLine(output, l -> l.equals("bin/command1")));
+ }
+
+ @Test
+ public void testCommandPathref() {
+ buildRule.executeTarget("commandpathref");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains command.",
+ containsLine(output, l -> l.equals("bin/command2")));
+ }
+
+ @Test
+ public void testCommandPathNested() {
+ buildRule.executeTarget("commandpath-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains command.",
+ containsLine(output, l -> l.equals("bin/command3")));
+ }
+
+ @Test
+ public void testCommandPathAttributeAndChildElement() {
+ buildRule.executeTarget("commandpath-both");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains commands "
+ + "from both attribute and child element.",
+ containsAll(output,
+ l -> l.equals("bin/command4"),
+ l -> l.equals("bin/command5")));
+ }
+
+ @Test
+ public void testHeaderPath() {
+ buildRule.executeTarget("headerpath");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains header file.",
+ containsLine(output, l -> l.equals("include/header1.h")));
+ }
+
+ @Test
+ public void testHeaderPathref() {
+ buildRule.executeTarget("headerpathref");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains header file.",
+ containsLine(output, l -> l.equals("include/header2.h")));
+ }
+
+ @Test
+ public void testHeaderPathNested() {
+ buildRule.executeTarget("headerpath-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains header file.",
+ containsLine(output, l -> l.equals("include/header3.h")));
+ }
+
+ @Test
+ public void testHeaderPathAttributeAndChildElement() {
+ buildRule.executeTarget("headerpath-both");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains header files "
+ + "from both attribute and child element.",
+ containsAll(output,
+ l -> l.equals("include/header4.h"),
+ l -> l.equals("include/header5.h")));
+ }
+
+ @Test
+ public void testConfigPath() {
+ buildRule.executeTarget("configpath");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains config file.",
+ containsLine(output, l -> l.equals("conf/config1.properties")));
+ }
+
+ @Test
+ public void testConfigPathref() {
+ buildRule.executeTarget("configpathref");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains config file.",
+ containsLine(output, l -> l.equals("conf/config2.properties")));
+ }
+
+ @Test
+ public void testConfigPathNested() {
+ buildRule.executeTarget("configpath-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains config file.",
+ containsLine(output, l -> l.equals("conf/config3.properties")));
+ }
+
+ @Test
+ public void testConfigPathAttributeAndChildElement() {
+ buildRule.executeTarget("configpath-both");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains config files "
+ + "from both attribute and child element.",
+ containsAll(output,
+ l -> l.equals("conf/config4.properties"),
+ l -> l.equals("conf/config5.properties")));
+ }
+
+ @Test
+ public void testLegalPath() {
+ buildRule.executeTarget("legalpath");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains license file.",
+ containsLine(output, l -> l.equals("legal/legal1.txt")));
+ }
+
+ @Test
+ public void testLegalPathref() {
+ buildRule.executeTarget("legalpathref");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains license file.",
+ containsLine(output, l -> l.equals("legal/legal2.txt")));
+ }
+
+ @Test
+ public void testLegalPathNested() {
+ buildRule.executeTarget("legalpath-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains license file.",
+ containsLine(output, l -> l.equals("legal/legal3.txt")));
+ }
+
+ @Test
+ public void testLegalPathAttributeAndChildElement() {
+ buildRule.executeTarget("legalpath-both");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains legal files "
+ + "from both attribute and child element.",
+ containsAll(output,
+ l -> l.equals("legal/legal4.txt"),
+ l -> l.equals("legal/legal5.txt")));
+ }
+
+ @Test
+ public void testManPath() {
+ buildRule.executeTarget("manpath");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains man page.",
+ containsLine(output, l -> l.equals("man/man1.1")));
+ }
+
+ @Test
+ public void testManPathref() {
+ buildRule.executeTarget("manpathref");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains man page.",
+ containsLine(output, l -> l.equals("man/man2.1")));
+ }
+
+ @Test
+ public void testManPathNested() {
+ buildRule.executeTarget("manpath-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains man page.",
+ containsLine(output, l -> l.equals("man/man3.1")));
+ }
+
+ @Test
+ public void testManPathAttributeAndChildElement() {
+ buildRule.executeTarget("manpath-both");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains man pages "
+ + "from both attribute and child element.",
+ containsAll(output,
+ l -> l.equals("man/man4.1"),
+ l -> l.equals("man/man5.1")));
+ }
+
+ @Test
+ public void testNativeLibPath() {
+ buildRule.executeTarget("nativelibpath");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains native library.",
+ containsLine(output, l -> l.matches("lib/[^/]+\\.(dll|dylib|so)")));
+ }
+
+ @Test
+ public void testNativeLibPathref() {
+ buildRule.executeTarget("nativelibpathref");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains native library.",
+ containsLine(output, l -> l.matches("lib/[^/]+\\.(dll|dylib|so)")));
+ }
+
+ @Test
+ public void testNativeLibPathNested() {
+ buildRule.executeTarget("nativelibpath-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains native library.",
+ containsLine(output, l -> l.matches("lib/[^/]+\\.(dll|dylib|so)")));
+ }
+
+ @Test
+ public void testNativeLibPathAttributeAndChildElement() {
+ buildRule.executeTarget("nativelibpath-both");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("list", jmod.toString());
+ Assert.assertTrue("Checking that jmod contains native libraries "
+ + "from both attribute and child element.",
+ containsAll(output,
+ l -> l.matches("lib/(lib)?zip\\.(dll|dylib|so)"),
+ l -> l.matches("lib/(lib)?jvm\\.(dll|dylib|so)")));
+ }
+
+ @Test
+ public void testVersion() {
+ buildRule.executeTarget("version");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String version = buildRule.getProject().getProperty("version");
+ Assert.assertNotNull("Checking that 'version' property is set",
+ version);
+ Assert.assertFalse("Checking that 'version' property is not empty",
+ version.isEmpty());
+
+ String output = runJmod("describe", jmod.toString());
+ Assert.assertTrue("Checking that jmod has correct version.",
+ containsLine(output, l -> l.endsWith("@" + version)));
+ }
+
+ @Test
+ public void testNestedVersion() {
+ buildRule.executeTarget("version-nested");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("describe", jmod.toString());
+ Assert.assertTrue("Checking that jmod has correct version.",
+ containsLine(output, l -> l.matches(".*@1\\.0\\.1[-+]+99")));
+ }
+
+ @Test
+ public void testNestedVersionNumberOnly() {
+ buildRule.executeTarget("version-nested-number");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("describe", jmod.toString());
+ Assert.assertTrue("Checking that jmod has correct version.",
+ containsLine(output, l -> l.endsWith("@1.0.1")));
+ }
+
+ @Test
+ public void testNestedVersionNoNumber() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("version-nested-no-number");
+ }
+
+ @Test
+ public void testNestedVersionInvalidNumber() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("version-nested-invalid-number");
+ }
+
+ @Test
+ public void testNestedVersionInvalidPreRelease() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("version-nested-invalid-prerelease");
+ }
+
+ @Test
+ public void testVersionAttributeAndChildElement() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("version-both");
+ }
+
+ @Test
+ public void testMainClass() {
+ buildRule.executeTarget("mainclass");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String mainClass =
+ buildRule.getProject().getProperty("hello.main-class");
+ Assert.assertNotNull("Checking that 'main-class' property is set",
+ mainClass);
+ Assert.assertFalse("Checking that 'main-class' property is not empty",
+ mainClass.isEmpty());
+
+ String output = runJmod("describe", jmod.toString());
+
+ String mainClassPattern = "main-class\\s+" + Pattern.quote(mainClass);
+ Assert.assertTrue("Checking that jmod has correct main class.",
+ containsLine(output, l -> l.matches(mainClassPattern)));
+ }
+
+ @Test
+ public void testPlatform() {
+ buildRule.executeTarget("platform");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String platform = buildRule.getProject().getProperty("target-platform");
+ Assert.assertNotNull("Checking that 'target-platform' property is set",
+ platform);
+ Assert.assertFalse("Checking that 'target-platform' property "
+ + "is not empty", platform.isEmpty());
+
+ String output = runJmod("describe", jmod.toString());
+
+ String platformPattern = "platform\\s+" + Pattern.quote(platform);
+ Assert.assertTrue("Checking that jmod has correct main class.",
+ containsLine(output, l -> l.matches(platformPattern)));
+ }
+
+ @Test
+ public void testHashing() {
+ buildRule.executeTarget("hashing");
+
+ File jmod = new File(buildRule.getProject().getProperty("jmod"));
+ Assert.assertTrue("Checking that jmod was successfully created.",
+ jmod.exists());
+
+ String output = runJmod("describe", jmod.toString());
+
+ Assert.assertTrue("Checking that jmod has module hashes.",
+ containsLine(output, l -> l.startsWith("hashes")));
+ }
+
+ private String runJmod(final String... args) {
+ ToolProvider jmod = ToolProvider.findFirst("jmod").orElseThrow(
+ () -> new RuntimeException("jmod tool not found in JDK."));
+
+ ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+ int exitCode;
+ try (PrintStream out = new PrintStream(stdout);
+ PrintStream err = new PrintStream(stderr)) {
+
+ exitCode = jmod.run(out, err, args);
+ }
+
+ if (exitCode != 0) {
+ throw new RuntimeException(
+ "jmod failed, output is: " + stdout + ", error is: " + stderr);
+ }
+
+ return stdout.toString();
+ }
+
+ private boolean containsLine(final String lines,
+ final Predicate super String> test) {
+ try (BufferedReader reader =
+ new BufferedReader(new StringReader(lines))) {
+
+ return reader.lines().anyMatch(test);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private boolean containsAll(final String lines,
+ final Predicate super String> test1,
+ final Predicate super String> test2) {
+
+ try (BufferedReader reader =
+ new BufferedReader(new StringReader(lines))) {
+
+ boolean test1Matched = false;
+ boolean test2Matched = false;
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ test1Matched |= test1.test(line);
+ test2Matched |= test2.test(line);
+ }
+
+ return test1Matched && test2Matched;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/modules/LinkTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/modules/LinkTest.java
new file mode 100644
index 000000000..9d89b773f
--- /dev/null
+++ b/src/tests/junit/org/apache/tools/ant/taskdefs/modules/LinkTest.java
@@ -0,0 +1,984 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.FileReader;
+import java.io.StringReader;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.spi.ToolProvider;
+
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.BuildFileRule;
+
+/**
+ * Tests the {@link Link} task.
+ */
+public class LinkTest {
+ /*
+ * TODO:
+ * Test --order-resources (how?)
+ * Test --exclude-files (what does this actually do?)
+ * Test --endian (how?)
+ * Test --vm (how?)
+ */
+
+ @Rule
+ public final BuildFileRule buildRule = new BuildFileRule();
+
+ @Rule
+ public final ExpectedException expected = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ buildRule.configureProject("src/etc/testcases/taskdefs/link.xml");
+ buildRule.executeTarget("setUp");
+ }
+
+ private static boolean isWindows() {
+ return System.getProperty("os.name").contains("Windows");
+ }
+
+ private static boolean isEarlierThan(final Instant time,
+ final Path path) {
+ try {
+ return Files.getLastModifiedTime(path).toInstant().isBefore(time);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static class ImageStructure {
+ final File root;
+ final File bin;
+ final File java;
+
+ ImageStructure(final File root) {
+ this.root = root;
+
+ bin = new File(root, "bin");
+ java = new File(bin, isWindows() ? "java.exe" : "java");
+ }
+ }
+
+ private ImageStructure verifyImageBuiltNormally() {
+ ImageStructure image = new ImageStructure(
+ new File(buildRule.getProject().getProperty("image")));
+
+ Assert.assertTrue("Checking that image was successfully created.",
+ image.root.exists());
+
+ Assert.assertTrue("Checking that image has java executable.",
+ image.java.exists());
+
+ return image;
+ }
+
+ @Test
+ public void testModulepath() {
+ buildRule.executeTarget("modulepath");
+ verifyImageBuiltNormally();
+ }
+
+ @Test
+ public void testImageNotRecreatedFromStaleJmods()
+ throws IOException {
+ buildRule.executeTarget("imageNewerThanJmods");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ Instant future = Instant.now().plus(30, ChronoUnit.MINUTES);
+ try (Stream imageFiles = Files.walk(image.root.toPath())) {
+
+ Assert.assertTrue("Checking that newer image was not written "
+ + "when source files are older.",
+ imageFiles.noneMatch(i -> isEarlierThan(future, i)));
+ }
+ }
+
+ @Test
+ public void testNoModulePath() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("nomodulepath");
+ }
+
+ @Test
+ public void testNoModules() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("nomodules");
+ }
+
+ @Test
+ public void testModulePathRef() {
+ buildRule.executeTarget("modulepathref");
+ verifyImageBuiltNormally();
+ }
+
+ @Test
+ public void testNestedModulePath() {
+ buildRule.executeTarget("modulepath-nested");
+ verifyImageBuiltNormally();
+ }
+
+ @Test
+ public void testModulePathInAttributeAndNested() {
+ buildRule.executeTarget("modulepath-both");
+ verifyImageBuiltNormally();
+ }
+
+ @Test
+ public void testNestedModules()
+ throws IOException,
+ InterruptedException {
+
+ buildRule.executeTarget("modules-nested");
+
+ ImageStructure image = verifyImageBuiltNormally();
+
+ ProcessBuilder builder = new ProcessBuilder(
+ image.java.toString(),
+ buildRule.getProject().getProperty("hello.main-class"));
+ builder.inheritIO();
+ int exitCode = builder.start().waitFor();
+ Assert.assertEquals(
+ "Checking that execution of first module succeeded.", 0, exitCode);
+
+ builder.command(
+ image.java.toString(),
+ buildRule.getProject().getProperty("smile.main-class"));
+ exitCode = builder.start().waitFor();
+ Assert.assertEquals(
+ "Checking that execution of second module succeeded.", 0, exitCode);
+ }
+
+ @Test
+ public void testNestedModuleMissingName() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("modules-nested-missing-name");
+ }
+
+ @Test
+ public void testModulesInAttributeAndNested() {
+ buildRule.executeTarget("modules-both");
+ verifyImageBuiltNormally();
+ }
+
+ @Test
+ public void testObservableModules() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("observable");
+ }
+
+ @Test
+ public void testNestedObservableModules() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("observable-nested");
+ }
+
+ @Test
+ public void testNestedObservableModuleMissingName() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("observable-nested-missing-name");
+ }
+
+ @Test
+ public void testObservableModulesInAttributeAndNested() {
+ buildRule.executeTarget("observable-both");
+ verifyImageBuiltNormally();
+ }
+
+ private void verifyLaunchersExist() {
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File launcher1 =
+ new File(image.bin, isWindows() ? "Hello.bat" : "Hello");
+ Assert.assertTrue("Checking that image has 'Hello' launcher.",
+ launcher1.exists());
+
+ File launcher2 =
+ new File(image.bin, isWindows() ? "Smile.bat" : "Smile");
+ Assert.assertTrue("Checking that image has 'Smile' launcher.",
+ launcher2.exists());
+ }
+
+ @Test
+ public void testLaunchers() {
+ buildRule.executeTarget("launchers");
+ verifyLaunchersExist();
+ }
+
+ @Test
+ public void testNestedLaunchers() {
+ buildRule.executeTarget("launchers-nested");
+ verifyLaunchersExist();
+ }
+
+ @Test
+ public void testNestedLauncherMissingName() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("launchers-nested-missing-name");
+ }
+
+ @Test
+ public void testNestedLauncherMissingModule() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("launchers-nested-missing-module");
+ }
+
+ @Test
+ public void testLaunchersInAttributeAndNested() {
+ buildRule.executeTarget("launchers-both");
+ verifyLaunchersExist();
+ }
+
+ private void verifyLocales()
+ throws IOException,
+ InterruptedException {
+
+ ImageStructure image = verifyImageBuiltNormally();
+
+ String mainClass =
+ buildRule.getProject().getProperty("localefinder.main-class");
+ Assert.assertNotNull("Checking that main-class property exists",
+ mainClass);
+
+ ProcessBuilder builder =
+ new ProcessBuilder(image.java.toString(), mainClass, "zh", "in");
+ builder.inheritIO();
+ int exitCode = builder.start().waitFor();
+
+ Assert.assertEquals("Verifying that image has access to locales "
+ + "specified during linking.", 0, exitCode);
+
+ builder.command(image.java.toString(), mainClass, "ja");
+ exitCode = builder.start().waitFor();
+
+ Assert.assertNotEquals(
+ "Verifying that image does not have access to locales "
+ + "not specified during linking.", 0, exitCode);
+ }
+
+ @Test
+ public void testLocales()
+ throws IOException,
+ InterruptedException {
+
+ buildRule.executeTarget("locales");
+ verifyLocales();
+ }
+
+ @Test
+ public void testNestedLocales()
+ throws IOException,
+ InterruptedException {
+
+ buildRule.executeTarget("locales-nested");
+ verifyLocales();
+ }
+
+ @Test
+ public void testNestedLocaleMissingName() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("locales-nested-missing-name");
+ }
+
+ @Test
+ public void testLocalesInAttributeAndNested()
+ throws IOException,
+ InterruptedException {
+
+ buildRule.executeTarget("locales-both");
+ verifyLocales();
+ }
+
+ @Test
+ public void testExcludeResources()
+ throws IOException {
+ buildRule.executeTarget("excluderesources");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ String mainClass =
+ buildRule.getProject().getProperty("hello.main-class");
+ Assert.assertNotNull("Checking that main-class property exists",
+ mainClass);
+
+ ProcessBuilder builder =
+ new ProcessBuilder(image.java.toString(), mainClass,
+ "resource1.txt", "resource2.txt");
+ builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
+ builder.redirectErrorStream(true);
+
+ Collection outputLines;
+ Process process = builder.start();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+
+ outputLines = reader.lines().collect(Collectors.toList());
+ }
+
+ Assert.assertTrue(
+ "Checking that excluded resource is actually excluded.",
+ outputLines.stream().anyMatch(
+ l -> l.endsWith("resource1.txt absent")));
+
+ Assert.assertTrue(
+ "Checking that resource not excluded is present.",
+ outputLines.stream().anyMatch(
+ l -> l.endsWith("resource2.txt present")));
+ }
+
+ @Test
+ public void testNestedExcludeResources()
+ throws IOException {
+ buildRule.executeTarget("excluderesources-nested");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ String mainClass =
+ buildRule.getProject().getProperty("hello.main-class");
+ Assert.assertNotNull("Checking that main-class property exists",
+ mainClass);
+
+ ProcessBuilder builder =
+ new ProcessBuilder(image.java.toString(), mainClass,
+ "resource1.txt", "resource2.txt");
+ builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
+ builder.redirectErrorStream(true);
+
+ Collection outputLines;
+ Process process = builder.start();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+
+ outputLines = reader.lines().collect(Collectors.toList());
+ }
+
+ Assert.assertTrue(
+ "Checking that excluded resource is actually excluded.",
+ outputLines.stream().anyMatch(
+ l -> l.endsWith("resource1.txt absent")));
+
+ Assert.assertTrue(
+ "Checking that resource not excluded is present.",
+ outputLines.stream().anyMatch(
+ l -> l.endsWith("resource2.txt present")));
+ }
+
+ @Test
+ public void testNestedExcludeResourcesFile()
+ throws IOException {
+ buildRule.executeTarget("excluderesources-nested-file");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ String mainClass =
+ buildRule.getProject().getProperty("hello.main-class");
+ Assert.assertNotNull("Checking that main-class property exists",
+ mainClass);
+
+ ProcessBuilder builder =
+ new ProcessBuilder(image.java.toString(), mainClass,
+ "resource1.txt", "resource2.txt");
+ builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
+ builder.redirectErrorStream(true);
+
+ Collection outputLines;
+ Process process = builder.start();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+
+ outputLines = reader.lines().collect(Collectors.toList());
+ }
+
+ Assert.assertTrue(
+ "Checking that excluded resource is actually excluded.",
+ outputLines.stream().anyMatch(
+ l -> l.endsWith("resource1.txt absent")));
+
+ Assert.assertTrue(
+ "Checking that resource not excluded is present.",
+ outputLines.stream().anyMatch(
+ l -> l.endsWith("resource2.txt present")));
+ }
+
+ @Test
+ public void testNestedExcludeResourcesNoAttributes() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("excluderesources-nested-no-attr");
+ }
+
+ @Test
+ public void testNestedExcludeResourcesFileAndPattern() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("excluderesources-nested-both");
+ }
+
+ @Test
+ public void testExcludeResourcesAttributeAndNested()
+ throws IOException {
+ buildRule.executeTarget("excluderesources-both");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ String mainClass =
+ buildRule.getProject().getProperty("hello.main-class");
+ Assert.assertNotNull("Checking that main-class property exists",
+ mainClass);
+
+ ProcessBuilder builder =
+ new ProcessBuilder(image.java.toString(), mainClass,
+ "resource1.txt", "resource2.txt");
+ builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
+ builder.redirectErrorStream(true);
+
+ Collection outputLines;
+ Process process = builder.start();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+
+ outputLines = reader.lines().collect(Collectors.toList());
+ }
+
+ Assert.assertTrue(
+ "Checking that first excluded resource is actually excluded.",
+ outputLines.stream().anyMatch(
+ l -> l.endsWith("resource1.txt absent")));
+
+ Assert.assertTrue(
+ "Checking that second excluded resource is actually excluded.",
+ outputLines.stream().anyMatch(
+ l -> l.endsWith("resource2.txt absent")));
+ }
+
+ @Test
+ public void testExcludeFiles()
+ throws IOException {
+ buildRule.executeTarget("excludefiles");
+ verifyImageBuiltNormally();
+ // TODO: Test created image (what does --exclude-files actually do?)
+ }
+
+ @Test
+ public void testNestedExcludeFiles()
+ throws IOException {
+ buildRule.executeTarget("excludefiles-nested");
+ verifyImageBuiltNormally();
+ // TODO: Test created image (what does --exclude-files actually do?)
+ }
+
+ @Test
+ public void testNestedExcludeFilesFile()
+ throws IOException {
+ buildRule.executeTarget("excludefiles-nested-file");
+ ImageStructure image = verifyImageBuiltNormally();
+ // TODO: Test created image (what does --exclude-files actually do?)
+ }
+
+ @Test
+ public void testNestedExcludeFilesNoAttributes() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("excludefiles-nested-no-attr");
+ }
+
+ @Test
+ public void testNestedExcludeFilesFileAndPattern() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("excludefiles-nested-both");
+ }
+
+ @Test
+ public void testExcludeFilesAttributeAndNested()
+ throws IOException {
+ buildRule.executeTarget("excludefiles-both");
+ verifyImageBuiltNormally();
+ // TODO: Test created image (what does --exclude-files actually do?)
+ }
+
+ @Test
+ public void testOrdering()
+ throws IOException {
+ buildRule.executeTarget("ordering");
+ verifyImageBuiltNormally();
+ // TODO: Test resource order in created image (how?)
+ }
+
+ @Test
+ public void testNestedOrdering()
+ throws IOException {
+ buildRule.executeTarget("ordering-nested");
+ verifyImageBuiltNormally();
+ // TODO: Test resource order in created image (how?)
+ }
+
+ @Test
+ public void testNestedOrderingListFile()
+ throws IOException {
+ buildRule.executeTarget("ordering-nested-file");
+ ImageStructure image = verifyImageBuiltNormally();
+ // TODO: Test resource order in created image (how?)
+ }
+
+ @Test
+ public void testNestedOrderingNoAttributes() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("ordering-nested-no-attr");
+ }
+
+ @Test
+ public void testNestedOrderingFileAndPattern() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("ordering-nested-both");
+ }
+
+ @Test
+ public void testOrderingAttributeAndNested()
+ throws IOException {
+ buildRule.executeTarget("ordering-both");
+ verifyImageBuiltNormally();
+ // TODO: Test resource order in created image (how?)
+ }
+
+ @Test
+ public void testIncludeHeaders() {
+ buildRule.executeTarget("includeheaders");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File[] headers = new File(image.root, "include").listFiles();
+ Assert.assertTrue("Checking that include files were omitted.",
+ headers == null || headers.length == 0);
+ }
+
+ @Test
+ public void testIncludeManPages() {
+ buildRule.executeTarget("includemanpages");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File[] manPages = new File(image.root, "man").listFiles();
+ Assert.assertTrue("Checking that man pages were omitted.",
+ manPages == null || manPages.length == 0);
+ }
+
+ @Test
+ public void testIncludeNativeCommands() {
+ buildRule.executeTarget("includenativecommands");
+ ImageStructure image = new ImageStructure(
+ new File(buildRule.getProject().getProperty("image")));
+
+ Assert.assertTrue("Checking that image was successfully created.",
+ image.root.exists());
+
+ Assert.assertFalse(
+ "Checking that image was stripped of java executable.",
+ image.java.exists());
+ }
+
+ private long totalSizeOf(final Path path)
+ throws IOException {
+ if (Files.isDirectory(path)) {
+ long size = 0;
+ try (DirectoryStream children = Files.newDirectoryStream(path)) {
+ for (Path child : children) {
+ size += totalSizeOf(child);
+ }
+ }
+ return size;
+ }
+
+ if (Files.isRegularFile(path)) {
+ return Files.size(path);
+ }
+
+ return 0;
+ }
+
+ @Test
+ public void testCompression()
+ throws IOException {
+ buildRule.executeTarget("compression");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File compressedImageRoot =
+ new File(buildRule.getProject().getProperty("compressed-image"));
+
+ long size = totalSizeOf(image.root.toPath());
+ long compressedSize = totalSizeOf(compressedImageRoot.toPath());
+
+ Assert.assertTrue("Checking that compression resulted in smaller image.",
+ compressedSize < size);
+ }
+
+ @Test
+ public void testNestedCompression()
+ throws IOException {
+ buildRule.executeTarget("compression-nested");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File compressedImageRoot =
+ new File(buildRule.getProject().getProperty("compressed-image"));
+
+ long size = totalSizeOf(image.root.toPath());
+ long compressedSize = totalSizeOf(compressedImageRoot.toPath());
+
+ Assert.assertTrue("Checking that compression resulted in smaller image.",
+ compressedSize < size);
+ }
+
+ @Test
+ public void testNestedCompressionNoAttributes() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("compression-nested-no-attr");
+ }
+
+ @Test
+ public void testNestedCompressionAttributeAndNested() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("compression-both");
+ }
+
+ @Test
+ public void testEndian() {
+ buildRule.executeTarget("endian");
+ verifyImageBuiltNormally();
+ // TODO: How can we test the created image? Which files does --endian
+ // affect?
+ }
+
+ @Test
+ public void testVMType() {
+ buildRule.executeTarget("vm");
+ verifyImageBuiltNormally();
+ // TODO: How can we test the created image? Which files does --vm
+ // affect?
+ }
+
+ @Test
+ public void testReleaseInfoFile()
+ throws IOException {
+ buildRule.executeTarget("releaseinfo-file");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File release = new File(image.root, "release");
+ try (BufferedReader reader =
+ Files.newBufferedReader(release.toPath())) {
+
+ Assert.assertTrue("Checking for 'test=true' in image release info.",
+ reader.lines().anyMatch(l -> l.equals("test=true")));
+ }
+ }
+
+ @Test
+ public void testReleaseInfoDelete()
+ throws IOException {
+ buildRule.executeTarget("releaseinfo-delete");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File release = new File(image.root, "release");
+ try (BufferedReader reader =
+ Files.newBufferedReader(release.toPath())) {
+
+ Assert.assertFalse("Checking that 'test' was deleted "
+ + "from image release info.",
+ reader.lines().anyMatch(l -> l.startsWith("test=")));
+ }
+ }
+
+ @Test
+ public void testReleaseInfoNestedDelete()
+ throws IOException {
+ buildRule.executeTarget("releaseinfo-nested-delete");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File release = new File(image.root, "release");
+ try (BufferedReader reader =
+ Files.newBufferedReader(release.toPath())) {
+
+ Assert.assertFalse("Checking that 'test' was deleted "
+ + "from image release info.",
+ reader.lines().anyMatch(l -> l.startsWith("test=")));
+ }
+ }
+
+ @Test
+ public void testReleaseInfoNestedDeleteNoKey() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("releaseinfo-nested-delete-no-key");
+ }
+
+ @Test
+ public void testReleaseInfoDeleteAttributeAndNested()
+ throws IOException {
+ buildRule.executeTarget("releaseinfo-nested-delete-both");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File release = new File(image.root, "release");
+ try (BufferedReader reader =
+ Files.newBufferedReader(release.toPath())) {
+
+ Assert.assertTrue(
+ "Checking that 'test' and 'foo' were deleted "
+ + "from image release info.",
+ reader.lines().noneMatch(l ->
+ l.startsWith("test=") || l.startsWith("foo=")));
+ }
+ }
+
+ @Test
+ public void testReleaseInfoAddFile()
+ throws IOException {
+ buildRule.executeTarget("releaseinfo-add-file");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File release = new File(image.root, "release");
+ try (BufferedReader reader = new BufferedReader(
+ new FileReader(release))) {
+
+ Assert.assertTrue("Checking that 'test=s\u00ed' was added "
+ + "to image release info.",
+ reader.lines().anyMatch(l -> l.equals("test=s\u00ed")));
+ }
+ }
+
+ @Test
+ public void testReleaseInfoAddFileWithCharset()
+ throws IOException {
+ buildRule.executeTarget("releaseinfo-add-file-charset");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File release = new File(image.root, "release");
+ // Using FileReader here since 'release' file is in platform's charset.
+ try (BufferedReader reader = new BufferedReader(
+ new FileReader(release))) {
+
+ Assert.assertTrue("Checking that 'test=s\u00ed' was added "
+ + "to image release info.",
+ reader.lines().anyMatch(l -> l.equals("test=s\u00ed")));
+ }
+ }
+
+ @Test
+ public void testReleaseInfoAddKeyAndValue()
+ throws IOException {
+ buildRule.executeTarget("releaseinfo-add-key");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ File release = new File(image.root, "release");
+ try (BufferedReader reader =
+ Files.newBufferedReader(release.toPath())) {
+
+ Assert.assertTrue("Checking that 'test=true' was added "
+ + "to image release info.",
+ reader.lines().anyMatch(l -> l.equals("test=true")));
+ }
+ }
+
+ @Test
+ public void testReleaseInfoAddNoValue() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("releaseinfo-add-no-value");
+ }
+
+ @Test
+ public void testReleaseInfoAddNoKey() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("releaseinfo-add-no-key");
+ }
+
+ @Test
+ public void testReleaseInfoAddFileAndKey() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("releaseinfo-add-file-and-key");
+ }
+
+ @Test
+ public void testReleaseInfoAddFileAndValue() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("releaseinfo-add-file-and-value");
+ }
+
+ @Test
+ public void testDebugStripping()
+ throws IOException,
+ InterruptedException {
+
+ buildRule.executeTarget("debug");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ ProcessBuilder builder = new ProcessBuilder(
+ image.java.toString(),
+ buildRule.getProject().getProperty("thrower.main-class"));
+ builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
+ builder.redirectErrorStream(true);
+
+ Process process = builder.start();
+ try (BufferedReader linesReader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+
+ Assert.assertTrue(
+ "Checking that stack trace contains no debug information.",
+ linesReader.lines().noneMatch(
+ l -> l.matches(".*\\([^)]*:[0-9]+\\)")));
+ }
+ process.waitFor();
+ }
+
+ @Test
+ public void testDeduplicationOfLicenses() {
+ buildRule.executeTarget("dedup");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ String helloModuleName =
+ buildRule.getProject().getProperty("hello.mod");
+ String smileModuleName =
+ buildRule.getProject().getProperty("smile.mod");
+
+ Assert.assertNotNull("Checking that 'hello.mod' property was set.",
+ helloModuleName);
+ Assert.assertNotNull("Checking that 'smile.mod' property was set.",
+ smileModuleName);
+
+ Assume.assumeFalse("Checking that this operating system"
+ + " supports symbolic links as a means of license de-duplication.",
+ System.getProperty("os.name").contains("Windows"));
+
+ Path legal = image.root.toPath().resolve("legal");
+
+ Path[] licenses = {
+ legal.resolve(helloModuleName).resolve("USELESSLICENSE"),
+ legal.resolve(smileModuleName).resolve("USELESSLICENSE"),
+ };
+
+ int nonLinkCount = 0;
+ for (Path license : licenses) {
+ if (!Files.isSymbolicLink(license)) {
+ nonLinkCount++;
+ }
+ }
+
+ Assert.assertEquals(
+ "Checking that USELESSLICENSE only exists once in image "
+ + "and all other instances are links to it.",
+ 1, nonLinkCount);
+ }
+
+ @Test
+ public void testIgnoreSigning() {
+ buildRule.executeTarget("ignoresigning");
+ verifyImageBuiltNormally();
+ }
+
+ /**
+ * Should fail due to jlink rejecting identically named files whose
+ * contents are different.
+ */
+ @Test
+ public void testDeduplicationOfInconsistentLicenses() {
+ expected.expect(BuildException.class);
+ buildRule.executeTarget("dedup-identical");
+ }
+
+ @Test
+ public void testBindingOfServices()
+ throws IOException,
+ InterruptedException {
+ buildRule.executeTarget("bindservices");
+ ImageStructure image = verifyImageBuiltNormally();
+
+ String mainClass = buildRule.getProject().getProperty("inc.main-class");
+
+ ProcessBuilder builder = new ProcessBuilder(
+ image.java.toString(), mainClass);
+ builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
+ builder.redirectError(ProcessBuilder.Redirect.INHERIT);
+
+ Process process = builder.start();
+ try (BufferedReader linesReader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+
+ Assert.assertEquals(
+ "Checking that bindServices=false results in no providers in image.",
+ 0, linesReader.lines().count());
+ }
+ process.waitFor();
+
+ image = new ImageStructure(
+ new File(buildRule.getProject().getProperty("image2")));
+
+ Assert.assertTrue("Checking that image2 was successfully created.",
+ image.root.exists());
+ Assert.assertTrue("Checking that image2 has java executable.",
+ image.java.exists());
+
+ builder = new ProcessBuilder(image.java.toString(), mainClass);
+ builder.redirectInput(ProcessBuilder.Redirect.INHERIT);
+ builder.redirectError(ProcessBuilder.Redirect.INHERIT);
+
+ process = builder.start();
+ try (BufferedReader linesReader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+
+ Assert.assertEquals(
+ "Checking that bindServices=true results in image with provider.",
+ 5, linesReader.lines().count());
+ }
+ process.waitFor();
+ }
+
+ private String runJlink(final String... args) {
+ ToolProvider jlink = ToolProvider.findFirst("jlink").orElseThrow(
+ () -> new RuntimeException("jlink tool not found in JDK."));
+
+ 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);
+ }
+
+ if (exitCode != 0) {
+ throw new RuntimeException(
+ "jlink failed, output is: " + stdout + ", error is: " + stderr);
+ }
+
+ return stdout.toString();
+ }
+}
diff --git a/src/tests/junit/org/apache/tools/ant/types/ModuleVersionTest.java b/src/tests/junit/org/apache/tools/ant/types/ModuleVersionTest.java
new file mode 100644
index 000000000..b3cf9042a
--- /dev/null
+++ b/src/tests/junit/org/apache/tools/ant/types/ModuleVersionTest.java
@@ -0,0 +1,115 @@
+package org.apache.tools.ant.types;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+/**
+ * Tests {@link ModuleVersion} class.
+ */
+public class ModuleVersionTest {
+ @Rule
+ public final ExpectedException expected = ExpectedException.none();
+
+ @Test
+ public void testModuleVersionStringNumberPreBuild() {
+ ModuleVersion moduleVersion = new ModuleVersion();
+
+ moduleVersion.setNumber("1.1.3");
+ moduleVersion.setPreRelease("ea");
+ moduleVersion.setBuild("25");
+
+ String versionStr = moduleVersion.toModuleVersionString();
+
+ Assert.assertNotNull("Checking for non-null module version string.",
+ versionStr);
+ Assert.assertTrue("Checking for correct module version string.",
+ versionStr.matches("1\\.1\\.3[-+]ea\\+25"));
+ }
+
+ @Test
+ public void testModuleVersionStringNumberPre() {
+ ModuleVersion moduleVersion = new ModuleVersion();
+
+ moduleVersion.setNumber("1.1.3");
+ moduleVersion.setPreRelease("ea");
+
+ String versionStr = moduleVersion.toModuleVersionString();
+
+ Assert.assertNotNull("Checking for non-null module version string.",
+ versionStr);
+ Assert.assertTrue("Checking for correct module version string.",
+ versionStr.matches("1\\.1\\.3[-+]ea"));
+ }
+
+ @Test
+ public void testModuleVersionStringNumberBuild() {
+ ModuleVersion moduleVersion = new ModuleVersion();
+
+ moduleVersion.setNumber("1.1.3");
+ moduleVersion.setBuild("25");
+
+ String versionStr = moduleVersion.toModuleVersionString();
+
+ Assert.assertNotNull("Checking for non-null module version string.",
+ versionStr);
+ Assert.assertTrue("Checking for correct module version string.",
+ versionStr.matches("1\\.1\\.3[-+]\\+25"));
+ }
+
+ @Test
+ public void testModuleVersionStringNumberOnly() {
+ ModuleVersion moduleVersion = new ModuleVersion();
+
+ moduleVersion.setNumber("1.1.3");
+
+ String versionStr = moduleVersion.toModuleVersionString();
+
+ Assert.assertNotNull("Checking for non-null module version string.",
+ versionStr);
+ Assert.assertEquals("Checking for correct module version string.",
+ "1.1.3", versionStr);
+ }
+
+ @Test
+ public void testModuleVersionStringNullNumber() {
+ expected.expect(IllegalStateException.class);
+
+ ModuleVersion moduleVersion = new ModuleVersion();
+ moduleVersion.toModuleVersionString();
+ }
+
+ @Test
+ public void testNullNumber() {
+ expected.expect(NullPointerException.class);
+
+ ModuleVersion moduleVersion = new ModuleVersion();
+ moduleVersion.setNumber(null);
+ }
+
+ @Test
+ public void testInvalidNumber() {
+ expected.expect(IllegalArgumentException.class);
+
+ ModuleVersion moduleVersion = new ModuleVersion();
+ moduleVersion.setNumber("1-1-3");
+ }
+
+ @Test
+ public void testInvalidNumber2() {
+ expected.expect(IllegalArgumentException.class);
+
+ ModuleVersion moduleVersion = new ModuleVersion();
+ moduleVersion.setNumber("1.1+3");
+ }
+
+ @Test
+ public void testInvalidPreRelease() {
+ expected.expect(IllegalArgumentException.class);
+
+ ModuleVersion moduleVersion = new ModuleVersion();
+ moduleVersion.setNumber("1.1.3");
+ moduleVersion.setPreRelease("ea+interim");
+ }
+}