Scripting in Ant
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Ant build files are declarative, and most builds never need more than the tasks in Ant’s own library. When a build needs a
small piece of logic that no task provides, Ant can run a script in the middle of a build: inline in a <script> task,
as a new task defined with <scriptdef>, or as a condition, a selector, a filter or a mapper written in a scripting language.
This page covers all six elements, the two script-engine managers behind them, and the problem every reader meets on a
modern JDK: the JavaScript engine that used to ship with Java is gone. It is written against Apache Ant 1.10.18. Every example
was run on 2026-10-16 on macOS with Oracle JDK 26 (JBR 21.0.10 and JBR 17.0.14 were also used where stated), with the Ant
1.10.18 distribution unpacked in a scratch directory. Paths in the output were shortened to .. Where something could not
be run here it is labelled "not run here".
The elements are in Ant’s own ant.jar (the BSF manager is in ant-apache-bsf.jar), and none of them brings a language with it. The
language engine is always an extra JAR that you add, which is why this page spends most of its time on the class path. The related
extension mechanisms are macros and presets (Macros & Presets),
Java tasks (Writing Custom Tasks) and custom conditions and
selectors (Custom Types, Conditions & Selectors).
Choose a script when the logic is a few lines, a prototype, or specific to one build file.
The six script elements
| Element | Where it is used | What the script sees |
|---|---|---|
|
A task, anywhere a task is allowed. |
|
|
Defines a new task, then used like any task. |
|
|
A condition, wherever a condition is allowed ( |
The same beans as |
|
A selector inside a fileset (see Selectors). |
|
|
A filter in a |
|
|
A mapper (see Filters & Mappers). |
|
All six take the same core attributes, which are listed below once and then used throughout the page.
| Attribute | Effect |
|---|---|
|
Required. The language name as known to the script engine manager ( |
|
Since Ant 1.7. |
|
A file with the script, instead of nested text. The nested text is then not used. |
|
The encoding of the |
|
Whether to bind every property, reference and target as a variable. |
|
Where the language engine is found. If one is given, it becomes the context class loader of the script and the class loader given to the manager. |
<scriptdef> adds name (the new task’s name), uri (an XML namespace for it), compiled (Ant 1.10.2: compile the script
once if the manager is javax and the engine implements javax.script.Compilable), loaderRef (share the class loader
between several definitions), nested <attribute name="..." default="..."> (the default since Ant 1.10.13),
<element name="..." type="..." classname="...">, and any resource collection that supplies the script text.
<scriptcondition> adds value, the default result (false).
A JDK without a JavaScript engine
JavaScript was the language the Ant manual used for years, because Rhino and then Nashorn shipped with Java. Nashorn was
deprecated in Java 11 and removed in Java 15 (JEP 372). Since then a <script language="javascript"> fails on a plain JDK,
and so does any build file or example from a book written before 2020. A file with the simplest possible script:
<project name="js-demo" default="js">
<target name="js">
<script language="javascript">
var greeting = "hello from JavaScript";
project.log(greeting);
</script>
</target>
</project>
Run on JDK 26 with no extra JARs:
$ ant -f hello-js.xml
Buildfile: ./hello-js.xml
js:
Java 15 has removed Nashorn, you must provide an engine for running JavaScript yourself. GraalVM JavaScript currently is the preferred option.
BUILD FAILED
./hello-js.xml:3: Unable to create javax script engine for javascript
Total time: 0 seconds
The first line comes from Ant, which knows what happened. The same two lines appeared with JAVA_HOME set to JBR 21.0.10 and to
JBR 17.0.14, and a three-line check program (new ScriptEngineManager().getEngineFactories()) listed zero engines on all
three runtimes. The only other JDK installed on this machine was Oracle JDK 26, so no JDK from 11 to 14 (where Nashorn still exists)
could be tried here; that part is from the manual. There is no engine of any kind in a plain JDK, so every language needs a JAR.
Note that the error does not name the missing JAR. When a script fails with Unable to create javax script engine for <language>,
the language is spelled wrong, or its engine is not on the class path of the script, which is the next section.
Adding an engine
An engine JAR reaches a script in one of three ways, from the widest to the narrowest scope.
-
ANT_HOME/lib, or~/.ant/lib: every build of that installation, or of that user. -
-lib <dir-or-jar>on the command line: one run (see Running Ant). -
A
classpathorclasspathrefattribute (or a nested<classpath>) on the script element: that script only, which keeps the Ant installation free of language JARs and lets the build file say what it needs. The examples below use-libfor brevity and show theclasspathrefvariant once.
Groovy with JSR 223
Groovy implements javax.script, so <script language="groovy"> works with the JSR 223 manager. It needs two JARs from Maven Central,
org.apache.groovy:groovy and org.apache.groovy:groovy-jsr223 (the version of both is 6.0.0 here, the current release on
Central on 2026-10-16). The groovy-bsf module, which the Library Dependencies table of the 1.10.18 manual still lists for script and scriptdef, is for the older BSF manager and is not needed here. A
tiny Maven project fetches them, together with anything they depend on, into a directory (Maven 3.10.0, not part of the build
itself):
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId><artifactId>fetch-groovy</artifactId><version>1</version>
<dependencies>
<dependency><groupId>org.apache.groovy</groupId><artifactId>groovy</artifactId><version>6.0.0</version></dependency>
<dependency><groupId>org.apache.groovy</groupId><artifactId>groovy-jsr223</artifactId><version>6.0.0</version></dependency>
</dependencies>
</project>
$ cd fetch-groovy
$ mvn -q dependency:copy-dependencies -DoutputDirectory=../lib-groovy
$ ls -la ../lib-groovy
-rw-r--r--@ 1 albertoirurueta staff 9361435 Sep 19 03:01 groovy-6.0.0.jar
-rw-r--r--@ 1 albertoirurueta staff 23337 Sep 19 03:02 groovy-jsr223-6.0.0.jar
(The dependency tasks on Maven Resolver Ant Tasks are the way to do the same inside an Ant build, without the extra Maven project.)
Without the JARs, the failure is the same one Ant reports for JavaScript, but without the Nashorn explanation:
$ ant script
Buildfile: ./build.xml
script:
BUILD FAILED
./build.xml:6: Unable to create javax script engine for groovy
With -lib ../lib-groovy the build file below runs. It is the first of several targets in one build.xml that reads the
tidepool station list:
<project name="tidepool-scripting" default="all" basedir=".">
<property name="stations" value="data/stations.csv"/>
<target name="script">
<script language="groovy">
def rows = new File(project.baseDir, project.getProperty("stations")).readLines().drop(1)
project.setProperty("station.count", rows.size().toString())
self.log("loaded " + rows.size() + " stations")
</script>
<echo>station.count=${station.count}</echo>
</target>
...
</project>
$ ant -lib ../lib-groovy script
script:
[script] loaded 3 stations
[echo] station.count=3
BUILD SUCCESSFUL
self.log prints with the task name, as any task does, and project.setProperty creates a property that later tasks read
in the usual way, so a script is a handy way to compute a value. Which setter to call matters, because of Ant’s rule that a
property cannot be changed once set (see Properties); the next
section shows the difference.
The same -lib switch was repeated with JBR 21.0.10 and JBR 17.0.14 as the runtime and printed the same output, so Groovy 6.0.0
is not tied to JDK 26.
The script can be moved out of the build file with src (the encoding attribute matters only for this form), and the engine
can be named by a path in the build file instead of -lib. This build file and a one-line hello.groovy
(self.log("script loaded from a file, encoding utf-8")) were run with no -lib at all:
<project name="src-demo" default="hello">
<path id="groovy.path">
<fileset dir="../lib-groovy" includes="*.jar"/>
</path>
<target name="hello">
<script language="groovy" classpathref="groovy.path" src="hello.groovy" encoding="UTF-8"/>
</target>
</project>
hello:
[script] script loaded from a file, encoding utf-8
JavaScript with GraalJS
The manual’s recommendation is the GraalVM JavaScript engine, which ships as ordinary Maven artifacts (no GraalVM installation is needed).
Three artifacts, all version 25.4.4.1.1 (current on Central on 2026-10-16), cover it:
org.graalvm.polyglot:polyglot, org.graalvm.polyglot:js-community (a POM artifact whose dependencies are the language
implementation) and org.graalvm.js:js-scriptengine. Fetched with the same kind of POM as for Groovy, with
<type>pom</type> on js-community, they resolved to 13 JARs and 67 MB (the two POMs that the copy step also leaves in the directory were deleted). The
directory was called lib-graal, and the failing build file from above was run again:
$ ant -lib ../lib-graal -f hello-js.xml
Buildfile: ./hello-js.xml
js:
WARNING: A restricted method in java.lang.System has been called
...
[script] [engine] WARNING: The polyglot engine uses a fallback runtime that does not support runtime compilation to native code.
[script] Your Java runtime '26+35-2893' is incompatible with optimized Truffle runtime version '25.4.4.1.1'.
[script] The Java runtime version must be greater or equal to JDK '25' and smaller than JDK '26'.
...
hello from JavaScript
BUILD SUCCESSFUL
The script ran. The output above is shortened with ... because on JDK 26 the engine prints about twenty lines of warnings first. They
have two causes, both specific to this JDK: Truffle’s optimising runtime accepts JDK 25 only in this release, so it
falls back to the interpreter, and the JDK warns about native access and sun.misc.Unsafe use by the engine. They are harmless for build scripts. They can be
silenced through ANT_OPTS:
$ export ANT_OPTS="-Dpolyglot.engine.WarnInterpreterOnly=false --enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow"
On JBR 21.0.10 and JBR 17.0.14 the same build printed none of these warnings. Always check the warning text of the engine on the JDK you actually use, since the engine version and the JDK version are coupled.
The manual’s own JavaScript example, changed only to give the project a name that is a valid JavaScript identifier, creates
echo tasks from the script:
<project name="squares" default="main" basedir=".">
<target name="main">
<script language="javascript"> <![CDATA[
for (i = 1; i <= 3; i++) {
echo = squares.createTask("echo");
echo.setMessage(i*i);
echo.perform();
}
]]> </script>
</target>
</project>
main:
[echo] 1
[echo] 4
[echo] 9
The project name matters: with setbeans on, the project is bound under its name, but only when that is a valid identifier for the
language. With a project called js-squares the script js_squares.createTask("echo") failed with:
BUILD FAILED
./hy.xml:3: ReferenceError: ReferenceError: "js_squares" is not defined
project is always available and does not depend on the name; prefer it in scripts you want to copy to other builds.
The same rule applies to property names: a property such as build.dir is not a valid variable, and so is not bound as one. Read it with
project.getProperty("build.dir").
The Nashorn compatibility mode
When the engine is GraalJS, Ant switches on the option polyglot.js.allowAllAccess, so scripts can use Ant’s objects and Java classes. By default it also enables GraalJS’s
Nashorn compatibility mode. That mode is narrower than it sounds. The Rhino/Nashorn habit of importClass(java.io.File) is not
enabled by it. The two targets used for these checks, in the same js.xml, differ only in the first line:
<target name="compat">
<script language="javascript"> <![CDATA[
importClass(java.io.File);
var f = new File("data/stations.csv");
project.log("size of " + f.getName() + " = " + f.length());
]]> </script>
</target>
<target name="moz">
<script language="javascript"> <![CDATA[
load("nashorn:mozilla_compat.js");
importClass(java.io.File);
var f = new File("data/stations.csv");
project.log("size of " + f.getName() + " = " + f.length());
]]> </script>
</target>
compat:
BUILD FAILED
./js.xml:12: ReferenceError: ReferenceError: "importClass" is not defined
What the compatibility mode does provide, as run here, is the Nashorn compat script loader. With
load("nashorn:mozilla_compat.js"); before importClass(java.io.File) the script printed size of stations.csv = 239. The same script with the magic property
ant.disable.graal.nashorn.compat set to true (ant -Dant.disable.graal.nashorn.compat=true) failed:
moz:
BUILD FAILED
./js.xml:26: TypeError: TypeError: Cannot load script: nashorn:mozilla_compat.js
The portable way, which works with the compatibility mode on or off, is Java.type:
<script language="javascript"> <![CDATA[
var File = Java.type('java.io.File');
var f = new File("data/stations.csv");
project.log("size of " + f.getName() + " = " + f.length());
]]> </script>
type:
size of stations.csv = 239
The manual offers Java.type and the compatibility script load("nashorn:mozilla_compat.js") as equal options; this page’s recommendation is to write Java.type on a new build and leave the compatibility script out. GraalJS is not a drop-in
replacement for Nashorn, and the GraalVM project has a migration guide for the differences.
Other languages
The manual’s examples also show BeanShell, JRuby, Jython, JudoScript, Rexx (NetRexx) and TCL (jacl, jtcl). They were not run
here; the language JAR for each is listed in the manual’s "Library Dependencies" table. Rhino JavaScript is the one the manual ties to BSF. Ant’s side of the BSF
manager, ant-apache-bsf.jar, is in ANT_HOME/lib already; the BSF JAR itself is not.
BSF and JSR 223
Ant has two ways to talk to a language:
-
JSR 223 (
javax.script, since Java 6) is part of the JDK. It looks the engine up by language name, using theServiceLoadermechanism, so a JAR with aMETA-INF/services/javax.script.ScriptEngineFactoryentry is enough. -
Apache BSF (Bean Scripting Framework) is an older library with its own engine registry. Ant 1.6 and later need BSF 2.3.0 or later, and 2.4.0 to use Rhino 1.5R4 or later; BSF 2.4.0 also needs
commons-logging.
The manager attribute chooses between them. auto, the default, uses BSF when its classes can be loaded (the manual describes
it so), and javax otherwise. Practical consequences:
-
Without a BSF JAR,
autois the same asjavax. This is the normal situation today: JSR 223 engines exist for the languages used in builds (Groovy and GraalJS were run here), and nothing in this page needed BSF. -
manager="javax"is the explicit choice when both are installed, which avoids a surprise if a BSF JAR is ever added toANT_HOME/lib. Running the Groovy build withmanager="javax"printed the same lines as the default. -
manager="bsf"without BSF fails, and the message names the missing class:
script:
BUILD FAILED
./bsf.xml:6: Unable to load script engine manager (org.apache.bsf.BSFManager)
BSF with a working engine was not run here.
The beans bound for a script are the same under both managers.
Source: Ant manual, script
How a script sees the build
Inside a script, the Ant objects are the ordinary Java API (see Embedding Ant for the same classes used from a Java program):
-
projectis theorg.apache.tools.ant.Project:getProperty,setProperty,setNewProperty,getBaseDir,log,createTask,createDataType,getReference,getTargets,executeTarget. -
selfis the script element itself: for<script>, theScripttask; for<scriptdef>, theScriptDefBase(withlog,fail,text). -
With
setbeanson, each property, reference id and target is bound by its name, sostationsin the script above is the property of that name. A name that is not a valid identifier in the language cannot be used as a variable: the project namejs-squaresabove, and a property with dots such asa.b.
The effect was checked with Groovy on properties a.b, a_b and keep (value original):
t:
[script] a_b = underscore
[script] a.b not bound as a.b
[script] script variable keep = changed by assignment
[script] project.getProperty('keep') = original
[script] after setProperty = set by setProperty
[script] after setNewProperty = set by setProperty
So a_b is a variable and a.b is not (read it with project.getProperty("a.b")); assigning to the bound variable keep changes the script’s own variable and
leaves the Ant property alone; project.setProperty does overwrite the property (a user property given with -D would not be overwritten); and
project.setNewProperty follows Ant’s rule and is ignored once the property exists, as the <property> task is. Use setNewProperty for properties a user may override,
and setProperty only to deliberately replace a value. With setbeans="false" only project and self exist:
t:
[script] setbeans=true : stations=data/stations.csv
[script] setbeans=false: No such property: stations for class: Script2
[script] setbeans=false: via project -> data/stations.csv
Prefer project for reading and setting properties, self.log for logging, and createTask and createDataType for tasks and
types. The following target, Groovy again, creates and runs three echo tasks from a list, the Groovy counterpart of the manual’s squares example:
<target name="script-tasks">
<script language="groovy">
["SF", "BOS", "HNL"].each { id ->
def e = project.createTask("echo")
e.setMessage("station id " + id)
e.perform()
}
</script>
</target>
script-tasks:
[echo] station id SF
[echo] station id BOS
[echo] station id HNL
The manual’s example uses perform() rather than execute(): perform() is the method Ant itself calls, and it wraps execute() with the
build events and the logging. The same applies to any task created by a script.
When a script creates targets itself, the manual asks that each get a distinct location, so that error messages can name them.
Source: Ant manual, script
scriptdef
<scriptdef> defines a task whose body is a script. It is the next step after a macrodef, when the logic needs real
code: parsing, conditionals over several values, or loops, and when a prototype of a Java task is wanted. The
tidepool example reads a row from stations.csv and logs one column of it:
<scriptdef name="stationinfo" language="groovy">
<attribute name="id"/>
<attribute name="column" default="name"/>
<element name="fileset" type="fileset"/>
<![CDATA[
def id = attributes.get("id")
if (id == null) self.fail("stationinfo needs an id")
def col = ["id": 0, "name": 1, "latitude": 2, "longitude": 3, "constituents": 4]
def idx = col[attributes.get("column")]
if (idx == null) self.fail("unknown column " + attributes.get("column"))
def line = new File(project.baseDir, project.getProperty("stations")).readLines().drop(1).find { it.startsWith(id + ",") }
if (line == null) self.fail("no station " + id)
self.log(id + " " + attributes.get("column") + " = " + line.split(",")[idx])
def sets = elements.get("fileset")
self.log("filesets passed: " + (sets == null ? 0 : sets.size()))
]]>
</scriptdef>
<target name="scriptdef">
<stationinfo id="BOS"/>
<stationinfo id="HNL" column="latitude">
<fileset dir="src"/>
<fileset dir="data"/>
</stationinfo>
</target>
scriptdef:
[stationinfo] BOS name = Boston
[stationinfo] filesets passed: 0
[stationinfo] HNL latitude = 21.3067
[stationinfo] filesets passed: 2
The rules the example uses:
-
attributesis a map from the attribute name to its value. Attribute and element names are lower-cased by Ant, so writeget("column")even if the definition saysColumn. An attribute that was not given and has nodefaultisnull, which is why the script checksid. Thedefaultattribute exists since Ant 1.10.13; in earlier versions a script had to supply the default itself. -
elementsmaps a nested element name to the list of the instances found, ornullwhen there are none, so a loop over a missing element needs the null check shown. An element is declared withtype(an Ant type or task name, with the namespace URI and:in front when it has one, for exampleantlib:example.org:newtype) or withclassname. -
self.fail(message)raises aBuildException.self.textis the nested text of the call, andself.loglogs under the new task’s name. This is why the output lines start with[stationinfo]. -
The
uriattribute puts the new task into an XML namespace, which callers must then use. The manual declares scripts in a new namespace so that Ant cannot confuse them with a task of the same name; its example defines anechothat way. -
A script is evaluated at each call.
compiled="true"compiles it once when the manager and the engine allow it. Groovy’s engine supports compilation; the same target withcompiled="true"printed identical output. Nothing in the output tells compiled and interpreted apart, so measure before assuming a speedup.
Failing is as easy as calling a task with an unknown id:
$ ant -lib ../lib-groovy scriptdef-error
scriptdef-error:
BUILD FAILED
./build.xml:53: javax.script.ScriptException: no station XXX
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:469)
...
Caused by: no station XXX
at org.apache.tools.ant.taskdefs.optional.script.ScriptDefBase.fail(ScriptDefBase.java:129)
The exception is the engine’s wrapper around the BuildException, and it prints a stack trace, which is long (the rest is omitted above). The line number
in the message is the call site, line 53 of build.xml, and not a line in the script. A syntax error is also found only when the script is executed,
not when the project is parsed. With a truncated expression in a scriptdef, the target below printed its first task and then failed at the call:
t:
[echo] before
BUILD FAILED
./syn.xml:5: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Script1.groovy: 2: Missing ')' @ line 2, column 16.
The manual says that the message names the scriptdef for some engines; Groovy’s did not.
<scriptdef> also takes src, classpath and loaderRef, and any resource collection as the script text (since Ant 1.7.1).
loaderRef lets several definitions share one class loader, which matters when the language engine is not on the Ant class path.
Source: Ant manual, scriptdef
scriptcondition
<scriptcondition> is a condition (see Conditions & Control Flow)
usable inside <condition>, <waitfor> and wherever a nested condition is allowed. It has the default result value
and decides in one of two ways: the script’s last value, if it is a boolean (since Ant 1.7.1), or a call to self.setValue(boolean).
<target name="condition">
<condition property="many.stations">
<scriptcondition language="groovy" value="false">
self.setValue(new File(project.baseDir, project.getProperty("stations")).readLines().size() > 3)
</scriptcondition>
</condition>
<echo>many.stations=${many.stations}</echo>
<condition property="boolean.result">
<scriptcondition language="groovy">
project.getProperty("stations").endsWith(".csv")
</scriptcondition>
</condition>
<echo>boolean.result=${boolean.result}</echo>
</target>
condition:
[echo] many.stations=true
[echo] boolean.result=true
(The file has a header and three stations, four lines, hence many.stations is true.) The <condition> task sets the property only
when the condition is true, so a false result leaves it unset and ${many.stations} stays literally as written. Details of that pitfall are on the conditions page.
setbeans for this element has been available since Ant 1.8.0.
Source: Ant manual, scriptcondition
scriptselector
<scriptselector> goes inside a fileset (or any selector container) and is called once for every candidate file. The beans are self, filename (a String),
file (a java.io.File) and basedir, besides the usual ones. Only selected can be written, through self.setSelected(boolean).
<target name="selector">
<delete dir="out"/>
<copy todir="out" overwrite="true">
<fileset dir="src">
<scriptselector language="groovy">
self.setSelected(filename.startsWith("b"))
</scriptselector>
</fileset>
</copy>
<fileset id="sel" dir="out"/>
<echo>selected: ${toString:sel}</echo>
</target>
selector:
[delete] Deleting directory ./out
[copy] Copying 1 file to ./out
[echo] selected: b.txt
Of the two files in src, a.txt and b.txt, only b.txt was selected. A script selector costs one script evaluation per file,
so for large trees prefer the built-in selectors (Selectors) and use a script only for the
rule that none of them can express. <scriptselector> has existed since Ant 1.7.
Source: Ant manual, scriptselector
scriptfilter
<scriptfilter> works on a token at a time: inside <tokenfilter>, a token is a line by default. The script gets self, with
getToken() and setToken(String). Setting the token to null deletes it from the output. It can also stand directly in a <filterchain>, where it
sees the content in the way the chain splits it. The next target upper-cases the lines of two text files and drops any line that contains bad:
<target name="filter">
<delete dir="out"/>
<copy todir="out">
<fileset dir="src" includes="**/*.txt"/>
<filterchain>
<tokenfilter>
<scriptfilter language="groovy">
if (self.getToken().contains("bad")) self.setToken(null)
else self.setToken(self.getToken().toUpperCase())
</scriptfilter>
</tokenfilter>
</filterchain>
</copy>
<concat><fileset dir="out"/></concat>
</target>
a.txt holds hello, this is bad and world; b.txt holds second file.
filter:
[copy] Copying 2 files to ./out
[concat] HELLO
[concat] WORLD
[concat] SECOND FILE
The class TokenFilter.Filter is the Java way to write the same filter; the filterchain page shows custom string filters and tokenizers.
Source: Ant manual, scriptfilter
scriptmapper
<scriptmapper> maps a source name to any number of target names. It is called once for every name, with source bound to the name, and the
mapped names are collected with self.addMappedName(String). The list is reset after every call (self.clear() does it by hand). It has no type
attribute for <mapper>: it is used as a nested element, wherever a mapper is expected.
The element is used here inside <pathconvert>, which shows all mapped names. Under <pathconvert>, source was the absolute
path of each file (found out by printing it), which is why the script takes the file’s name with new File(source).name:
<target name="mapper">
<fileset id="srcfiles" dir="src"/>
<pathconvert property="mapped.names" refid="srcfiles" pathsep=", ">
<scriptmapper language="groovy">
self.addMappedName(new File(source).name.replace(".txt", ".orig"))
self.addMappedName("copy-of-" + new File(source).name)
</scriptmapper>
</pathconvert>
<echo>${mapped.names}</echo>
</target>
mapper:
[echo] a.orig, copy-of-a.txt, b.orig, copy-of-b.txt
Two names per source file. Whether a task uses all of them is the task’s decision. <copy> uses the first one by default; with
enablemultiplemappings="true" it uses all of them. The same <scriptmapper> inside <copy todir="out">, listing the result with ${toString:result}:
$ ant -Dmulti=false ...
[copy] Copying 2 files to ./out
[echo] a.orig;b.orig
$ ant -Dmulti=true ...
[copy] Copying 2 files to ./out
[copy] Copied 1 empty directory to 1 empty directory under ./out
[echo] a.orig;b.orig;copy-of-a.txt;copy-of-b.txt
(The extra "empty directory" line is copy reporting the src directory itself, which the fileset also matches.) The same attribute exists on
mappedresources, described on Resource Collections.
Source: Ant manual, scriptmapper
Choosing a language
| Language | Strength | Cost |
|---|---|---|
Groovy (JSR 223) |
Close to Java syntax, one 9 MB engine, run here on JDK 17, 21 and 26, full access to Java and Ant classes. See Integrating Groovy into Java Applications. |
A new language in the build file for readers who know only Java. |
JavaScript (GraalJS) |
The language of the manual’s examples; widely known. |
67 MB of JARs, engine version tied to the JDK version (warnings on JDK 26), not a drop-in for Nashorn. |
JRuby, Jython, BeanShell, others |
Existing code or team preference. Listed in the manual. |
Not run here. Check that the engine supports the Java release you use. |
In general: if the logic is longer than about twenty lines, or is used by more than one build, write a Java task (the next step is Writing Custom Tasks) so that it can be compiled, tested with AntUnit, and used from IDEs.
Security and maintenance
-
A script runs with the full permissions of the JVM and can read files, run programs and open network connections. Inline scripts in a build file you did not write deserve the same review as a Java task, and a build file that takes a
srcpath from a property is only as safe as that property. Scripts in untrusted builds should not be run at all. -
The engine JAR is part of the supply chain. Pin its version, take it from Maven Central and verify it as for any dependency (see Supply Chain Security).
-
The engine can change under the build. A Java upgrade can break a script that worked (as Java 15 did for every JavaScript script), so keep the engine JAR next to the build file (a
libdirectory used with-lib, or aclasspathref) and not in a sharedANT_HOME/lib. -
In the other tools: Gradle build scripts are programs in Groovy or Kotlin, so the question hardly arises there (see Ant Integration for calling Ant from Gradle); Maven can run Ant tasks, scripts included, through the
maven-antrun-plugin.
Where the books and older build files are outdated
-
Every JavaScript example in material from before 2020 assumes Nashorn or Rhino. It needs GraalJS (or another engine) on the class path today and, for Rhino idioms, rewriting with
Java.type. -
importPackageandimportClassare not defined by the default Nashorn compatibility mode of GraalJS, as shown above. -
Examples calling
<script language="beanshell">or other languages never say which JAR is required; the library table of the manual does. -
<scriptdef>before Ant 1.10.13 had nodefaulton<attribute>and nosetbeans.
References
-
Apache Ant manual: script
-
Apache Ant manual: scriptdef
-
Apache Ant manual: scriptcondition
-
Apache Ant manual: scriptselector
-
Apache Ant manual: scriptfilter
-
Apache Ant manual: scriptmapper
-
Apache Ant manual: Library Dependencies
-
GraalVM: GraalVM JavaScript
-
GraalVM: Nashorn migration guide
-
Apache Groovy: groovy-lang.org