From 343dff90f2a06a14a5e3d8f787de87d4446af3d9 Mon Sep 17 00:00:00 2001 From: VGR Date: Thu, 6 Dec 2018 23:32:57 -0500 Subject: [PATCH] Added tasks for JDK's jmod and jlink tools. --- build.xml | 6 + manual/Tasks/jlink.html | 7 +- manual/Tasks/jmod.html | 330 +++ manual/Tasks/link.html | 579 +++++ manual/tasklist.html | 2 + src/etc/testcases/taskdefs/jar.xml | 8 +- src/etc/testcases/taskdefs/jmod.xml | 992 ++++++++ src/etc/testcases/taskdefs/link.xml | 1088 +++++++++ .../tools/ant/taskdefs/defaults.properties | 2 + .../tools/ant/taskdefs/modules/Jmod.java | 1282 ++++++++++ .../tools/ant/taskdefs/modules/Link.java | 2120 +++++++++++++++++ .../ant/taskdefs/modules/package-info.java | 23 + .../taskdefs/optional/jlink/JlinkTask.java | 6 +- .../apache/tools/ant/types/ModuleVersion.java | 147 ++ .../tools/ant/taskdefs/modules/JmodTest.java | 690 ++++++ .../tools/ant/taskdefs/modules/LinkTest.java | 984 ++++++++ .../tools/ant/types/ModuleVersionTest.java | 115 + 17 files changed, 8374 insertions(+), 7 deletions(-) create mode 100644 manual/Tasks/jmod.html create mode 100644 manual/Tasks/link.html create mode 100644 src/etc/testcases/taskdefs/jmod.xml create mode 100644 src/etc/testcases/taskdefs/link.xml create mode 100644 src/main/org/apache/tools/ant/taskdefs/modules/Jmod.java create mode 100644 src/main/org/apache/tools/ant/taskdefs/modules/Link.java create mode 100644 src/main/org/apache/tools/ant/taskdefs/modules/package-info.java create mode 100644 src/main/org/apache/tools/ant/types/ModuleVersion.java create mode 100644 src/tests/junit/org/apache/tools/ant/taskdefs/modules/JmodTest.java create mode 100644 src/tests/junit/org/apache/tools/ant/taskdefs/modules/LinkTest.java create mode 100644 src/tests/junit/org/apache/tools/ant/types/ModuleVersionTest.java diff --git a/build.xml b/build.xml index b34bbca8e..aebcb4ab8 100644 --- a/build.xml +++ b/build.xml @@ -42,6 +42,7 @@ + @@ -178,6 +179,10 @@ =================================================================== --> + + + + @@ -609,6 +614,7 @@ + diff --git a/manual/Tasks/jlink.html b/manual/Tasks/jlink.html index 76978fb2c..2c3b49c6e 100644 --- a/manual/Tasks/jlink.html +++ b/manual/Tasks/jlink.html @@ -26,9 +26,14 @@

This task has been deprecated. Use a zipfileset or zipgroupfileset with the Jar task or Zip task -instead.

+instead. For a task based on the JDK's jlink tool, see +Link.

Description

+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeDescriptionRequired
destFilejmod file to create.Yes
classpathFiles to be placed in the jmod file. Usually a single module.One of these is required, unless a nested + <classpath> is present.
classpathrefFiles to be placed in the jmod file, given as a + reference + to a path defined elsewhere.
modulepathLocations of modules on which classpath modules depend.No
modulepathrefLocations of modules on which classpath modules depend, + given as a reference + to a path defined elsewhere.No
commandpathDirectories containing native commands to include in jmod.No
commandpathrefDirectories containing native commands to include in jmod, + given as a reference + to a path defined elsewhere.No
headerpathDirectories containing header files to include in jmod.No
headerpathrefDirectories containing header files to include in jmod, + given as a reference + to a path defined elsewhere.No
configpathDirectories containing user-editable configuration files + to include in jmod.No
configpathrefDirectories containing user-editable configuration files + to include in jmod, + given as a reference + to a path defined elsewhere.No
legalpathDirectories containing legal licenses and notices to include in jmod.No
legalpathrefDirectories containing legal licenses and notices to include in jmod, + given as a reference + to a path defined elsewhere.No
nativelibpathDirectories containing native libraries to include in jmod.No
nativelibpathrefDirectories containing native libraries to include in jmod, + given as a reference + to a path defined elsewhere.No
manpathDirectories containing man pages to include in jmod.No
manpathrefDirectories containing man pages to include in jmod, + given as a reference + to a path defined elsewhere.No
versionModule version of jmod.No
mainclassClass that acts as executable entry point of module.No
platformThe 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 platformNo
hashModulesPatternRegular 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
resolveByDefaultBoolean 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.
moduleWarningsWhether 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
+
+
No, default is no warnings.
+ +

Parameters specified as nested elements

+

classpath, modulepath, commandpath, headerpath, configpath, legalpath, nativelibpath, manpath

+

The + classpath, + modulepath, + commandpath, + headerpath, + configpath, + legalpath, + nativelibpath, and + manpath + attributes are path-like structures + and can also be set via nested + <classpath>, + <modulepath>, + <commandpath>, + <headerpath>, + <configpath>, + <legalpath>, + <nativelibpath>, and + <manpath> + elements, respectively.

+ +

version

+

Fine-grained alternative to the version attribute. This +nested element has these attributes:

+ + + + + + + + + + + + + + + + + + + + +
AttributeDescriptionRequired
numberPrimary version number. Can be any text, as long as it does not + contain a hyphen (-) or plus (+).Yes
preReleasePre-release version. Can be any text, as long as it does not + contain a plus (+).No
buildBuild version. Can be any text. + No
+ +

See the ModuleDescriptor.Version documentation +for a full description of the meaning of each version component.

+ +

moduleWarning

+

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:

+ + + + + + + + + + +
AttributeDescriptionRequired
reasonCondition 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
+
+
Yes
+ +

Examples

+ +

Basic jmod

+

Create a jmod from a single modular jar file:

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"/>
+
+ +

With dependencies

+

Create a jmod from a modular jar file which depends on another module:

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar">
+    <modulepath>
+        <pathelement location="libs/thirdpartyutils.jar"/>
+    </modulepath>
+</jmod>
+
+ +

With version

+

Create a jmod with a module version:

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"
+      version="1.2.1-ea+29"/>
+
+ +

Create a versioned jmod from module version components:

+
+<property name="version" value="1.2.1"/>
+<buildnumber/>
+<loadfile property="buildnum" srcFile="build.number"/>
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar">
+    <version number="${version}" build="${buildnum}"/>
+</jmod>
+
+ +

Main class

+

Create a jmod with a main class:

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"
+      mainclass="com.example.myapp.MainWindow"/>
+
+ +

Target platform

+

Create a jmod for a specific platform, possibly different from the +current platform:

+ +
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"
+      platform="windows-amd64"/>
+
+ + + diff --git a/manual/Tasks/link.html b/manual/Tasks/link.html new file mode 100644 index 000000000..078acc172 --- /dev/null +++ b/manual/Tasks/link.html @@ -0,0 +1,579 @@ + + + + + + +Link Task + + + + + +

Description

+

Assembles jmod files into an executable image. Equivalent to the JDK's +jlink +tool. +

+

Requires Java 9 or later.

+ +

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeDescriptionRequired
destDirRoot directory of created image.Yes
modulepathPath-like sequence of jmod files to link in order to create image.One of these is required, unless a nested + <modulepath> is present.
modulepathrefPath-like sequence of jmod files to link in order to + create image, given as a reference + to a path defined elsewhere.
modulesComma-separated list of modules to place in the linked image.Yes, unless one or more nested <module> elements + are present.
observableModulesComma-separated list of explicit modules that comprise + "universe" visible to link tool while linking.No
launchersComma-separated list of commands, each of the form + name=module or + name=module/mainclassNo
localesComma-separated list of extra locales, or wildcard patterns matching + multiple locale names, to include. + Requires jdk.localedata module.No
excludeResourcesComma-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
excludeFilesComma-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
resourceOrderComma-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
bindServicesBoolean, 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
ignoreSigningBoolean, whether to allow signed jar files. + (Note: As of Java 11, this is ignored and is always treated as true.)No, default is false
includeHeadersBoolean, whether to include header files in linked image.No, default is true
includeManPagesBoolean, whether to include man pages in linked image.No, default is true
includeNativeCommandsBoolean, whether to include native executables in linked image.No, default is true
debugBoolean, whether to include debug information.No, default is true
verboseLevelIf 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
compressCompression 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
endiannessByte order of linked image, must be little or big + No, default is native byte order
checkDuplicateLegalBoolean. 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.
vmTypeHotspot VM in image. One of: +
    +
  • client +
  • server +
  • minimal +
  • all +
+
No, default is all
+ +

Parameters specified as nested elements

+ +

<link> can have the following nested elements:

+ + +

modulepath

+

Path-like structure pointing to + jmod files to link into image.

+ +

module

+

Names a single module to be placed in the linked image. This may be +specified multiple times.

+

Attributes:

+ + + + + + + + + + + +
AttributeDescriptionRequired
nameName of module to add.Yes
+ +

observableModule

+

Names a module visible to the linking process, instead of every module +in the module path being considered. This may be specified multiple times. +

Attributes:

+ + + + + + + + + + + +
AttributeDescriptionRequired
nameName of module to add to list of observable modules.Yes
+ +

launcher

+

Specifies an executable file which will be added to the linked image, +which executes a particular module's main class. Attributes:

+ + + + + + + + + + + + + + + + + + + + + +
AttributeDescriptionRequired
nameName of launcher. This typically is used for the name of the + executable file.Yes
moduleName of module to execute.Yes
mainClassName of entry point class in module to execute.Required unless module has its own main class defined.
+ +

locale

+

Specifies locales to include in linked image. May be specified multiple +times. Requires jdk.localedata module. Attributes: + + + + + + + + + + + +
AttributeDescriptionRequired
nameName of locale, or wildcard pattern with * + that matches multiple locale names.Yes
+ +

resourceOrder

+

Explicit resource search order in linked image. May be specified multiple +times. Attributes:

+ + + + + + + + + + + + + + + +
AttributeDescriptionRequired
patternA standard PathMatcher pattern + for matching resourcesExactly one of these
listFileText file containing list of resource names (not patterns), + one per line
+ +

If the resourceOrder attribute is also present on the task, its +patterns are treated as if they occur before patterns in nested + elements.

+ +

excludeResources

+

Excludes files from linked image tree. May be specified multiple times. + Attributes:

+ + + + + + + + + + + + + + + +
AttributeDescriptionRequired
patternA standard PathMatcher pattern + for matching resourcesExactly one of these
listFileText file containing list of resource names (not patterns), + one per line
+ +

excludeFiles

+

Excludes files from linked image. May be specified multiple times. + Attributes:

+ + + + + + + + + + + + + + + +
AttributeDescriptionRequired
patternA standard PathMatcher pattern + for matching filesExactly one of these
listFileText file containing list of file names (not patterns), + one per line
+ +

compress

+

Describes how image should be compressed. Attributes:

+ + + + + + + + + + + + + + + + +
AttributeDescriptionRequired
levelCompression level of linked image. One of: +
+
0 or + none
+
no compression (default)
+
1 or + strings
+
constant string sharing
+
2 or + zip
+
zip compression
+
+
Yes
filesComma-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:

+ + + + + + + + + + + + + + + +
AttributeDescriptionRequired
patternA standard PathMatcher pattern + for matching filesExactly one of these
listFileText file containing list of file names (not patterns), + one per line
+ +

releaseInfo

+

Replaces, augments, or trims the image's release info properties. +Can be specified multiple times. Attributes: + + + + + + + + + + + + + + + +
AttributeDescriptionRequired
fileJava properties file containing new release info properties + that will entirely replace the current ones.No
deleteComma-separated property keys to remove from application's + release info + No
+ +

<releaseInfo> can also have any number of these nested elements:

+
add
+

Specifies additional release info properties. Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeDescriptionRequired
keyKey of single property to add.Yes, unless file is specified
valueValue of single property to add.
fileJava property file containing any number of properties to add.Yes, unless key and value are specified
charsetCharacter set of property file.No, default is ISO_8859_1, in accordance with + java.util.Properties class.
+ +
delete
+

Property keys to remove from applicaiton's release info. Attributes:

+ + + + + + + + + + + +
AttributeDescriptionRequired
keyKey of property to remove.Yes
+ +

Examples

+

Basic linking

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"/>
+<link destDir="build/image" modulepath="MyApp.jmod"
+      modules="com.example.myapp"/>
+
+ +

Custom binaries

+

This will cause a bin/MyEditor script to appear in the +image: +

+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"/>
+<link destDir="build/image" modulepath="MyApp.jmod"
+      modules="com.example.myapp"
+      launchers="MyEditor=com.example.myapp/com.example.myapp.editors.EditorMain"/>
+
+ +

Same thing, using a nested launcher element:

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"/>
+<link destDir="build/image" modulepath="MyApp.jmod"
+      modules="com.example.myapp">
+
+    <launcher name="MyEditor" module="com.example.myapp"
+              mainClass="com.example.myapp.editors.EditorMain"/>
+
+</link>
+
+ +

Limiting locales

+

Include just the locales needed by the application from the jdk.localedata module:

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"/>
+<link destDir="build/image" modulepath="MyApp.jmod"
+      modules="com.example.myapp,jdk.localedata"
+      locales="zh,jp-*"/>
+
+ +

Compressed image

+

Compress entire image:

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"/>
+<link destDir="build/image" modulepath="MyApp.jmod"
+      modules="com.example.myapp,jdk.localedata"
+      compress="zip"/>
+
+ +

Compress only some files in the image:

+
+<jmod destfile="MyApp.jmod" classpath="build/myapp.jar"/>
+<link destDir="build/image" modulepath="MyApp.jmod"
+      modules="com.example.myapp,jdk.localedata">
+
+    <compress level="zip" files=".*\.xml"/>
+
+</link>
+
+ +

Cross-compiling

+

To create an image for a different platform: + +

    +
  • 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: +
    +jmod describe "$FOREIGN_JDK_HOME"/jmods/java.base.jmod | grep '^platform'
    +
    +
  • +
  • Create your jmod using the foreign JDK's platform string: +
    +<jmod destfile="MyApp.jmod" classpath="build/myapp.jar" platform="windows-amd64"/>
    +
    +
  • +
  • Link with the foreign JDK's jmods directory in the module path: +
    +<link destDir="build/image"
    +      modulepath="MyApp.jmod;${foreign-jdk-home}/jmods"
    +      modules="com.example.myapp"/>
    +
    +
  • +
+
    + + + diff --git a/manual/tasklist.html b/manual/tasklist.html index 5325e3897..8194bdb32 100644 --- a/manual/tasklist.html +++ b/manual/tasklist.html @@ -108,11 +108,13 @@
  • JJDoc
  • JJTree
  • Jlink
  • +
  • Jmod
  • JspC
  • JUnit (3 & 4)
  • JUnitLauncher (JUnit 5)
  • JUnitReport
  • Length
  • +
  • Link
  • LoadFile
  • LoadProperties
  • LoadResource
  • diff --git a/src/etc/testcases/taskdefs/jar.xml b/src/etc/testcases/taskdefs/jar.xml index 78d1abc68..cb686f780 100644 --- a/src/etc/testcases/taskdefs/jar.xml +++ b/src/etc/testcases/taskdefs/jar.xml @@ -121,7 +121,7 @@ destfile="${tmp.jar}" basedir="." includes="j*.xml" - excludes="java.xml" + excludes="java.xml,jmod.xml" update="true" /> @@ -131,7 +131,7 @@ destfile="${tmp.jar}" basedir="." includes="j*.xml" - excludes="java.xml" + excludes="java.xml,jmod.xml" /> @@ -144,14 +144,14 @@ depends="makezip"> - + - + diff --git a/src/etc/testcases/taskdefs/jmod.xml b/src/etc/testcases/taskdefs/jmod.xml new file mode 100644 index 000000000..2a22998cb --- /dev/null +++ b/src/etc/testcases/taskdefs/jmod.xml @@ -0,0 +1,992 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +package ${hello.pkg}; + +import java.util.logging.Logger; + +public class HelloWorld { + public void run() { + Logger logger = Logger.getLogger(HelloWorld.class.getName()); + logger.info("HELLO WORLD"); + } + + public static void main(String[] args) { + new HelloWorld().run(); + } +} + + +module ${hello.pkg} { + exports ${hello.pkg}; + requires java.logging; +} + + + + + + + + + + + + + + + + + + + + + + + + + +package ${smile.pkg}; + +import java.util.logging.Logger; +import ${hello.pkg}.HelloWorld; + +public class Smile { + public void run() { + Logger logger = Logger.getLogger(Smile.class.getName()); + logger.info("\u263a\u263b\u263a\u263b"); + } + + public static void main(String[] args) { + new Smile().run(); + new HelloWorld().run(); + } +} + + +module ${smile.pkg} { + exports ${smile.pkg}; + requires java.logging; + requires ${hello.pkg}; +} + + + + + + + + + + + + + + + + + + + + + + + + + + +package ${foobar.pkg}; + +import ${hello.main-class}; +import ${smile.main-class}; + +public class FooBar { + public void run() { + new HelloWorld().run(); + new Smile().run(); + } + + public static void main(String[] args) { + new FooBar().run(); + } +} + + +module ${foobar.pkg} { + exports ${foobar.pkg}; + requires java.logging; + requires ${hello.pkg}; + requires ${smile.pkg}; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +#!/bin/bash +`dirname "$0"`/java -m ${hello.pkg}/${hello.pkg}.HelloWorld + + + + + + + + + + + + + + + +#!/bin/bash +`dirname "$0"`/java -m ${hello.pkg}/${hello.pkg}.HelloWorld + + + + + + + + + + + + + + + + + + + +#!/bin/bash +`dirname "$0"`/java -m ${hello.pkg}/${hello.pkg}.HelloWorld + + + + + + + + + + + + + + + + + + + + +#!/bin/bash +`dirname "$0"`/java -m ${hello.pkg}/${hello.pkg}.HelloWorld + + + + + + + +#!/bin/bash +`dirname "$0"`/java -m ${hello.pkg}/${hello.pkg}.HelloWorld + + + + + + + + + + + + + + + + + + + + + +typedef int index_t; + + + + + + + + + + + + +typedef char * string_t; + + + + + + + + + + + + + + + + +typedef char * string_t; + + + + + + + + + + + + + + + + + + + +typedef int index_t; + + +typedef char * string_t; + + + + + + + + + + + + + + + + + + +timeout=3600 + + + + + + + + + + + + +timeout=7200 + + + + + + + + + + + + + + + + +timeout=7200 + + + + + + + + + + + + + + + + + + + +timeout=3600 + + +timeout=7200 + + + + + + + + + + + + + + + + + + +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. + + + + + + + + + + + + +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. + + + + + + + + + + + + + + + + +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. + + + + + + + + + + + + + + + + + + + +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. + + + + + + + + + + + + + + + + + + + +.br +Report true translation bugs to +.SH COPYRIGHT +Copyright \(co 2016 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later . +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +.SH "SEE ALSO" +Full documentation at: +]]> + + + + + + + + + + + +.br +Report false translation bugs to +.SH COPYRIGHT +Copyright \(co 2016 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later . +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +.SH "SEE ALSO" +Full documentation at: +]]> + + + + + + + + + + + + + + + +.br +Report false translation bugs to +.SH COPYRIGHT +Copyright \(co 2016 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later . +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +.SH "SEE ALSO" +Full documentation at: +]]> + + + + + + + + + + + + + + + + + + +.br +Report true translation bugs to +.SH COPYRIGHT +Copyright \(co 2016 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later . +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +.SH "SEE ALSO" +Full documentation at: +]]> + +.br +Report false translation bugs to +.SH COPYRIGHT +Copyright \(co 2016 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later . +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +.SH "SEE ALSO" +Full documentation at: +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/etc/testcases/taskdefs/link.xml b/src/etc/testcases/taskdefs/link.xml new file mode 100644 index 000000000..321247556 --- /dev/null +++ b/src/etc/testcases/taskdefs/link.xml @@ -0,0 +1,1088 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +package ${hello.pkg}; + +import java.util.logging.Logger; + +public class HelloWorld { + public void run(String[] resources) { + Logger logger = Logger.getLogger(HelloWorld.class.getName()); + logger.info("HELLO WORLD"); + + for (String resource : resources) { + Object url = HelloWorld.class.getResource(resource); + logger.info(resource + " " + (url != null ? "present" : "absent")); + } + } + + public static void main(String[] args) { + new HelloWorld().run(args); + } +} + + +module ${hello.mod} { + exports ${hello.pkg}; + requires java.logging; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +package ${smile.pkg}; + +import java.util.logging.Logger; +import ${hello.pkg}.HelloWorld; + +public class Smile { + public void run(String[] resources) { + Logger logger = Logger.getLogger(Smile.class.getName()); + logger.info("\u263a\u263b\u263a\u263b"); + + for (String resource : resources) { + Object url = HelloWorld.class.getResource(resource); + logger.info(resource + " " + (url != null ? "present" : "absent")); + } + } + + public static void main(String[] args) { + new Smile().run(args); + new HelloWorld().run(args); + } +} + + +module ${smile.mod} { + exports ${smile.pkg}; + requires java.logging; + requires ${hello.mod}; +} + + + + + + + + + + + + + + + + + +.br +Report true translation bugs to +.SH COPYRIGHT +Copyright \(co 2016 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later . +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +.SH "SEE ALSO" +Full documentation at: +]]> + + +.br +Report false translation bugs to +.SH COPYRIGHT +Copyright \(co 2016 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later . +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +.SH "SEE ALSO" +Full documentation at: +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + languages = new HashSet<>(); + for (Locale locale : Locale.getAvailableLocales()) { + languages.add(locale.getLanguage()); + } + + boolean matched = languages.containsAll(Arrays.asList(languagesToFind)); + System.exit(matched ? 0 : 1); + } +}]]> + + +module ${localefinder.mod} { + exports ${localefinder.pkg}; + requires jdk.localedata; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +package ${thrower.pkg}; + +public class Thrower { + public static void main(String[] args) { + throw new RuntimeException("Deliberate exception."); + } +} + + +module ${thrower.mod} { + exports ${thrower.pkg}; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +package ${inc.pkg}; + +public interface IncrementProvider { + int getIncrement(); +} + + +package ${inc.pkg}; + +import java.util.ServiceLoader; + +public class Incrementer { + public static void main(String[] args) { + for (IncrementProvider provider : ServiceLoader.load(IncrementProvider.class)) { + int n = 0; + for (int i = 0; i < 5; i++) { + n += provider.getIncrement(); + System.out.println(n); + } + } + } +} + + +module ${inc.mod} { + exports ${inc.pkg}; + uses ${inc.pkg}.IncrementProvider; +} + + + + + + + + + + + + + + + + + + + + + +package ${provider.pkg}; + +import ${inc.pkg}.IncrementProvider; + +public class ByTwoProvider +implements IncrementProvider { + @Override + public int getIncrement() { + return 2; + } +} + + +module ${provider.mod} { + exports ${provider.pkg}; + requires transitive ${inc.mod}; + provides ${inc.pkg}.IncrementProvider with ${provider.pkg}.ByTwoProvider; +} + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/org/apache/tools/ant/taskdefs/defaults.properties b/src/main/org/apache/tools/ant/taskdefs/defaults.properties index a78cedb34..d847ae9d0 100644 --- a/src/main/org/apache/tools/ant/taskdefs/defaults.properties +++ b/src/main/org/apache/tools/ant/taskdefs/defaults.properties @@ -66,6 +66,8 @@ jar=org.apache.tools.ant.taskdefs.Jar java=org.apache.tools.ant.taskdefs.Java javac=org.apache.tools.ant.taskdefs.Javac javadoc=org.apache.tools.ant.taskdefs.Javadoc +jmod=org.apache.tools.ant.taskdefs.modules.Jmod +link=org.apache.tools.ant.taskdefs.modules.Link length=org.apache.tools.ant.taskdefs.Length loadfile=org.apache.tools.ant.taskdefs.LoadFile loadproperties=org.apache.tools.ant.taskdefs.LoadProperties diff --git a/src/main/org/apache/tools/ant/taskdefs/modules/Jmod.java b/src/main/org/apache/tools/ant/taskdefs/modules/Jmod.java new file mode 100644 index 000000000..2284ed364 --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/modules/Jmod.java @@ -0,0 +1,1282 @@ +/* + * 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.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.IOException; + +import java.nio.file.Files; + +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; + +import java.util.Map; +import java.util.LinkedHashMap; + +import java.util.Collections; + +import java.util.spi.ToolProvider; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; + +import org.apache.tools.ant.util.MergingMapper; +import org.apache.tools.ant.util.FileUtils; +import org.apache.tools.ant.util.ResourceUtils; + +import org.apache.tools.ant.types.EnumeratedAttribute; +import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.types.ModuleVersion; +import org.apache.tools.ant.types.Path; +import org.apache.tools.ant.types.Reference; +import org.apache.tools.ant.types.Resource; +import org.apache.tools.ant.types.ResourceCollection; + +import org.apache.tools.ant.types.resources.FileResource; +import org.apache.tools.ant.types.resources.Union; + +/** + * Creates a linkable .jmod file from a modular jar file, and optionally from + * other resource files such as native libraries and documents. Equivalent + * to the JDK's + * jmod + * tool. + *

    + * Supported attributes: + *

    + *
    {@code destFile} + *
    Required, jmod file to create. + *
    {@code classpath} + *
    {@code classpathref} + *
    Where to locate files to be placed in the jmod file. + *
    {@code modulepath} + *
    {@code modulepathref} + *
    Where to locate dependencies. + *
    {@code commandpath} + *
    {@code commandpathref} + *
    Directories containing native commands to include in jmod. + *
    {@code headerpath} + *
    {@code headerpathref} + *
    Directories containing header files to include in jmod. + *
    {@code configpath} + *
    {@code configpathref} + *
    Directories containing user-editable configuration files + * to include in jmod. + *
    {@code legalpath} + *
    {@code legalpathref} + *
    Directories containing legal licenses and notices to include in jmod. + *
    {@code nativelibpath} + *
    {@code nativelibpathref} + *
    Directories containing native libraries to include in jmod. + *
    {@code manpath} + *
    {@code manpathref} + *
    Directories containing man pages to include in jmod. + *
    {@code version} + *
    Module version. + *
    {@code mainclass} + *
    Main class of module. + *
    {@code platform} + *
    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: + *
    + *
    {@code pattern} + *
    A standard PathMatcher pattern + *
    {@code listFile} + *
    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: + *
    + *
    {@code pattern} + *
    A standard PathMatcher pattern + *
    {@code listFile} + *
    Text file containing list of file names (not patterns), + * one per line + *
    + *
    {@code } + *
    Excludes resources from jmods. May be specified multiple times. + * Exactly one of these attributes is required: + *
    + *
    {@code pattern} + *
    A standard PathMatcher pattern + *
    {@code listFile} + *
    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 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 test1, + final Predicate 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"); + } +}