Ant Conditions & Control Flow

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.

This page is written against Apache Ant 1.10.18. Every example was run on JDK 26 on 2026-10-16 with that release and the JARs in its lib directory, nothing else on the class path. The examples use the tidepool scenario of this section (data/stations.csv and the station directories SF, BOS and HNL) in two throwaway build files that are not part of the scenario itself. The network conditions were run against a local web server on localhost. The absolute scratch path in console output was shortened to /work/tidepool.

Ant has no if statement. Decisions are made in three places: a condition computes a boolean and the condition, waitfor and fail tasks act on it, targets are switched on and off by properties (if and unless), and a few container tasks (parallel, sequential, retry) arrange when and how often nested tasks run. This page explains each and ends with the question that every Ant user eventually asks: how do I loop?

Properties are covered on Properties, and the target-level if and unless attributes on Targets & Extension Points. Utility tasks such as sleep, tstamp and input are on Utility Tasks, and get and the other remote tasks on Network & Remote Tasks.

The condition task

condition is the generalisation of available and uptodate: it sets a property when a nested condition holds. Exactly one condition must be nested, and and, or, not and xor let you combine several.

Attribute Meaning Default

property

The property to set. Required.

value

The value to set when the condition is true.

true

else

The value to set when the condition is false (Ant 1.6.3). Without it the property is left unset when the condition is false.

unset

Leaving the property unset when the condition is false is what makes the if and unless attributes of targets work, and it is also the source of the classic bug: an unset property is not false, it is absent, and ${name} stays literally as ${name} in any attribute that refers to it. Use else="false" when you want an explicit value.

The same set of conditions is accepted by waitfor and by the nested <condition> of fail, and custom conditions (see Custom Types, Conditions & Selectors) can be used anywhere a core condition can.

A helper that prints any condition

The examples evaluate many conditions, so a macro runs one nested condition and prints its result. Because it sets the property with else="false", every outcome is visible. See Macros & Presets for macrodef.

<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool-conditions" default="logic" basedir=".">

  <!-- evaluate any nested condition and print the result -->
  <macrodef name="check">
    <attribute name="name"/>
    <element name="test" implicit="true"/>
    <sequential>
      <condition property="r.@{name}" value="true" else="false">
        <test/>
      </condition>
      <echo message="@{name}: ${r.@{name}}"/>
    </sequential>
  </macrodef>

  <property name="stations" value="data/stations.csv"/>
  <property name="empty.prop" value=""/>
  <property name="flag" value="yes"/>

Core conditions

The manual lists the nested elements below under "Core Conditions". Attributes not shown in the table are on the manual page.

Condition What it tests Since

not, and, or, xor

Boolean combinators. and and or evaluate in document order and stop at the first deciding result, like Java’s && and ||. xor has no shortcut and is true when an odd number of children are true. not takes exactly one child.

available

The available task as a condition: a file, directory, class or resource exists.

uptodate

The uptodate task as a condition: targets are not older than sources.

os

Operating system by family (windows, unix, mac, dos, netware, os/2, tandem, win9x, winnt, z/os, os/400, openvms), name, arch and version. All given attributes must match.

equals

arg1 equals arg2; casesensitive (default true), trim (default false), forcestring.

forcestring 1.8.1

isset

The property named by property is set.

checksum

The checksum task as a condition. Here property holds the expected checksum value, not a property name.

http

The URL answers with a status below errorsBeginAt (default 400). requestMethod, followRedirects, readTimeout.

requestMethod 1.8.0, followRedirects 1.9.7, readTimeout 1.10.6

socket

Something listens on server and port.

filesmatch

Two files are identical byte for byte, or with line endings ignored if textfile="true".

textfile 1.7

contains

string contains substring; casesensitive (default true).

istrue, isfalse

value is one of true, yes, on (or is not).

isreference

A reference refid exists, optionally of the given type.

1.6

issigned

A JAR has a signature (any, or the one named name); it does not validate it.

1.7

isfileselected

A file passes the nested selector.

1.6.3

typefound

A task or type name (with optional uri) is defined and its class can be loaded.

1.7

scriptcondition

A script decides. Needs a script engine.

1.7

parsersupports

The XML parser supports a SAX or JAXP feature or property.

1.7

isreachable

A host or the host of a url answers within timeout seconds (default 30); an unknown host is false, not an error.

1.7

length

The length task as a condition, for a string or files.

1.6.3

isfailure

An executable’s return code signals failure.

1.7

resourcecount

The size of a resource collection compares with count using when.

1.7

resourcesmatch

The content of the nested resources is identical; astext ignores line endings.

1.7

resourcecontains

A resource or refid contains substring.

1.7.1

hasmethod

A class has a method or field; a class that cannot be loaded fails the build.

1.7

matches

string matches pattern or a nested regexp; casesensitive, multiline, singleline.

1.7

antversion

The antversion task as a condition: atleast or exactly a version.

1.7

hasfreespace

partition has at least needed free space (K, M, G, T, P).

1.7.0

islastmodified

The modification time of a nested resource compares with millis or datetime using mode (equals, before, not-before, after, not-after).

1.8.0

resourceexists

A nested resource exists.

1.8.0

javaversion

The JVM running Ant is atleast, atmost (1.10.10) or exactly a version. Since Java 9 only the major number is compared.

1.10.2

canCreateSymlink

The process can create symbolic links.

1.10.16

Logic and strings

<target name="logic">
  <check name="and"><and><isset property="flag"/><equals arg1="${flag}" arg2="yes"/></and></check>
  <check name="or"><or><isset property="nope"/><isset property="flag"/></or></check>
  <check name="not"><not><isset property="nope"/></not></check>
  <check name="xor-two-true"><xor><isset property="flag"/><isset property="stations"/></xor></check>
  <check name="xor-one-true"><xor><isset property="flag"/><isset property="nope"/></xor></check>
  <check name="equals-case"><equals arg1="M2" arg2="m2"/></check>
  <check name="equals-ignore-case"><equals arg1="M2" arg2="m2" casesensitive="false"/></check>
  <check name="equals-trim"><equals arg1="  M2 " arg2="M2" trim="true"/></check>
  <check name="contains"><contains string="SF,San Francisco" substring="francisco" casesensitive="false"/></check>
  <check name="istrue-yes"><istrue value="${flag}"/></check>
  <check name="istrue-unset"><istrue value="${nope}"/></check>
  <check name="isfalse-unset"><isfalse value="${nope}"/></check>
  <check name="isfalse-off"><isfalse value="off"/></check>
  <check name="matches"><matches string="M2:0.55:120" pattern="^[A-Z][A-Z0-9]*:\d+\.\d+:\d+$$"/></check>
  <check name="matches-multiline"><matches string="a${line.separator}b" pattern="^b$$" multiline="true"/></check>
  <check name="length-string"><length string=" M2 " trim="true" length="2"/></check>
  <check name="length-file"><length file="${stations}" when="greater" length="0"/></check>
</target>
$ ant -q logic
     [echo] and: true
     [echo] or: true
     [echo] not: true
     [echo] xor-two-true: false
     [echo] xor-one-true: true
     [echo] equals-case: false
     [echo] equals-ignore-case: true
     [echo] equals-trim: true
     [echo] contains: true
     [echo] istrue-yes: true
     [echo] istrue-unset: false
     [echo] isfalse-unset: true
     [echo] isfalse-off: true
     [echo] matches: true
     [echo] matches-multiline: true
     [echo] length-string: true
     [echo] length-file: true
BUILD SUCCESSFUL
Total time: 0 seconds

Three results are worth reading twice. xor-two-true is false because two children are true; xor is not "exactly one". The istrue-unset and isfalse-unset pair shows how an unset property is handled: ${nope} stays the literal string ${nope}, which is not one of true, yes and on, so istrue is false and isfalse is true. And equals-trim needs trim="true" to ignore the spaces around M2. In the matches patterns the end-of-line anchor is written $$ because a single dollar sign starts a property reference in a build file.

Files and resources

<target name="files">
  <check name="resourceexists"><resourceexists><file file="${stations}"/></resourceexists></check>
  <check name="filesmatch-same"><filesmatch file1="${stations}" file2="data/stations-copy.csv"/></check>
  <check name="filesmatch-different"><filesmatch file1="${stations}" file2="data/copy.csv"/></check>
  <check name="filesmatch-line-endings"><filesmatch file1="data/crlf.csv" file2="data/lf.csv"/></check>
  <check name="filesmatch-textfile"><filesmatch file1="data/crlf.csv" file2="data/lf.csv" textfile="true"/></check>
  <check name="resourcecontains"><resourcecontains resource="${stations}" substring="Honolulu"/></check>
  <check name="resourcecount"><resourcecount count="3"><dirset dir="stations" includes="*"/></resourcecount></check>
  <check name="isfileselected"><isfileselected file="${stations}"><size value="100" when="more"/></isfileselected></check>
  <check name="islastmodified"><islastmodified datetime="01/01/2020 12:00 AM" mode="after"><file file="${stations}"/></islastmodified></check>
  <checksum file="${stations}" algorithm="SHA-256" property="stations.sha256"/>
  <check name="checksum"><checksum file="${stations}" algorithm="SHA-256" property="${stations.sha256}"/></check>
  <check name="checksum-wrong"><checksum file="${stations}" algorithm="SHA-256" property="0000"/></check>
  <check name="canCreateSymlink"><canCreateSymlink/></check>
  <check name="hasfreespace"><hasfreespace partition="/" needed="1M"/></check>
  <check name="isreference-no"><isreference refid="no.such.ref"/></check>
  <path id="cp"><pathelement location="data"/></path>
  <check name="isreference-path"><isreference refid="cp" type="path"/></check>
  <check name="isreference-wrong-type"><isreference refid="cp" type="fileset"/></check>
  <check name="issigned"><issigned file="${ant.library.dir}/ant.jar"/></check>
</target>
$ ant -q files
     [echo] resourceexists: true
     [echo] filesmatch-same: true
     [echo] filesmatch-different: false
     [echo] filesmatch-line-endings: false
     [echo] filesmatch-textfile: true
     [echo] resourcecontains: true
     [echo] resourcecount: true
     [echo] isfileselected: true
     [echo] islastmodified: true
     [echo] checksum: true
     [echo] checksum-wrong: false
     [echo] canCreateSymlink: true
     [echo] hasfreespace: true
     [echo] isreference-no: false
     [echo] isreference-path: true
     [echo] isreference-wrong-type: false
     [echo] issigned: false
BUILD SUCCESSFUL
Total time: 0 seconds

filesmatch-line-endings is false and filesmatch-textfile is true for the same two files, which differ only in CRLF versus LF line endings: set textfile="true" when a file may have been checked out with another platform’s line endings. The resourcecount condition counts the three station directories only because a dirset was used; a fileset of the same directory would have counted zero, since a fileset selects files. isreference-wrong-type is false because the reference exists but is a path, not a fileset, and issigned is false for the unsigned ant.jar.

Environment, versions and types

<target name="environment">
  <check name="os-family-unix"><os family="unix"/></check>
  <check name="os-family-mac"><os family="mac"/></check>
  <check name="os-family-windows"><os family="windows"/></check>
  <check name="os-family-mac-and-not-unix"><and><os family="mac"/><not><os family="unix"/></not></and></check>
  <check name="javaversion-atleast-21"><javaversion atleast="21"/></check>
  <check name="javaversion-exactly-26"><javaversion exactly="26"/></check>
  <check name="javaversion-atmost-17"><javaversion atmost="17"/></check>
  <check name="antversion-atleast-1.10.2"><antversion atleast="1.10.2"/></check>
  <check name="antversion-exactly-1.10.18"><antversion exactly="1.10.18"/></check>
  <check name="typefound-junitlauncher"><typefound name="junitlauncher"/></check>
  <check name="typefound-checkstations"><typefound name="checkstations"/></check>
  <check name="hasmethod"><hasmethod classname="java.util.ArrayList" method="trimToSize"/></check>
  <check name="hasmethod-missing"><hasmethod classname="java.util.ArrayList" method="shrinkToFit"/></check>
  <check name="parsersupports"><parsersupports feature="http://xml.org/sax/features/namespaces"/></check>
  <check name="available-class"><available classname="java.util.HexFormat"/></check>
  <check name="available-file"><available file="${stations}" type="file"/></check>
  <check name="available-dir"><available file="data" type="dir"/></check>
  <check name="available-resource"><available resource="org/apache/tools/ant/taskdefs/defaults.properties"/></check>
  <exec executable="false" resultproperty="rc" failonerror="false"/>
  <check name="isfailure"><isfailure code="${rc}"/></check>
</target>
$ ant -q environment
     [echo] os-family-unix: true
     [echo] os-family-mac: true
     [echo] os-family-windows: false
     [echo] os-family-mac-and-not-unix: false
     [echo] javaversion-atleast-21: true
     [echo] javaversion-exactly-26: true
     [echo] javaversion-atmost-17: false
     [echo] antversion-atleast-1.10.2: true
     [echo] antversion-exactly-1.10.18: true
     [echo] typefound-junitlauncher: true
     [echo] typefound-checkstations: false
     [echo] hasmethod: true
     [echo] hasmethod-missing: false
     [echo] parsersupports: true
     [echo] available-class: true
     [echo] available-file: true
     [echo] available-dir: true
     [echo] available-resource: true
     [echo] isfailure: true
BUILD SUCCESSFUL
Total time: 0 seconds

The isfailure check uses the Unix false command, which always exits with a non-zero code, so it needs a Unix-like system. os-family-mac and os-family-unix are both true on the macOS machine used for this page, so os-family-mac-and-not-unix is false: the manual’s isMacOsButNotMacOsX example (family mac and not family unix) matches only the classic Mac OS, because macOS is in the unix family as well. typefound-junitlauncher is true because ant-junitlauncher.jar is in lib, while typefound-checkstations is false because the <checkstations> task of the tidepool scenario has not been defined here (see Writing Custom Tasks). The javaversion result for exactly="26" is true because the examples ran on JDK 26. Use javaversion and antversion in a guard target to fail early with a readable message instead of with a class-format error halfway through the build.

Network conditions against localhost

The conditions below were run against a Python http.server started on port 8765 in the directory that holds data/stations.csv; nothing on the Internet was contacted.

<target name="network">
  <check name="http-ok"><http url="http://localhost:${port}/data/stations.csv"/></check>
  <check name="http-404"><http url="http://localhost:${port}/missing.csv"/></check>
  <check name="http-404-errorsBeginAt"><http url="http://localhost:${port}/missing.csv" errorsBeginAt="500"/></check>
  <check name="http-head"><http url="http://localhost:${port}/data/stations.csv" requestMethod="HEAD"/></check>
  <check name="socket-open"><socket server="localhost" port="${port}"/></check>
  <check name="socket-closed"><socket server="localhost" port="9"/></check>
  <check name="isreachable-localhost"><isreachable host="localhost" timeout="2"/></check>
</target>
$ ant -q -Dport=8765 network
     [echo] http-ok: true
     [echo] http-404: false
     [echo] http-404-errorsBeginAt: true
     [echo] http-head: true
     [echo] socket-open: true
     [echo] socket-closed: false
     [echo] isreachable-localhost: true
BUILD SUCCESSFUL
Total time: 0 seconds

http-404 is false because the default errorsBeginAt is 400, and raising it to 500 makes the same 404 response count as healthy: the condition checks the status class, not that the page has the content you expect. socket-closed tests port 9, which nothing listens on. A socket or http condition used in a waitfor is the normal way to wait for a server to come up. isreachable to a remote host depends on the network and on firewalls, because the JVM may use ICMP or a TCP echo probe, so prefer socket or http against the port you actually need.

scriptcondition needs an engine

<target name="scriptcondition">
  <condition property="r">
    <scriptcondition language="javascript" value="true">self.setValue(false);</scriptcondition>
  </condition>
</target>
$ ant -q scriptcondition
Java 15 has removed Nashorn, you must provide an engine for running JavaScript yourself. GraalVM JavaScript currently is the preferred option.
BUILD FAILED
/work/tidepool/build.xml:96: Unable to create javax script engine for javascript
Total time: 0 seconds
$ echo $?
1

The manual’s scriptcondition example uses JavaScript, which the JDK no longer provides since Java 15. Use a built-in condition, a custom condition class, or add a script engine as described on Scripting.

available and uptodate

available and uptodate are the two oldest condition-like tasks. They set a property instead of failing.

available takes exactly one of classname, file or resource, plus classpath, filepath, classpathref, type (dir or file), ignoresystemclasses and searchparents (1.7), and property with an optional value. uptodate compares one srcfile or nested <srcfiles> (a fileset) or <srcresources> with a targetfile or a nested <mapper> and sets the property when no source is newer than its target. With <srcfiles> and no mapper it uses a merge mapper onto targetfile. See Filters & Mappers for the mapper types.

<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool-flow" default="probe" basedir="." xmlns:if="ant:if" xmlns:unless="ant:unless">
  <property name="out" location="build"/>

  <target name="clean"><delete dir="${out}"/><mkdir dir="${out}"/></target>
<target name="probe" depends="clean">
  <available file="data/stations.csv" type="file" property="have.stations"/>
  <available file="data/tides.csv" property="have.tides"/>
  <available classname="io.tidepool.model.Height" property="have.model"/>
  <echo message="have.stations=${have.stations} have.tides=${have.tides} have.model=${have.model}"/>
</target>
<target name="load" depends="probe" if="have.stations">
  <echo message="loading data/stations.csv"/>
</target>
<target name="load-missing" depends="probe" unless="have.tides">
  <echo message="no tide data, skipping the tide import"/>
</target>
$ ant -q -f flow.xml load
     [echo] have.stations=true have.tides=${have.tides} have.model=${have.model}
     [echo] loading data/stations.csv
BUILD SUCCESSFUL
Total time: 0 seconds
$ ant -q -f flow.xml load-missing
     [echo] have.stations=true have.tides=${have.tides} have.model=${have.model}
     [echo] no tide data, skipping the tide import
BUILD SUCCESSFUL
Total time: 0 seconds

have.tides and have.model are unset, so they print literally. The io.tidepool.model.Height class is not on Ant’s own class path, which is why have.model is unset too: classname looks in Ant’s class path unless classpath is given. The load-missing target runs because its unless property is unset; its probe dependency ran first, as it always does.

A guarded target still runs its dependencies

A target whose if or unless is not satisfied is skipped, but its depends targets still run first. This is the question in the Ant FAQ about a target skipped by unless whose prerequisites run anyway:

<target name="guarded" depends="probe" unless="skip.import">
  <echo message="importing"/>
</target>
$ ant -q -f flow.xml -Dskip.import=true guarded
     [echo] have.stations=true have.tides=${have.tides} have.model=${have.model}
BUILD SUCCESSFUL
Total time: 0 seconds

Only importing was skipped; probe ran. Put the condition on the dependency that does the expensive work, or move the check into a small target that the others depend on.

uptodate

<target name="uptodate" depends="clean">
  <echo file="${out}/report.txt" message="report${line.separator}"/>
  <sleep milliseconds="1100"/>
  <touch file="data/stations.csv"/>
  <uptodate property="report.current" targetfile="${out}/report.txt">
    <srcfiles dir="data" includes="*.csv"/>
  </uptodate>
  <echo message="after touching the sources: report.current=${report.current}"/>
  <sleep milliseconds="1100"/>
  <touch file="${out}/report.txt"/>
  <uptodate property="report.current" targetfile="${out}/report.txt">
    <srcfiles dir="data" includes="*.csv"/>
  </uptodate>
  <echo message="after touching the report: report.current=${report.current}"/>
</target>
$ ant -q -f flow.xml uptodate
     [echo] after touching the sources: report.current=${report.current}
     [echo] after touching the report: report.current=true
BUILD SUCCESSFUL
Total time: 2 seconds

A property that is already set is never overwritten, so the second uptodate can only add report.current. Two details matter in practice. The sleep calls exist because file timestamps have a granularity of up to a second or two on some file systems, so "touch then compare immediately" is not reliable. And uptodate is the building block of Ant’s incremental build: javac and jar do the same comparison internally, as covered in Build Automation Fundamentals.

if and unless on tasks

Since Ant 1.9.1 the namespaces ant:if and ant:unless add conditions to any task or nested element, with the three tests true (the value is true), blank (the value is empty) and set (a property is set). The project element declares them with xmlns:if="ant:if" and xmlns:unless="ant:unless", as the header of this build file does.

<target name="task-level">
  <echo if:true="${strict}">strict build</echo>
  <echo unless:true="${strict}">relaxed build</echo>
  <echo if:set="release.version">releasing ${release.version}</echo>
  <echo unless:blank="${release.version}">version is not blank</echo>
</target>
$ ant -q -f flow.xml task-level
     [echo] relaxed build
     [echo] version is not blank
BUILD SUCCESSFUL
Total time: 0 seconds
$ ant -q -f flow.xml -Dstrict=true -Drelease.version=2.0 task-level
     [echo] strict build
     [echo] releasing 2.0
     [echo] version is not blank
BUILD SUCCESSFUL
Total time: 0 seconds

The first run shows a trap. With release.version unset, ${release.version} is not expanded to an empty string; it stays the literal text ${release.version}, which is not blank, so unless:blank does not skip the task. Test a property with if:set or unless:set, which look at the property by name, and use blank for values you know are defined.

fail

fail throws a BuildException, so it stops the build with a message.

Attribute or element Meaning

message

The text of the exception; the text of the element can be used instead. Without either, the message is No message.

if, unless

Fail only when the named property is set (if) or unset (unless). Without a message, the text names the attribute, for example unless=release.version.

status

The exit code of the JVM if the exception is not caught (Ant 1.6.2).

nested <condition>

Fail when the single nested condition holds (Ant 1.6.2). Without a message, the text is condition satisfied.

<target name="fail-unless"><fail unless="release.version"/></target>
<target name="fail-message"><fail message="data/tides.csv is missing"/></target>
<target name="fail-text"><fail>stations.csv has no header</fail></target>
<target name="fail-condition">
  <property name="min.java" value="21"/>
  <fail message="Java ${min.java} or newer is required">
    <condition><not><javaversion atleast="${min.java}"/></not></condition>
  </fail>
  <echo message="running on Java ${ant.java.version}, at least ${min.java}"/>
</target>
$ ant -q -f flow.xml fail-unless
BUILD FAILED
/work/tidepool/flow.xml:39: unless=release.version
Total time: 0 seconds
$ echo $?
1
<target name="fail-condition">
  <property name="min.java" value="21"/>
  <fail message="Java ${min.java} or newer is required">
    <condition><not><javaversion atleast="${min.java}"/></not></condition>
  </fail>
  <echo message="running on Java ${ant.java.version}, at least ${min.java}"/>
</target>
$ ant -q -f flow.xml fail-condition
     [echo] running on Java 26, at least 21
BUILD SUCCESSFUL
Total time: 0 seconds
$ ant -q -f flow.xml -Dmin.java=99 fail-condition
BUILD FAILED
/work/tidepool/flow.xml:44: Java 99 or newer is required
Total time: 0 seconds
$ echo $?
1
<target name="fail-status"><fail message="stations check failed" status="3"/></target>
<target name="fail-empty"><fail/></target>
<target name="fail-if"><property name="strict" value="true"/><fail if="strict" message="strict mode"/></target>

<!-- waitfor / parallel / sequential -->
<target name="waitfor" depends="clean">
  <parallel>
    <sequential>
      <sleep milliseconds="700"/>
      <echo file="${out}/ready.flag" message="ready"/>
      <echo message="producer: wrote ready.flag"/>
    </sequential>
    <sequential>
      <waitfor maxwait="5" maxwaitunit="second" checkevery="100" timeoutproperty="gave.up">
        <available file="${out}/ready.flag"/>
      </waitfor>
      <echo message="consumer: gave.up=${gave.up}"/>
    </sequential>
  </parallel>
</target>
$ ant -q -f flow.xml fail-status
BUILD FAILED
/work/tidepool/flow.xml:49: stations check failed
Total time: 0 seconds
$ echo $?
3

The exit status is what a CI server sees: any failed build exits with 1, and status lets different failures exit with different codes, here 3. A <fail/> with nothing reports No message, and the message can be given as an attribute or as the text of the element:

$ ant -q -f flow.xml fail-empty
BUILD FAILED
/work/tidepool/flow.xml:50: No message
Total time: 0 seconds
$ echo $?
1
$ ant -q -f flow.xml fail-message
BUILD FAILED
/work/tidepool/flow.xml:40: data/tides.csv is missing
Total time: 0 seconds
$ ant -q -f flow.xml fail-text
BUILD FAILED
/work/tidepool/flow.xml:41: stations.csv has no header
Total time: 0 seconds

waitfor

waitfor blocks until all nested conditions are true. It is built to synchronise concurrent branches of a parallel and to wait for a server.

Attribute Meaning Default

maxwait, maxwaitunit

How long to wait, in millisecond, second, minute, hour, day or week.

180000 milliseconds, that is 3 minutes

checkevery, checkeveryunit

The polling interval and its unit.

500 milliseconds

timeoutproperty

The property to set if maxwait is exceeded.

none

<target name="waitfor" depends="clean">
  <parallel>
    <sequential>
      <sleep milliseconds="700"/>
      <echo file="${out}/ready.flag" message="ready"/>
      <echo message="producer: wrote ready.flag"/>
    </sequential>
    <sequential>
      <waitfor maxwait="5" maxwaitunit="second" checkevery="100" timeoutproperty="gave.up">
        <available file="${out}/ready.flag"/>
      </waitfor>
      <echo message="consumer: gave.up=${gave.up}"/>
    </sequential>
  </parallel>
</target>
$ ant -q -f flow.xml waitfor
     [echo] producer: wrote ready.flag
     [echo] consumer: gave.up=${gave.up}
BUILD SUCCESSFUL
Total time: 0 seconds

gave.up is unset, so it prints literally: the file appeared in time. The next target lets the timeout expire:

<target name="waitfor-timeout" depends="clean">
  <waitfor maxwait="500" checkevery="100" timeoutproperty="gave.up">
    <available file="${out}/never.flag"/>
  </waitfor>
  <echo message="gave.up=${gave.up}"/>
  <waitfor maxwait="500" checkevery="100">
    <available file="${out}/never.flag"/>
  </waitfor>
</target>
$ ant -q -f flow.xml waitfor-timeout
     [echo] gave.up=true
BUILD SUCCESSFUL
Total time: 1 second

With timeoutproperty set, the build learns that the wait ended in a timeout. The second waitfor, which has no timeoutproperty, also timed out, and the build reached its end with BUILD SUCCESSFUL: in Ant 1.10.18 a waitfor that times out without a timeoutproperty does not fail the build, although the manual’s wording is that the task will "fail" if the conditions do not come true in maxwait. If a timeout must stop the build, set timeoutproperty and follow the task with <fail if="gave.up"/>.

parallel and sequential

parallel runs each nested task in its own thread and waits for all of them. sequential is a container that runs its children in order, so that one thread of a parallel can do several steps.

Attribute Meaning Default

threadCount

Maximum number of threads running at once. Tasks start in the order written.

all at once

threadsPerProcessor

Maximum threads per processor; overrides threadCount.

defers to threadCount

timeout

Milliseconds before the whole execution is terminated.

none

failonany

Complete as soon as any nested task fails, without waiting for the others.

false

pollInterval

Currently has no effect.

1000

A nested <daemons> element holds tasks that run in daemon threads: parallel does not wait for them, and they cannot keep Ant alive. The manual’s own caution applies: Ant’s core is believed to be thread safe, but individual tasks are not tested for it, and javac in particular is not re-entrant, so parallel is for running independent external programs, such as an application server beside a test run, and not a general way to speed up a build.

<target name="parallel" depends="clean">
  <parallel threadCount="2">
    <sequential><sleep milliseconds="300"/><echo message="task A done"/></sequential>
    <sequential><sleep milliseconds="300"/><echo message="task B done"/></sequential>
    <sequential><echo message="task C done"/></sequential>
  </parallel>
</target>
$ ant -q -f flow.xml parallel
     [echo] task B done
     [echo] task C done
     [echo] task A done
BUILD SUCCESSFUL
Total time: 0 seconds

With threadCount="2", tasks A and B are started together and C waits for a free thread. The order of the messages is not guaranteed by parallel; the output above is one real run.

<target name="parallel-fail" depends="clean">
  <parallel failonany="true">
    <fail message="station SF failed"/>
    <sequential><sleep seconds="3"/><echo message="slow task finished"/></sequential>
  </parallel>
</target>
$ ant -q -f flow.xml parallel-fail
BUILD FAILED
/work/tidepool/flow.xml:90: station SF failed
Total time: 0 seconds
$ echo $?
1
<target name="parallel-timeout" depends="clean">
  <parallel timeout="500">
    <sleep seconds="5"/>
  </parallel>
</target>
$ ant -q -f flow.xml parallel-timeout
BUILD FAILED
/work/tidepool/flow.xml:96: Parallel execution timed out
Total time: 0 seconds
$ echo $?
1
<target name="daemons" depends="clean">
  <parallel>
    <daemons>
      <sequential><sleep seconds="60"/><echo message="never printed"/></sequential>
    </daemons>
    <echo message="the main work finished"/>
  </parallel>
  <echo message="build continues without waiting for the daemon"/>
</target>
$ ant -q -f flow.xml daemons
     [echo] the main work finished
     [echo] build continues without waiting for the daemon
BUILD SUCCESSFUL
Total time: 0 seconds

failonany ended the build at once with the message of the failing branch rather than waiting three seconds for the slow branch. A timeout makes the whole parallel fail with Parallel execution timed out. The daemon thread, which would have slept for a minute, did not hold up the build.

retry

retry runs one nested task and re-runs it if it fails. retrycount is required; retrydelay is the pause between attempts in milliseconds (Ant 1.8.3). The task exists since Ant 1.7.1. It is meant for unreliable resources, such as a get from a flaky server.

<target name="retry" depends="clean">
  <parallel>
    <sequential>
      <sleep milliseconds="450"/>
      <copy file="data/stations.csv" tofile="${out}/stations.csv"/>
    </sequential>
    <retry retrycount="5" retrydelay="200">
      <copy file="${out}/stations.csv" tofile="${out}/copy.csv" failonerror="true"/>
    </retry>
  </parallel>
  <echo message="copy.csv exists: ${out}/copy.csv"/>
</target>
$ ant -q -f flow.xml retry
     [echo] copy.csv exists: /work/tidepool/build/copy.csv
BUILD SUCCESSFUL
Total time: 0 seconds

The copy fails while stations.csv does not exist, and retry tries again every 200 milliseconds until the other branch has created it. When every attempt fails, the build fails with the collected messages:

<target name="retry-exhausted" depends="clean">
  <retry retrycount="3" retrydelay="100">
    <get src="http://localhost:9/stations.csv" dest="${out}/stations.csv"/>
  </retry>
</target>
$ ant -q -f flow.xml retry-exhausted
BUILD FAILED
/work/tidepool/flow.xml:125: Task [get] failed after [3] attempts; giving up.
Error messages:
java.net.ConnectException: Connection refused
java.net.ConnectException: Connection refused
java.net.ConnectException: Connection refused
java.net.ConnectException: Connection refused
Total time: 0 seconds
$ echo $?
1

retrycount="3" produced four connection errors in the message: Ant runs the task once and then retries it up to retrycount more times, so count the number of retries, not the number of attempts, when you size it.

Why Ant has no loops

Ant’s build file is declarative XML with no loop construct. The <for> and <foreach> tasks that older build files use come from Ant-Contrib, a third-party task library that the Ant site lists on its projects page but that is dormant: its newest files on SourceForge date from 2008. Do not add it to a new build. The core alternatives are:

  • A macro (macrodef) with a parameter, called once per item. It is the right tool when the list is short and known.

  • subant (or ant) over a dirset, filelist or fileset, which runs a target once per directory or build file, with that directory as basedir.

  • A <script> loop in a language with a script engine. On JDK 15 and later this needs an engine added to the class path, as the scriptfilter example on Filters & Mappers shows.

  • A resource collection or a mapper that makes the task itself process many files, as copy and apply do: very often the loop is already inside a task.

<target name="loop-macro">
  <report-station id="SF"/>
  <report-station id="BOS"/>
  <report-station id="HNL"/>
</target>
$ ant -q -f flow.xml loop-macro
     [echo] station SF
     [echo] station BOS
     [echo] station HNL
BUILD SUCCESSFUL
Total time: 0 seconds
<target name="loop-subant">
  <subant target="report" genericantfile="${ant.file}" inheritall="false">
    <dirset dir="stations" includes="*"/>
  </subant>
</target>
$ ant -q -f flow.xml loop-subant
     [echo] reporting tidepool-flow in /work/tidepool/stations/BOS
     [echo] reporting tidepool-flow in /work/tidepool/stations/HNL
     [echo] reporting tidepool-flow in /work/tidepool/stations/SF
BUILD SUCCESSFUL
Total time: 0 seconds

The subant call ran the report target once in each station directory (inheritall="false" keeps the callers' properties out), and the order is that of the dirset. subant is covered in Multi-File Builds: import, include & subant.

Equivalents in Maven and Gradle

Need Ant Maven Gradle

Decide from the environment

condition with os, javaversion, available, isset

Profile activation by JDK, OS, property or file (see Profiles)

Plain Kotlin or Groovy if in the build script, onlyIf on a task (Tasks)

Fail on an unsuitable environment

fail with a nested condition

Enforcer rules such as requireJavaVersion (Enforcer)

throw GradleException(...) or a task that fails

Skip work whose output is current

uptodate, or the built-in checks of tasks

the compiler plugin’s stale-source detection

declared task inputs and outputs, UP-TO-DATE

Run steps concurrently

parallel

-T threads (Build Performance)

--parallel (Command-Line Interface)

Repeat a step

macrodef, subant

plugin executions, one per module in the reactor

a loop in the build script, or a task per item

Where the books are outdated

Java Development with Ant (Hatcher and Loughran, 2002) was written for Ant 1.5. It describes condition, available, uptodate, fail, waitfor and sleep, but cannot cover javaversion (1.10.2), antversion, matches, resourceexists or canCreateSymlink (1.10.16), the else attribute of condition (1.6.3), the ant:if and ant:unless namespaces (1.9.1) or retry (1.7.1). Examples that use <for>, <foreach> or <if> from Ant-Contrib should be rewritten with the core tools above.

Common mistakes

  • Reading an unset property as false: it is unset, and ${name} stays as literal text, including in istrue, equals and unless:blank. Use else="false" or isset.

  • Expecting xor to mean "exactly one".

  • Expecting waitfor to fail on timeout; it only sets timeoutproperty if you give one.

  • Assuming unless on a target also skips its dependencies; it does not.

  • Using parallel to run javac and javadoc together: the tasks are not designed for it.

  • Sizing retrycount as the number of attempts: it is the number of retries.

  • Checking a file with uptodate right after writing it on a file system with coarse timestamps.

  • Using a scriptcondition on a JDK with no JavaScript engine.

Where to go next