ant-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From bode...@apache.org
Subject [4/6] ant git commit: Added tasks for JDK's jmod and jlink tools.
Date Sat, 15 Dec 2018 16:28:14 GMT
http://git-wip-us.apache.org/repos/asf/ant/blob/343dff90/src/etc/testcases/taskdefs/link.xml
----------------------------------------------------------------------
diff --git a/src/etc/testcases/taskdefs/link.xml b/src/etc/testcases/taskdefs/link.xml
new file mode 100644
index 0000000..3212475
--- /dev/null
+++ b/src/etc/testcases/taskdefs/link.xml
@@ -0,0 +1,1088 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<project name="linktest" default="nil" xmlns:unless="ant:unless">
+  <import file="../buildfiletest-base.xml"/>
+
+  <target name="nil"/>
+
+  <target name="-dirs">
+    <mkdir dir="${input}"/>
+
+    <property name="jmods" value="${output}/jmods"/>
+    <mkdir dir="${jmods}"/>
+
+    <property name="manpages" value="${output}/manpages"/>
+    <mkdir dir="${manpages}"/>
+
+    <property name="image" value="${output}/image"/>
+
+    <!--
+      Before Java 10, JDK's jmods directory must be explicitly included
+      in module paths.
+     -->
+    <condition property="jdkmods" value=""
+               else="${java.home}/jmods${path.separator}">
+      <hasmethod classname="java.util.List" method="copyOf"/>
+    </condition>
+  </target>
+
+  <target name="setUp" depends="-dirs">
+  </target>
+
+  <!-- Creates simple modular jar, with only Java SE dependencies. -->
+  <target name="-hello" depends="-dirs">
+
+    <property name="hello.root" value="${input}/hello"/>
+
+    <property name="hello.src" value="${hello.root}/src"/>
+    <property name="hello.classes" value="${hello.root}/classes"/>
+    <property name="hello.jar.dir" value="${hello.root}/jars"/>
+    <property name="hello.jar" value="${hello.jar.dir}/hello.jar"/>
+    <property name="hello.legal" value="${hello.root}/legal"/>
+
+    <property name="hello.mod"      value="org.apache.tools.ant.test.hello"/>
+    <property name="hello.pkg"      value="org.apache.tools.ant.test.hello"/>
+    <property name="hello.pkg.path" value="org/apache/tools/ant/test/hello"/>
+    <property name="hello.pkg.dir" value="${hello.src}/${hello.pkg.path}"/>
+
+    <property name="hello.main-class" value="${hello.pkg}.HelloWorld"/>
+
+    <mkdir dir="${hello.pkg.dir}"/>
+    <echo file="${hello.pkg.dir}/HelloWorld.java">
+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);
+    }
+}
+    </echo>
+    <echo file="${hello.src}/module-info.java">
+module ${hello.mod} {
+    exports ${hello.pkg};
+    requires java.logging;
+}
+    </echo>
+
+    <mkdir dir="${hello.classes}"/>
+    <javac srcdir="${hello.src}" destdir="${hello.classes}"
+           includeAntRuntime="false" debug="true"/>
+    <echo file="${hello.classes}/${hello.pkg.path}/resource1.txt"
+          message="First HelloWorld resource."/>
+    <echo file="${hello.classes}/${hello.pkg.path}/resource2.txt"
+          message="Second HelloWorld resource."/>
+
+    <mkdir dir="${hello.jar.dir}"/>
+    <jar destfile="${hello.jar}" basedir="${hello.classes}"/>
+  </target>
+
+  <!-- Creates modular jar with dependency on hello module. -->
+  <target name="-smile" depends="-dirs,-hello">
+    <property name="smile.root" value="${input}/smile"/>
+
+    <property name="smile.src" value="${smile.root}/src"/>
+    <property name="smile.classes" value="${smile.root}/classes"/>
+    <property name="smile.jar.dir" value="${smile.root}/jars"/>
+    <property name="smile.jar" value="${smile.jar.dir}/smile.jar"/>
+    <property name="smile.legal" value="${smile.root}/legal"/>
+
+    <property name="smile.mod"      value="org.apache.tools.ant.test.smile"/>
+    <property name="smile.pkg"      value="org.apache.tools.ant.test.smile"/>
+    <property name="smile.pkg.path" value="org/apache/tools/ant/test/smile"/>
+    <property name="smile.pkg.dir" value="${smile.src}/${smile.pkg.path}"/>
+
+    <property name="smile.main-class" value="${smile.pkg}.Smile"/>
+
+    <mkdir dir="${smile.pkg.dir}"/>
+    <echo file="${smile.pkg.dir}/Smile.java">
+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);
+    }
+}
+    </echo>
+    <echo file="${smile.src}/module-info.java">
+module ${smile.mod} {
+    exports ${smile.pkg};
+    requires java.logging;
+    requires ${hello.mod};
+}
+    </echo>
+
+    <mkdir dir="${smile.classes}"/>
+    <javac srcdir="${smile.src}" destdir="${smile.classes}"
+           includeAntRuntime="false" debug="true"
+           modulepath="${hello.jar}"/>
+    <echo file="${smile.classes}/${smile.pkg.path}/resource1.txt"
+          message="First Smile resource."/>
+    <echo file="${smile.classes}/${smile.pkg.path}/resource2.txt"
+          message="Second Smile resource."/>
+
+    <mkdir dir="${smile.jar.dir}"/>
+    <jar destfile="${smile.jar}" basedir="${smile.classes}"/>
+  </target>
+
+  <target name="-manpages" depends="-dirs">
+
+    <property name="man1" value="${manpages}/man1.1"/>
+    <property name="man2" value="${manpages}/man2.1"/>
+
+    <echo file="${man1}"><!--
+--><![CDATA[.TH TRUE "1" "February 2017" "GNU coreutils 8.26" "User Commands"
+.SH NAME
+true \- do nothing, successfully
+.SH SYNOPSIS
+.B true
+[\fI\,ignored command line arguments\/\fR]
+.br
+.B true
+\fI\,OPTION\/\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Exit with a status code indicating success.
+.TP
+\fB\-\-help\fR
+display this help and exit
+.TP
+\fB\-\-version\fR
+output version information and exit
+.PP
+NOTE: your shell may have its own version of true, which usually supersedes
+the version described here.  Please refer to your shell's documentation
+for details about the options it supports.
+.SH AUTHOR
+Written by Jim Meyering.
+.SH "REPORTING BUGS"
+GNU coreutils online help: <http://www.gnu.org/software/coreutils/>
+.br
+Report true translation bugs to <http://translationproject.org/team/>
+.SH COPYRIGHT
+Copyright \(co 2016 Free Software Foundation, Inc.
+License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
+.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: <http://www.gnu.org/software/coreutils/true>
+]]><!--
+    --></echo>
+
+    <echo file="${man2}"><!--
+--><![CDATA[.TH FALSE "1" "February 2017" "GNU coreutils 8.26" "User Commands"
+.SH NAME
+false \- do nothing, unsuccessfully
+.SH SYNOPSIS
+.B false
+[\fI\,ignored command line arguments\/\fR]
+.br
+.B false
+\fI\,OPTION\/\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Exit with a status code indicating failure.
+.TP
+\fB\-\-help\fR
+display this help and exit
+.TP
+\fB\-\-version\fR
+output version information and exit
+.PP
+NOTE: your shell may have its own version of false, which usually supersedes
+the version described here.  Please refer to your shell's documentation
+for details about the options it supports.
+.SH AUTHOR
+Written by Jim Meyering.
+.SH "REPORTING BUGS"
+GNU coreutils online help: <http://www.gnu.org/software/coreutils/>
+.br
+Report false translation bugs to <http://translationproject.org/team/>
+.SH COPYRIGHT
+Copyright \(co 2016 Free Software Foundation, Inc.
+License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
+.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: <http://www.gnu.org/software/coreutils/false>
+]]><!--
+    --></echo>
+  </target>
+
+  <target name="-legal" depends="-smile">
+    <property name="sharedlicense.name" value="USELESSLICENSE"/>
+    <property name="sharedlicense.text"
+              value="This license grants no particular rights."/>
+
+    <mkdir dir="${hello.legal}"/>
+    <mkdir dir="${smile.legal}"/>
+
+    <echo file="${hello.legal}/${sharedlicense.name}"
+          message="${sharedlicense.text}"/>
+    <echo file="${smile.legal}/${sharedlicense.name}"
+          message="${sharedlicense.text}"/>
+
+    <property name="uniquelicense.name" value="UNIQUELICENSE"/>
+    <echo file="${hello.legal}/${uniquelicense.name}"
+          message="License for ${hello.mod}."/>
+    <echo file="${smile.legal}/${uniquelicense.name}"
+          message="License for ${smile.mod}."/>
+  </target>
+
+  <target name="-jmods" depends="-smile,-legal">
+    <property name="hello.jmod" value="${jmods}/hello.jmod"/>
+    <property name="smile.jmod" value="${jmods}/smile.jmod"/>
+
+    <jmod destfile="${hello.jmod}"
+          classpath="${hello.jar}"
+          legalpath="${hello.legal}"
+          manpath="${manpages}"/>
+
+    <jmod destfile="${smile.jmod}"
+          classpath="${smile.jar}"
+          legalpath="${smile.legal}"
+          manpath="${manpages}"
+          modulepath="${hello.jar.dir}"/>
+  </target>
+
+  <target name="modulepath" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"/>
+  </target>
+
+  <target name="imageNewerThanJmods" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"/>
+
+    <chmod perm="a+w">
+      <fileset dir="${image}"/>
+    </chmod>
+
+    <property name="dateformat" value="yyyy-MM-dd HH:mm:ss.SSS"/>
+    <tstamp>
+      <format property="future" pattern="${dateformat}" offset="1" unit="hour"/>
+    </tstamp>
+    <touch datetime="${future}" pattern="${dateformat}">
+      <fileset dir="${image}"/>
+    </touch>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"/>
+  </target>
+
+  <target name="nomodulepath" depends="-jmods">
+    <link destdir="${image}"
+          modules="${hello.mod},${smile.mod}"/>
+  </target>
+
+  <target name="nomodules" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"/>
+  </target>
+
+  <target name="modulepathref" depends="-jmods">
+    <path id="modules">
+      <pathelement location="${jdkmods}" unless:blank="${jdkmods}"/>
+      <pathelement location="${jmods}"/>
+    </path>
+    <link destdir="${image}" modulepathref="modules"
+          modules="${hello.mod},${smile.mod}"/>
+  </target>
+
+  <target name="modulepath-nested" depends="-jmods">
+    <link destdir="${image}" modules="${hello.mod},${smile.mod}">
+      <modulepath>
+        <pathelement location="${jdkmods}" unless:blank="${jdkmods}"/>
+        <pathelement location="${jmods}"/>
+      </modulepath>
+    </link>
+  </target>
+
+  <target name="modulepath-both" depends="-jmods">
+    <property name="mod1" value="${output}/mod1"/>
+    <property name="mod2" value="${output}/mod2"/>
+
+    <mkdir dir="${mod1}"/>
+    <mkdir dir="${mod2}"/>
+
+    <copy file="${hello.jmod}" todir="${mod1}"/>
+    <copy file="${smile.jmod}" todir="${mod2}"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${mod1}"
+          modules="${hello.mod},${smile.mod}">
+      <modulepath>
+        <pathelement location="${mod2}"/>
+      </modulepath>
+    </link>
+  </target>
+
+  <target name="modules-nested" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}">
+      <module name="${hello.mod}"/>
+      <module name="${smile.mod}"/>
+    </link>
+  </target>
+
+  <target name="modules-nested-missing-name" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}">
+      <module/>
+      <module name="${smile.mod}"/>
+    </link>
+  </target>
+
+  <target name="modules-both" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}" modules="${hello.mod}">
+      <module name="${smile.mod}"/>
+      <!--
+        Adding this launcher guarantees an error occurs unless
+        both modules are found.
+      -->
+      <launcher name="Smile" module="${smile.mod}" mainclass="${smile.main-class}"/>
+    </link>
+  </target>
+
+  <target name="observable" depends="-jmods">
+    <link destdir="${image}" modulepath="${jmods};${java.home}/jmods"
+          modules="${hello.mod},${smile.mod}"
+          observableModules="java.base"/>
+  </target>
+
+  <target name="observable-nested" depends="-jmods">
+    <link destdir="${image}" modulepath="${jmods};${java.home}/jmods"
+          modules="${hello.mod},${smile.mod}">
+        <observableModule name="java.base"/>
+    </link>
+  </target>
+
+  <target name="observable-nested-missing-name" depends="-jmods">
+    <link destdir="${image}" modulepath="${jmods};${java.home}/jmods"
+          modules="${hello.mod},${smile.mod}">
+        <observableModule/>
+    </link>
+  </target>
+
+  <target name="observable-both" depends="-jmods">
+    <link destdir="${image}" modulepath="${jmods};${java.home}/jmods"
+          modules="${hello.mod},${smile.mod}"
+          observableModules="java.base">
+        <observableModule name="java.logging"/>
+    </link>
+  </target>
+
+  <target name="launchers" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          launchers="Hello=${hello.mod}/${hello.main-class},Smile=${smile.mod}/${smile.main-class}"/>
+  </target>
+
+  <target name="launchers-nested" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <launcher name="Hello" module="${hello.mod}" mainClass="${hello.main-class}"/>
+      <launcher name="Smile" module="${smile.mod}" mainClass="${smile.main-class}"/>
+    </link>
+  </target>
+
+  <target name="launchers-nested-missing-name" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <launcher              module="${hello.mod}" mainClass="${hello.main-class}"/>
+      <launcher name="Smile" module="${smile.mod}" mainClass="${smile.main-class}"/>
+    </link>
+  </target>
+
+  <target name="launchers-nested-missing-module" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <launcher name="Hello"                       mainClass="${hello.main-class}"/>
+      <launcher name="Smile" module="${smile.mod}" mainClass="${smile.main-class}"/>
+    </link>
+  </target>
+
+  <target name="launchers-both" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          launchers="Hello=${hello.mod}/${hello.main-class}">
+      <launcher name="Smile" module="${smile.mod}" mainClass="${smile.main-class}"/>
+    </link>
+  </target>
+
+  <target name="-localefinder" depends="-dirs">
+
+    <property name="localefinder.root" value="${input}/localefinder"/>
+
+    <property name="localefinder.src" value="${localefinder.root}/src"/>
+    <property name="localefinder.classes" value="${localefinder.root}/classes"/>
+    <property name="localefinder.jar.dir" value="${localefinder.root}/jars"/>
+    <property name="localefinder.jar" value="${localefinder.jar.dir}/localefinder.jar"/>
+
+    <property name="localefinder.mod"      value="org.apache.tools.ant.test.localefinder"/>
+    <property name="localefinder.pkg"      value="org.apache.tools.ant.test.localefinder"/>
+    <property name="localefinder.pkg.path" value="org/apache/tools/ant/test/localefinder"/>
+    <property name="localefinder.pkg.dir" value="${localefinder.src}/${localefinder.pkg.path}"/>
+
+    <property name="localefinder.main-class" value="${localefinder.pkg}.LocaleFinder"/>
+
+    <mkdir dir="${localefinder.pkg.dir}"/>
+    <echo file="${localefinder.pkg.dir}/LocaleFinder.java"><![CDATA[
+package ${localefinder.pkg};
+
+import java.util.Locale;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.HashSet;
+
+public class LocaleFinder {
+    public static void main(String[] languagesToFind) {
+        Set<String> languages = new HashSet<>();
+        for (Locale locale : Locale.getAvailableLocales()) {
+            languages.add(locale.getLanguage());
+        }
+
+        boolean matched = languages.containsAll(Arrays.asList(languagesToFind));
+        System.exit(matched ? 0 : 1);
+    }
+}]]>
+    </echo>
+    <echo file="${localefinder.src}/module-info.java">
+module ${localefinder.mod} {
+    exports ${localefinder.pkg};
+    requires jdk.localedata;
+}
+    </echo>
+
+    <mkdir dir="${localefinder.classes}"/>
+    <javac srcdir="${localefinder.src}"
+           destdir="${localefinder.classes}"
+           includeAntRuntime="false"
+           debug="true"/>
+
+    <mkdir dir="${localefinder.jar.dir}"/>
+    <jar destfile="${localefinder.jar}" basedir="${localefinder.classes}"/>
+
+    <property name="localefinder.jmod" value="${jmods}/localefinder.jmod"/>
+    <jmod destfile="${localefinder.jmod}" classpath="${localefinder.jar}"/>
+  </target>
+
+  <target name="locales" depends="-localefinder">
+    <link destdir="${image}" modulepath="${jmods};${java.home}/jmods"
+          modules="${localefinder.mod},jdk.localedata"
+          locales="zh,in"/>
+  </target>
+
+  <target name="locales-nested" depends="-localefinder">
+    <link destdir="${image}" modulepath="${jmods};${java.home}/jmods"
+          modules="${localefinder.mod},jdk.localedata">
+      <locale name="zh"/>
+      <locale name="in"/>
+    </link>
+  </target>
+
+  <target name="locales-nested-missing-name" depends="-localefinder">
+    <link destdir="${image}" modulepath="${jmods};${java.home}/jmods"
+          modules="${localefinder.mod},jdk.localedata">
+      <locale/>
+      <locale name="in"/>
+    </link>
+  </target>
+
+  <target name="locales-both" depends="-localefinder">
+    <link destdir="${image}" modulepath="${jmods};${java.home}/jmods"
+          modules="${localefinder.mod},jdk.localedata"
+          locales="zh">
+      <locale name="in"/>
+    </link>
+  </target>
+
+  <target name="ordering" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          resourceOrder="**/resource1.txt"/>
+  </target>
+
+  <target name="ordering-nested" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <resourceOrder pattern="**/resource1.txt"/>
+    </link>
+  </target>
+
+  <target name="ordering-nested-file" depends="-jmods">
+    <tempfile property="listfile" destdir="${output}" suffix=".lst"/>
+    <echo file="${listfile}" message="**/resource1.txt"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <resourceOrder listfile="${listfile}"/>
+    </link>
+  </target>
+
+  <target name="ordering-nested-no-attr" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <resourceOrder/>
+    </link>
+  </target>
+
+  <target name="ordering-nested-both" depends="-jmods">
+    <tempfile property="listfile" destdir="${output}" suffix=".lst"/>
+    <echo file="${listfile}" message="**/resource1.txt"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <resourceOrder pattern="**/resource1.txt" listfile="${listfile}"/>
+    </link>
+  </target>
+
+  <target name="ordering-both" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          resourceOrder="**/resource1.txt">
+      <resourceOrder pattern="**/resource2.txt"/>
+    </link>
+  </target>
+
+  <target name="excluderesources" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          excludeResources="**/resource1.txt"/>
+  </target>
+
+  <target name="excluderesources-nested" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <excludeResources pattern="**/resource1.txt"/>
+    </link>
+  </target>
+
+  <target name="excluderesources-nested-file" depends="-jmods">
+    <tempfile property="listfile" destdir="${output}" suffix=".lst"/>
+    <echo file="${listfile}" message="**/resource1.txt"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <excludeResources listfile="${listfile}"/>
+    </link>
+  </target>
+
+  <target name="excluderesources-nested-no-attr" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <excludeResources/>
+    </link>
+  </target>
+
+  <target name="excluderesources-nested-both" depends="-jmods">
+    <tempfile property="listfile" destdir="${output}" suffix=".lst"/>
+    <echo file="${listfile}" message="**/resource1.txt"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <excludeResources pattern="**/resource1.txt" listfile="${listfile}"/>
+    </link>
+  </target>
+
+  <target name="excluderesources-both" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          excludeResources="**/resource1.txt">
+      <excludeResources pattern="**/resource2.txt"/>
+    </link>
+  </target>
+
+  <target name="excludefiles" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          excludeFiles="**/file1.txt"/>
+  </target>
+
+  <target name="excludefiles-nested" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <excludeFiles pattern="**/file1.txt"/>
+    </link>
+  </target>
+
+  <target name="excludefiles-nested-file" depends="-jmods">
+    <tempfile property="listfile" destdir="${output}" suffix=".lst"/>
+    <echo file="${listfile}" message="**/file1.txt"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <excludeFiles listfile="${listfile}"/>
+    </link>
+  </target>
+
+  <target name="excludefiles-nested-no-attr" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <excludeFiles/>
+    </link>
+  </target>
+
+  <target name="excludefiles-nested-both" depends="-jmods">
+    <tempfile property="listfile" destdir="${output}" suffix=".lst"/>
+    <echo file="${listfile}" message="**/file1.txt"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <excludeFiles pattern="**/file1.txt" listfile="${listfile}"/>
+    </link>
+  </target>
+
+  <target name="excludefiles-both" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          excludeFiles="**/file1.txt">
+      <excludeFiles pattern="**/file2.txt"/>
+    </link>
+  </target>
+
+  <target name="includeheaders" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          includeHeaders="false"/>
+  </target>
+
+  <target name="includemanpages" depends="-manpages,-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          includeManPages="false"/>
+  </target>
+
+  <target name="includenativecommands" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          includeNativeCommands="false"/>
+  </target>
+
+  <target name="compression" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"/>
+
+    <property name="compressed-image" value="${output}/image2"/>
+    <link destdir="${compressed-image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          compress="zip"/>
+  </target>
+
+  <target name="compression-nested" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"/>
+
+    <property name="compressed-image" value="${output}/image2"/>
+    <link destdir="${compressed-image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <compress level="zip"/>
+    </link>
+  </target>
+
+  <target name="compression-nested-no-attr" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <compress/>
+    </link>
+  </target>
+
+  <target name="compression-both" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"/>
+
+    <property name="compressed-image" value="${output}/image2"/>
+    <link destdir="${compressed-image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          compress="zip">
+      <compress level="zip"/>
+    </link>
+  </target>
+
+  <target name="endian" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}" endianness="little"/>
+  </target>
+
+  <target name="vm" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}" vmType="server"/>
+  </target>
+
+  <target name="releaseinfo-file" depends="-jmods">
+    <property name="props" value="${output}/app.properties"/>
+    <echo file="${props}" message="test=true" encoding="ISO-8859-1"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo file="${props}"/>
+    </link>
+  </target>
+
+  <target name="releaseinfo-delete" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add key="test" value="true"/>
+      </releaseinfo>
+      <releaseinfo delete="test"/>
+    </link>
+  </target>
+
+  <target name="releaseinfo-nested-delete" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add key="test" value="true"/>
+      </releaseinfo>
+      <releaseinfo>
+        <delete key="test"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-nested-delete-no-key" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <delete/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-nested-delete-both" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add key="test" value="true"/>
+        <add key="foo" value="bar"/>
+      </releaseinfo>
+      <releaseinfo delete="test">
+        <delete key="foo"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-add-file" depends="-jmods">
+    <property name="props" value="${output}/app.properties"/>
+    <echo file="${props}" message="test=s&#xed;" encoding="ISO-8859-1"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add file="${props}"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-add-file-charset" depends="-jmods">
+    <property name="props" value="${output}/app.properties"/>
+    <echo file="${props}" message="test=s&#xed;" encoding="UTF-8"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add file="${props}" charset="UTF-8"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-add-key" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add key="test" value="true"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-add-no-value" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add key="test"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-add-no-key" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add value="true"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-add-file-and-key" depends="-jmods">
+    <property name="props" value="${output}/app.properties"/>
+    <echo file="${props}" message="test=s&#xed;" encoding="UTF-8"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add file="${props}" key="test"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="releaseinfo-add-file-and-value" depends="-jmods">
+    <property name="props" value="${output}/app.properties"/>
+    <echo file="${props}" message="test=s&#xed;" encoding="UTF-8"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}">
+      <releaseinfo>
+        <add file="${props}" value="true"/>
+      </releaseinfo>
+    </link>
+  </target>
+
+  <target name="-thrower" depends="-dirs">
+
+    <property name="thrower.root" value="${input}/thrower"/>
+
+    <property name="thrower.src" value="${thrower.root}/src"/>
+    <property name="thrower.classes" value="${thrower.root}/classes"/>
+    <property name="thrower.jar.dir" value="${thrower.root}/jars"/>
+    <property name="thrower.jar" value="${thrower.jar.dir}/thrower.jar"/>
+
+    <property name="thrower.mod"      value="org.apache.tools.ant.test.thrower"/>
+    <property name="thrower.pkg"      value="org.apache.tools.ant.test.thrower"/>
+    <property name="thrower.pkg.path" value="org/apache/tools/ant/test/thrower"/>
+    <property name="thrower.pkg.dir" value="${thrower.src}/${thrower.pkg.path}"/>
+
+    <property name="thrower.main-class" value="${thrower.pkg}.Thrower"/>
+
+    <mkdir dir="${thrower.pkg.dir}"/>
+    <echo file="${thrower.pkg.dir}/Thrower.java">
+package ${thrower.pkg};
+
+public class Thrower {
+    public static void main(String[] args) {
+        throw new RuntimeException("Deliberate exception.");
+    }
+}
+    </echo>
+    <echo file="${thrower.src}/module-info.java">
+module ${thrower.mod} {
+    exports ${thrower.pkg};
+}
+    </echo>
+
+    <mkdir dir="${thrower.classes}"/>
+    <javac srcdir="${thrower.src}" destdir="${thrower.classes}"
+           includeAntRuntime="false" debug="true"/>
+
+    <mkdir dir="${thrower.jar.dir}"/>
+    <jar destfile="${thrower.jar}" basedir="${thrower.classes}"/>
+  </target>
+
+  <target name="debug" depends="-thrower">
+    <property name="thrower.jmod" value="${jmods}/thrower.jmod"/>
+    <jmod destfile="${thrower.jmod}" classpath="${thrower.jar}"/>
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${thrower.mod}" debug="false"/>
+  </target>
+
+  <target name="dedup" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"/>
+  </target>
+
+  <target name="dedup-identical" depends="-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          checkDuplicateLegal="true"/>
+  </target>
+
+  <target name="-sign" depends="-smile,-legal">
+    <property name="keystore.file" value="${output}/keystore"/>
+    <property name="keystore.alias" value="test"/>
+    <property name="keystore.password" value="swordfish"/>
+
+    <condition property="execsuffix" value=".exe" else="">
+      <os family="windows"/>
+    </condition>
+    <exec executable="${java.home}/bin/keytool${execsuffix}" failonerror="true">
+      <arg value="-genkeypair"/>
+      <arg value="-keystore"/>
+      <arg file="${keystore.file}"/>
+      <arg value="-storepass"/>
+      <arg value="${keystore.password}"/>
+      <arg value="-keypass"/>
+      <arg value="${keystore.password}"/>
+      <arg value="-alias"/>
+      <arg value="${keystore.alias}"/>
+      <arg value="-dname"/>
+      <arg value="CN=Ant Unit Test, OU=Ant, O=Apache Software Foundation, L=Unknown, ST=Unknown, C=US"/>
+    </exec>
+
+    <signjar jar="${hello.jar}"
+             keystore="${keystore.file}"
+             alias="${keystore.alias}"
+             storepass="${keystore.password}"/>
+  </target>
+
+  <target name="ignoresigning" depends="-sign,-jmods">
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${hello.mod},${smile.mod}"
+          ignoreSigning="true"/>
+  </target>
+
+  <target name="bindservices" depends="-dirs">
+    <property name="inc.root" value="${input}/inc"/>
+
+    <property name="inc.src" value="${inc.root}/src"/>
+    <property name="inc.classes" value="${inc.root}/classes"/>
+    <property name="inc.jar.dir" value="${inc.root}/jars"/>
+    <property name="inc.jar" value="${inc.jar.dir}/inc.jar"/>
+
+    <property name="inc.mod"      value="org.apache.tools.ant.test.inc"/>
+    <property name="inc.pkg"      value="org.apache.tools.ant.test.inc"/>
+    <property name="inc.pkg.path" value="org/apache/tools/ant/test/inc"/>
+    <property name="inc.pkg.dir" value="${inc.src}/${inc.pkg.path}"/>
+
+    <property name="inc.main-class" value="${inc.pkg}.Incrementer"/>
+
+    <mkdir dir="${inc.pkg.dir}"/>
+    <echo file="${inc.pkg.dir}/IncrementProvider.java">
+package ${inc.pkg};
+
+public interface IncrementProvider {
+    int getIncrement();
+}
+    </echo>
+    <echo file="${inc.pkg.dir}/Incrementer.java">
+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 &lt; 5; i++) {
+                n += provider.getIncrement();
+                System.out.println(n);
+            }
+        }
+    }
+}
+    </echo>
+    <echo file="${inc.src}/module-info.java">
+module ${inc.mod} {
+    exports ${inc.pkg};
+    uses ${inc.pkg}.IncrementProvider;
+}
+    </echo>
+
+    <mkdir dir="${inc.classes}"/>
+    <javac srcdir="${inc.src}" destdir="${inc.classes}"
+           includeAntRuntime="false" debug="true"/>
+
+    <mkdir dir="${inc.jar.dir}"/>
+    <jar destfile="${inc.jar}" basedir="${inc.classes}"/>
+
+    <property name="provider.root" value="${input}/provider"/>
+    <property name="provider.src" value="${provider.root}/src"/>
+    <property name="provider.classes" value="${provider.root}/classes"/>
+    <property name="provider.jar.dir" value="${provider.root}/jars"/>
+    <property name="provider.jar" value="${provider.jar.dir}/provider.jar"/>
+
+    <property name="provider.mod"      value="org.apache.tools.ant.test.provider"/>
+    <property name="provider.pkg"      value="org.apache.tools.ant.test.provider"/>
+    <property name="provider.pkg.path" value="org/apache/tools/ant/test/provider"/>
+    <property name="provider.pkg.dir" value="${provider.src}/${provider.pkg.path}"/>
+
+    <mkdir dir="${provider.pkg.dir}"/>
+    <echo file="${provider.pkg.dir}/ByTwoProvider.java">
+package ${provider.pkg};
+
+import ${inc.pkg}.IncrementProvider;
+
+public class ByTwoProvider
+implements IncrementProvider {
+    @Override
+    public int getIncrement() {
+        return 2;
+    }
+}
+    </echo>
+    <echo file="${provider.src}/module-info.java">
+module ${provider.mod} {
+    exports ${provider.pkg};
+    requires transitive ${inc.mod};
+    provides ${inc.pkg}.IncrementProvider with ${provider.pkg}.ByTwoProvider;
+}
+    </echo>
+
+    <mkdir dir="${provider.classes}"/>
+    <javac srcdir="${provider.src}" destdir="${provider.classes}"
+           includeAntRuntime="false" debug="true"
+           modulepath="${inc.classes}"/>
+
+    <mkdir dir="${provider.jar.dir}"/>
+    <jar destfile="${provider.jar}" basedir="${provider.classes}"/>
+
+    <property name="inc.jmod" value="${jmods}/inc.jmod"/>
+    <property name="provider.jmod" value="${jmods}/provider.jmod"/>
+
+    <jmod destfile="${inc.jmod}"
+          classpath="${inc.jar}"/>
+
+    <jmod destfile="${provider.jmod}"
+          classpath="${provider.jar}"
+          modulepath="${inc.jar.dir}"/>
+
+    <property name="image2" value="${output}/image2"/>
+
+    <link destdir="${image}" modulepath="${jdkmods}${jmods}"
+          modules="${inc.mod}"
+          bindServices="false"/>
+  
+    <link destdir="${image2}" modulepath="${jdkmods}${jmods}"
+          modules="${inc.mod}"
+          bindServices="true"/>
+  </target>
+</project>

http://git-wip-us.apache.org/repos/asf/ant/blob/343dff90/src/main/org/apache/tools/ant/taskdefs/defaults.properties
----------------------------------------------------------------------
diff --git a/src/main/org/apache/tools/ant/taskdefs/defaults.properties b/src/main/org/apache/tools/ant/taskdefs/defaults.properties
index a78cedb..d847ae9 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

http://git-wip-us.apache.org/repos/asf/ant/blob/343dff90/src/main/org/apache/tools/ant/taskdefs/modules/Jmod.java
----------------------------------------------------------------------
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 0000000..2284ed3
--- /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
+ * <a href="https://docs.oracle.com/en/java/javase/11/tools/jmod.html">jmod</a>
+ * tool.
+ * <p>
+ * Supported attributes:
+ * <dl>
+ * <dt>{@code destFile}
+ * <dd>Required, jmod file to create.
+ * <dt>{@code classpath}
+ * <dt>{@code classpathref}
+ * <dd>Where to locate files to be placed in the jmod file.
+ * <dt>{@code modulepath}
+ * <dt>{@code modulepathref}
+ * <dd>Where to locate dependencies.
+ * <dt>{@code commandpath}
+ * <dt>{@code commandpathref}
+ * <dd>Directories containing native commands to include in jmod.
+ * <dt>{@code headerpath}
+ * <dt>{@code headerpathref}
+ * <dd>Directories containing header files to include in jmod.
+ * <dt>{@code configpath}
+ * <dt>{@code configpathref}
+ * <dd>Directories containing user-editable configuration files
+ *     to include in jmod.
+ * <dt>{@code legalpath}
+ * <dt>{@code legalpathref}
+ * <dd>Directories containing legal licenses and notices to include in jmod.
+ * <dt>{@code nativelibpath}
+ * <dt>{@code nativelibpathref}
+ * <dd>Directories containing native libraries to include in jmod.
+ * <dt>{@code manpath}
+ * <dt>{@code manpathref}
+ * <dd>Directories containing man pages to include in jmod.
+ * <dt>{@code version}
+ * <dd>Module <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">version</a>.
+ * <dt>{@code mainclass}
+ * <dd>Main class of module.
+ * <dt>{@code platform}
+ * <dd>The target platform for the jmod.  A particular JDK's platform
+ * can be seen by running
+ * <code>jmod describe $JDK_HOME/jmods/java.base.jmod | grep -i platform</code>.
+ * <dt>{@code hashModulesPattern}
+ * <dd>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.
+ * <dt>{@code resolveByDefault}
+ * <dd>Boolean indicating whether the jmod should be one of
+ *     the default resolved modules in an application.  Default is true.
+ * <dt>{@code moduleWarnings}
+ * <dd>Whether to emit warnings when resolving modules which are
+ *     not recommended for use.  Comma-separated list of one of more of
+ *     the following:
+ *     <dl>
+ *     <dt>{@code deprecated}
+ *     <dd>Warn if module is deprecated
+ *     <dt>{@code leaving}
+ *     <dd>Warn if module is deprecated for removal
+ *     <dt>{@code incubating}
+ *     <dd>Warn if module is an incubating (not yet official) module
+ *     </dl>
+ * </dl>
+ *
+ * <p>
+ * Supported nested elements:
+ * <dl>
+ * <dt>{@code <classpath>}
+ * <dd>Path indicating where to locate files to be placed in the jmod file.
+ * <dt>{@code <modulepath>}
+ * <dd>Path indicating where to locate dependencies.
+ * <dt>{@code <commandpath>}
+ * <dd>Path of directories containing native commands to include in jmod.
+ * <dt>{@code <headerpath>}
+ * <dd>Path of directories containing header files to include in jmod.
+ * <dt>{@code <configpath>}
+ * <dd>Path of directories containing user-editable configuration files
+ *     to include in jmod.
+ * <dt>{@code <legalpath>}
+ * <dd>Path of directories containing legal notices to include in jmod.
+ * <dt>{@code <nativelibpath>}
+ * <dd>Path of directories containing native libraries to include in jmod.
+ * <dt>{@code <manpath>}
+ * <dd>Path of directories containing man pages to include in jmod.
+ * <dt>{@code <version>}
+ * <dd><a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">Module version</a> of jmod.
+ *     Must have a required {@code number} attribute.  May also have optional
+ *     {@code preRelease} and {@code build} attributes.
+ * <dt>{@code <moduleWarning>}
+ * <dd>Has one required attribute, {@code reason}.  See {@code moduleWarnings}
+ *     attribute above.  This element may be specified multiple times.
+ * </dl>
+ * <p>
+ * 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
+     * <code>jmod describe $JDK_HOME/jmods/java.base.jmod</code> 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<ResolutionWarningSpec> 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 <classpath>} 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.
+     * <p>
+     * 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 <modulePath>} 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 <moduleVersion> element is allowed.",
+                getLocation());
+        }
+        moduleVersion = new ModuleVersion();
+        return moduleVersion;
+    }
+
+    /**
+     * Attribute which specifies
+     * a <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">module version</a>
+     * for created jmod.
+     *
+     * @return module version for created jmod
+     */
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * Sets the <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/module/ModuleDescriptor.Version.html">module version</a>
+     * 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
+     * <var>OS</var>{@code -}<var>architecture</var>.  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
+     * <var>OS</var>{@code -}<var>architecture</var>.  If unset,
+     * current platform is used.
+     * <p>
+     * A JDK's platform can be viewed with a command like:
+     * <code>jmod describe $JDK_HOME/jmods/java.base.jmod | grep -i platform</code>.
+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<String, String> VALUES_TO_OPTIONS;
+
+        static {
+            Map<String, String> 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 <version>} 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 <version> 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<String> 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 <code>jmod</code> tool, based on this
+     * instance's current state.
+     *
+     * @return new list of <code>jmod</code> arguments
+     */
+    private Collection<String> buildJmodArgs() {
+        Collection<String> 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;
+    }
+}


Mime
View raw message