Ant Targets & Extension Points

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 and was run on JDK 26 on 2026-10-16. Every build file and every console output below comes from a real run of the Ant 1.10.18 distribution, in a scratch copy of the tidepool-model library (one record, Height, and one JUnit Jupiter test); the project directory is shortened to /work/tidepool-model in the output, and the closing Total time line is omitted. Features newer than Ant 1.9 name their release in the text.

A target is Ant’s unit of ordering. The build file lists tasks inside targets, and the targets name the other targets they need, so a build is a graph that Ant walks from the targets named on the command line. The Build File Structure page introduced <project>, <target> and tasks; this page covers the target element completely, the extension points added in Ant 1.8 that make shared build logic extensible, and the conditional features that sit on targets and on single tasks. The other half of the control flow, the <condition> task and friends, is on Conditions & Control Flow; properties, which most conditions test, are on Properties.

What a target is

A target is a named container of tasks and data types that cooperate to reach a desired state during the build, for example "the classes are compiled" or "the distributable exists". A target can depend on other targets, and Ant makes sure those have run before the target itself. That is the whole model: unlike Maven, there is no fixed lifecycle, and unlike Gradle, there is no task graph computed from inputs and outputs. The author draws the graph by hand with depends, and Ant sorts it. The ideas behind this are compared across the three tools on Build Automation Fundamentals.

The attributes of <target> in Ant 1.10.18 are:

Attribute Meaning Required

name

the name of the target

yes

depends

a comma-separated list of the targets this target depends on

no

if

the target runs only if this property is set (or the value evaluates to true)

no

unless

the target runs only if this property is not set (or the value evaluates to false)

no

description

a one-line description, printed by ant -p

no

extensionOf

adds this target to the depends list of the named extension point (since Ant 1.8.0)

no

onMissingExtensionPoint

what to do when extensionOf names an extension point that does not exist: fail, warn or ignore (since Ant 1.8.2)

no; allowed only together with extensionOf; defaults to fail

depends and the resolution order

Ant runs the targets in a depends list from left to right, but a target may already have run earlier because an earlier target depends on it. The manual’s example is the clearest way to see it, so here it is as a runnable file:

order.xml
<project name="order" default="D">
  <target name="A"><echo>A</echo></target>
  <target name="B" depends="A"><echo>B</echo></target>
  <target name="C" depends="B"><echo>C</echo></target>
  <target name="D" depends="C,B,A"><echo>D</echo></target>
</project>

The list of D reads C,B,A, and a quick reading suggests that C runs first, then B, then A. That is wrong, because C depends on B and B depends on A:

$ ant -f order.xml
Buildfile: /work/tidepool-model/order.xml

A:
     [echo] A

B:
     [echo] B

C:
     [echo] C

D:
     [echo] D

BUILD SUCCESSFUL

Within the dependency chain of one target, each target runs only once, however many targets depend on it. A, B and C are all dependencies of D, but each executed a single time, and Ant never repeated B and A when it came back to the D list. The rule is a topological sort of the graph, with depends order as the tie-breaker. The corollary is that the order of a depends list expresses nothing about dependencies between its members: if a target needs another one to have run first, it must name it in its own depends.

Targets named on the command line are a different matter. The "once" rule holds inside the chain of one named target, and Ant resolves each name on the command line separately, as the next run shows (Ant 1.10.18; the manual does not describe this case):

$ ant -f order.xml B D
Buildfile: /work/tidepool-model/order.xml

A:
     [echo] A

B:
     [echo] B

A:
     [echo] A

B:
     [echo] B

C:
     [echo] C

D:
     [echo] D

BUILD SUCCESSFUL

A and B ran twice, once for B and once for the chain of D. Name a single entry point on the command line, or build a single target that depends on the others, when a shared dependency should run once. The command-line options are covered on Running Ant.

Two classic failures come from the graph itself. Naming a target that does not exist, and closing a cycle, both stop the build before any task runs (the second file is cycle.xml, in which a depends on b and b depends on a):

$ ant -f order.xml nope
Buildfile: /work/tidepool-model/order.xml

BUILD FAILED
Target "nope" does not exist in the project "order".

$ ant -f cycle.xml
Buildfile: /work/tidepool-model/cycle.xml

BUILD FAILED
Circular dependency: a <- b <- a

A target name can be any string that is valid in the encoding of the XML file, including the empty string, a comma or a space, but the manual asks you to avoid those because they are confusing on the command line and in IDEs and will not be supported in future versions. Targets that begin with a hyphen, such as -prepare, are valid and are the idiom for internal targets: Ant’s main class treats every argument that begins with a hyphen as one of its own options, so they cannot be called from the command line (ant -prepare prints Unknown argument: -prepare followed by the usage text), although IDEs usually can call them.

The tidepool target graph

The running build for tidepool-model has two files. common.xml is the shared build logic, imported by each module (the mechanism is on Multi-File Builds: import, include & subant). It defines init, an extension point named ready-to-compile, and the chain compile, test, jar and dist. build.xml imports it and contributes one target of its own, generate-version, which writes a generated Version class. The diagram shows the resulting graph; the extension point is drawn as a stadium.

flowchart LR init["init"] --> rtc(["ready-to-compile: extension point"]) gv["generate-version: extensionOf"] --> rtc init --> gv rtc --> compile["compile"] --> test["test"] --> jar["jar"] --> dist["dist"]

The two files, shortened to the targets that matter here (the full common.xml also holds the property definitions, the test class path and clean):

common.xml
<project name="common" xmlns:if="ant:if">
  <target name="init">
    <tstamp/>
    <echo>init: building ${ant.project.name} on ${DSTAMP}</echo>
    <mkdir dir="${classes.dir}"/>
    <mkdir dir="${gen.dir}"/>
  </target>

  <extension-point name="ready-to-compile" depends="init"
                   description="state reached when all sources exist"/>

  <target name="compile" depends="ready-to-compile" description="compile main sources">
    <echo>compile</echo>
    <javac destdir="${classes.dir}" release="21" includeantruntime="false">
      <src path="${src.dir}"/>
      <src path="${gen.dir}"/>
    </javac>
  </target>

  <target name="test" depends="compile" description="run the unit tests">
    <!-- javac for the tests, then the JUnit console launcher via <java> -->
  </target>

  <target name="jar" depends="test" description="package the library">
    <echo>jar</echo>
    <jar destfile="${jar.file}" basedir="${classes.dir}"/>
  </target>

  <target name="dist" depends="jar" description="publish to build/repo (file repository)">
    <echo>dist</echo>
    <copy file="${jar.file}" todir="${build.dir}/repo"/>
  </target>
</project>
build.xml
<project name="tidepool-model" default="dist" basedir=".">
  <import file="common.xml"/>

  <target name="generate-version" extensionOf="ready-to-compile" depends="init"
          description="write a generated Version class">
    <echo>generate-version</echo>
    <mkdir dir="${gen.dir}/io/tidepool/model"/>
    <echo file="${gen.dir}/io/tidepool/model/Version.java">package io.tidepool.model;
public final class Version { public static final String VALUE = "1.0.0"; }
</echo>
  </target>
</project>

Running ant clean dist names two targets, and the build file’s default is dist:

$ ant clean dist
Buildfile: /work/tidepool-model/build.xml

clean:
   [delete] Deleting directory /work/tidepool-model/build

init:
     [echo] init: building tidepool-model on 20261016
    [mkdir] Created dir: /work/tidepool-model/build/classes
    [mkdir] Created dir: /work/tidepool-model/build/generated

generate-version:
     [echo] generate-version
    [mkdir] Created dir: /work/tidepool-model/build/generated/io/tidepool/model

ready-to-compile:

compile:
     [echo] compile
    [javac] Compiling 2 source files to /work/tidepool-model/build/classes

test:
    [mkdir] Created dir: /work/tidepool-model/build/test-classes
    [javac] Compiling 1 source file to /work/tidepool-model/build/test-classes
     [java]
     [java] Test run finished after 24 ms
     [java] [         4 containers found      ]
     [java] [         0 containers skipped    ]
     [java] [         4 containers started    ]
     [java] [         0 containers aborted    ]
     [java] [         4 containers successful ]
     [java] [         0 containers failed     ]
     [java] [         1 tests found           ]
     [java] [         0 tests skipped         ]
     [java] [         1 tests started         ]
     [java] [         0 tests aborted         ]
     [java] [         1 tests successful      ]
     [java] [         0 tests failed          ]
     [java]

jar:
     [echo] jar
      [jar] Building jar: /work/tidepool-model/build/tidepool-model-1.0.0.jar

dist:
     [echo] dist
     [copy] Copying 1 file to /work/tidepool-model/build/repo

BUILD SUCCESSFUL

generate-version ran between init and ready-to-compile, so compile found the generated source in build/generated ("Compiling 2 source files": Height and Version). The extension point itself did no work, which is why its section of the output is empty.

if and unless on targets

The if and unless attributes of a target enable or disable that single target depending on a property. They let a build adapt to the Java version, the operating system or a -D option. In the simplest form Ant only checks whether the property is set, and its value does not matter:

cond.xml
<project name="cond" default="all">
  <target name="all" depends="build-module-A, build-own-fake-module-A"/>
  <target name="build-module-A" if="module-A-present">
    <echo>using the real module A</echo>
  </target>
  <target name="build-own-fake-module-A" unless="module-A-present">
    <echo>using a fake module A</echo>
  </target>
</project>

Without the property the first target is skipped. With the property set to false, it still runs, because the property is set:

$ ant -f cond.xml
Buildfile: /work/tidepool-model/cond.xml

build-module-A:

build-own-fake-module-A:
     [echo] using a fake module A

all:

BUILD SUCCESSFUL

$ ant -f cond.xml -Dmodule-A-present=false
Buildfile: /work/tidepool-model/cond.xml

build-module-A:
     [echo] using the real module A

build-own-fake-module-A:

all:

BUILD SUCCESSFUL

The rules, in the order they bite:

  • A skipped target still prints its name followed by an empty body, as above. It is skipped, not removed.

  • Only one property name may appear in an if or unless attribute. To combine several tests, compute the result in a helper target with <condition> and test the property it sets.

  • If a target has both depends and if/unless, the depends list runs first; the condition is evaluated only when the target is about to run and all its predecessors have finished. The attributes switch off the target they are attached to and never the targets it depends on.

  • If neither attribute is present the target always runs.

  • The attribute value may be a property expansion that evaluates to true or false (the manual says "or something evaluating to true"); a value that is not a boolean is treated as a property name.

The expansion case is worth a run (true and yes enable the target, false does not), because an unset property leaves the literal text ${deploy.enabled} in place:

expand.xml
<project name="expand" default="deploy">
  <target name="deploy" if="${deploy.enabled}">
    <echo>deploying</echo>
  </target>
</project>

Running it four times (the lines are the target part of each run) gives:

$ ant -f expand.xml
deploy:

$ ant -f expand.xml -Ddeploy.enabled=true
deploy:
     [echo] deploying

$ ant -f expand.xml -Ddeploy.enabled=false
deploy:

$ ant -f expand.xml -Ddeploy.enabled=yes
deploy:
     [echo] deploying

The manual’s idiom for several conditions uses a check target that sets the flag. Here the flag needs a jar and the station data, and the example runs twice, first without data/stations.csv and then with it:

check.xml
<project name="check" default="publish">
  <target name="publish.check">
    <condition property="publish.run">
      <and>
        <available file="build/tidepool-model-1.0.0.jar"/>
        <available file="data/stations.csv"/>
      </and>
    </condition>
  </target>
  <target name="publish" depends="publish.check" if="publish.run">
    <echo>jar and station data are present, publishing</echo>
  </target>
</project>
$ ant -f check.xml
Buildfile: /work/tidepool-model/check.xml

publish.check:

publish:

BUILD SUCCESSFUL

$ mkdir data && echo 'id,name' > data/stations.csv
$ ant -f check.xml
Buildfile: /work/tidepool-model/check.xml

publish.check:

publish:
     [echo] jar and station data are present, publishing

BUILD SUCCESSFUL

description, ant -p and the default target

The description attribute is more than a comment. ant -p (long form -projecthelp) lists the targets that have one, so the descriptions are the public interface of the build file; targets without a description are considered internal. -v or -d together with -p list the internal targets too, under "Other targets". The next file has a hyphenated internal target, a helper without a description and a public target:

internal.xml
<project name="internal" default="release">
  <target name="-prepare"><echo>prepare</echo></target>
  <target name="check-tools"><echo>check</echo></target>
  <target name="release" depends="-prepare, check-tools" description="make a release"><echo>release</echo></target>
</project>
$ ant -f internal.xml -p
Buildfile: /work/tidepool-model/internal.xml

Main targets:

 release  make a release
Default target: release

$ ant -f internal.xml -p -v
Apache Ant(TM) version 1.10.18 compiled on September 3 2026
Buildfile: /work/tidepool-model/internal.xml
Detected Java version: 26 in: /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home
Detected OS: Mac OS X
parsing buildfile /work/tidepool-model/internal.xml with URI = file:/work/tidepool-model/internal.xml
Project base dir set to: /work/tidepool-model

Main targets:

 release  make a release
Other targets:

 -prepare
 check-tools
Default target: release

Run on the tidepool-model build.xml, ant -p lists the targets of the imported common.xml as well, including the extension point and its description:

$ ant -p
Buildfile: /work/tidepool-model/build.xml

Main targets:

 clean             delete build output
 compile           compile main sources
 dist              publish to build/repo (file repository)
 generate-version  write a generated Version class
 jar               package the library
 ready-to-compile  state reached when all sources exist
 test              run the unit tests
Default target: dist

The default attribute of <project> names the target that runs when none is given on the command line. It is optional. A build file without a default target and without a command-line target runs only the tasks that stand directly under <project> (allowed since Ant 1.6.0 and handy for a <tstamp/>), and then reports success:

nodefault.xml
<project name="nodefault">
  <tstamp/>
  <echo>top-level task runs first, DSTAMP=${DSTAMP}</echo>
  <target name="hello"><echo>hello</echo></target>
</project>
$ ant -f nodefault.xml
Buildfile: /work/tidepool-model/nodefault.xml
     [echo] top-level task runs first, DSTAMP=20261016

BUILD SUCCESSFUL

$ ant -f nodefault.xml hello
Buildfile: /work/tidepool-model/nodefault.xml
     [echo] top-level task runs first, DSTAMP=20261016

hello:
     [echo] hello

BUILD SUCCESSFUL

The manual recommends putting <tstamp> in an initialization target, conventionally named init, on which every other target depends and which comes first in their depends lists, as common.xml above does. Top-level tasks run before any target, each time and for every invocation, which is why they should be limited to cheap set-up.

Extension points

An extension point is the answer to a design problem that existed before Ant 1.8: how can an imported build file leave a slot where the importing file adds its own steps, without editing the shared file or overriding whole targets? Since Ant 1.8.0 the <extension-point> element is a target with a name and a depends list but with no tasks. Its job is to stand for a state the build must reach, and to collect the targets that contribute to that state.

Contributors join with the extensionOf attribute on their own target. The manual’s example puts a slot in front of compile:

<target name="create-directory-layout">
   ...
</target>
<extension-point name="ready-to-compile"
                 depends="create-directory-layout"/>
<target name="compile" depends="ready-to-compile">
   ...
</target>

and a target in the main build file fills it:

<target name="generate-sources"
        extensionOf="ready-to-compile">
   ...
</target>

Ant appends generate-sources to the extension point’s depends list, so it runs after the targets in the explicit depends attribute of the extension point and before compile. Three details from the manual and the runs above:

  • If several targets add themselves to the same extension point, their relative order is not defined. Do not rely on the order of the contributions. If generate-version needs init first, it says so in its own depends, as in the example, because extensionOf does not imply any ordering with respect to other targets.

  • An extension point is an ordinary entry point. It can be named on the command line to build only up to that state, and it appears in ant -p when it has a description. In the tidepool build, ant ready-to-compile runs init, generate-version and the empty extension point, and stops:

    $ ant ready-to-compile
    Buildfile: /work/tidepool-model/build.xml
    
    init:
         [echo] init: building tidepool-model on 20261016
    
    generate-version:
         [echo] generate-version
    
    ready-to-compile:
    
    BUILD SUCCESSFUL
  • The point of the construct is imported build files. A shared file declares the states (ready-to-compile, and in a larger build also ready-to-package or ready-to-publish) and each module’s own file adds targets that contribute.

The extension point is not limited to imported files. Within one file it behaves the same:

extonly.xml
<project name="ext" default="ready">
  <target name="one"><echo>one</echo></target>
  <target name="two"><echo>two</echo></target>
  <target name="three" depends="one"><echo>three</echo></target>
  <extension-point name="ready" depends="one"/>
  <target name="late" extensionOf="ready"><echo>late</echo></target>
  <target name="late2" extensionOf="ready"><echo>late2</echo></target>
</project>
$ ant -f extonly.xml
Buildfile: /work/tidepool-model/extonly.xml

one:
     [echo] one

late:
     [echo] late

late2:
     [echo] late2

ready:

BUILD SUCCESSFUL

late and late2 ran after one (the explicit depends), the unrelated targets two and three did not run, and the extension point came last. The contribution order here happens to match the declaration order, but the manual guarantees nothing about it.

onMissingExtensionPoint

A contributing target fails the build when the extension point it names does not exist. That is the safe default, since a typo would otherwise silently drop the target from the build. Since Ant 1.8.2, onMissingExtensionPoint changes the reaction to warn or ignore, which is how an optional contribution is written: a target that adds itself to an extension point that exists in some of the files it may be combined with but not in all of them. The three values, with the same target and an extension point that does not exist:

<project name="miss" default="extra">
  <target name="extra" extensionOf="no-such-point" onMissingExtensionPoint="fail">
    <echo>extra ran</echo>
  </target>
</project>
$ ant -f miss.xml        # onMissingExtensionPoint="fail", also the behavior when the attribute is absent
Buildfile: /work/tidepool-model/miss.xml

BUILD FAILED
can't add target extra to extension-point no-such-point because the extension-point is unknown.

$ ant -f miss.xml        # onMissingExtensionPoint="warn"
Buildfile: /work/tidepool-model/miss.xml
Warning: can't add target extra to extension-point no-such-point because the extension-point is unknown.

extra:
     [echo] extra ran

BUILD SUCCESSFUL

$ ant -f miss.xml        # onMissingExtensionPoint="ignore"
Buildfile: /work/tidepool-model/miss.xml

extra:
     [echo] extra ran

BUILD SUCCESSFUL

The attribute is allowed only together with extensionOf; the target still runs when it is requested directly, as the extra runs above show, and in the warn and ignore cases it simply is not attached to anything.

bindtargets

Sometimes the contributing target lives in a third file, or the shared file cannot be edited, and a bridge target would be needed only to connect the two. The <bindtargets> task (since Ant 1.8.2) makes that bridge for you: it adds a list of targets to the depends list of an extension point, but only in the project where it is written, so other projects that import the same shared files are unaffected. The manual gives the equivalent:

<bindtargets targets="jar,javadoc" extensionPoint="dist"/>

is the same as

<target name="bind-to-dist" depends="jar,javadoc" extensionOf="dist"/>

The attributes are targets (a comma-separated list, required), extensionPoint (required) and onMissingExtensionPoint (fail by default, with the same warn and ignore as above). It may be used only as a top-level task, which keeps the dependency graph static and predictable as soon as all the files are loaded. A build file that binds two documentation targets to the extension point of the shared common.xml:

bind.xml
<project name="bind" default="dist">
  <import file="common.xml"/>
  <target name="docs"><echo>docs: write the site</echo></target>
  <target name="sources-jar" depends="init"><echo>sources-jar: package sources</echo></target>
  <bindtargets targets="docs,sources-jar" extensionPoint="ready-to-compile"/>
</project>
$ ant -f bind.xml compile
Buildfile: /work/tidepool-model/bind.xml

init:
     [echo] init: building bind on 20261016
    [mkdir] Created dir: /work/tidepool-model/build/classes
    [mkdir] Created dir: /work/tidepool-model/build/generated

docs:
     [echo] docs: write the site

sources-jar:
     [echo] sources-jar: package sources

ready-to-compile:

compile:
     [echo] compile
    [javac] Compiling 1 source file to /work/tidepool-model/build/classes

BUILD SUCCESSFUL

Putting the task inside a target fails at the moment that target runs, not while the file is parsed:

<project name="badbind" default="x">
  <target name="x"><echo>x</echo></target>
  <target name="t">
    <bindtargets targets="x" extensionPoint="y"/>
  </target>
</project>
$ ant -f badbind.xml t
Buildfile: /work/tidepool-model/badbind.xml

t:

BUILD FAILED
/work/tidepool-model/badbind.xml:4: bindtargets only allowed as a top-level task

if and unless on any task

The target-level if and unless act on a whole target. Since Ant 1.9.1 any task or nested element can carry if and unless as well, through two special namespaces that are declared on the project:

xmlns:if="ant:if"
xmlns:unless="ant:unless"

The namespaces support three conditions, each usable in both directions: true (the value of the attribute evaluates to true), blank (the value is null or empty) and set (the named property is set). So if:set, unless:set, if:true, unless:true, if:blank and unless:blank exist as attributes. A file that uses all three kinds:

ns.xml
<project name="ns" default="show" xmlns:if="ant:if" xmlns:unless="ant:unless">
  <target name="show">
    <condition property="onmac"><os family="mac"/></condition>
    <echo if:set="onmac">running on macOS</echo>
    <echo unless:set="onmac">not running on macOS</echo>
    <echo if:true="${verbose}">verbose is true</echo>
    <echo unless:true="${verbose}">verbose is not true</echo>
    <echo unless:blank="${who}">hello ${who}</echo>
    <echo if:blank="${who}">nobody to greet</echo>
  </target>
</project>
$ ant -f ns.xml
Buildfile: /work/tidepool-model/ns.xml

show:
     [echo] running on macOS
     [echo] verbose is not true
     [echo] hello ${who}

BUILD SUCCESSFUL

$ ant -f ns.xml -Dverbose=true -Dwho=tide
Buildfile: /work/tidepool-model/ns.xml

show:
     [echo] running on macOS
     [echo] verbose is true
     [echo] hello tide

BUILD SUCCESSFUL

The first run holds the classic trap of this feature: blank tests the value of the attribute, and an unset property expands to its own text, ${who}, which is not blank. unless:blank="${who}" therefore ran and echoed hello ${who} instead of skipping. Use if:set and unless:set with a property name to ask "was it defined", and use blank only for values that are guaranteed to expand, for example a property given a default with <property name="who" value=""/>. A build that must run on Ant releases older than 1.9.1 cannot use the namespaces. For checks that stop the build, see Conditions & Control Flow.

Targets are not functions

The most frequent beginner mistake is to treat a target as a subroutine: to call it with arguments, to call it more than once and to expect it to return. A target is a node in a graph, which is why the one-execution rule exists. Ant offers <antcall>, which runs another target of the same file in a new project, and it is the trap, because it runs the called target together with all of its dependencies again:

call.xml
<project name="call" default="main">
  <target name="init"><echo>init</echo></target>
  <target name="greet" depends="init"><echo>greet ${who}</echo></target>
  <target name="main" depends="init">
    <antcall target="greet"><param name="who" value="tide"/></antcall>
    <antcall target="greet"><param name="who" value="moon"/></antcall>
  </target>
</project>
$ ant -f call.xml
Buildfile: /work/tidepool-model/call.xml

init:
     [echo] init

main:

init:
     [echo] init

greet:
     [echo] greet tide

init:
     [echo] init

greet:
     [echo] greet moon

BUILD SUCCESSFUL

init ran three times: once for main and once for every <antcall>, since each call creates a new project and re-evaluates the dependency chain of the called target. The properties and references set by the called target do not persist back to the caller either, the <param> values override only inside the call, and a property defined on the command line cannot be overridden by a <param>. The manual’s <antcall> page lists the attributes target, inheritAll (default true) and inheritRefs (default false) and the nested <param>, <reference> and <propertyset> elements.

The guidance that follows from this:

  • Use depends for ordering and for steps that run once; use extension-point and extensionOf for extensibility.

  • Use <macrodef> (Macros & Presets) for a reusable step with parameters and no dependency graph: it is the real counterpart of a function.

  • Use <ant>, <subant> and <import> when the unit of reuse is a build file (Multi-File Builds).

  • Keep <antcall> for the few cases where a target must run again with different properties, and expect its dependencies to run again.

The same ideas in Maven and Gradle

Ant’s explicit target graph has a counterpart in each of the other tools, but it is expressed differently:

Idea Ant Maven Gradle

a step

<target> with tasks

a plugin goal, bound to a lifecycle phase

a task

ordering

depends on the target, hand-written

the fixed order of the lifecycle phases

dependsOn, plus mustRunAfter and shouldRunAfter for ordering without dependency; task inputs and outputs add implicit dependencies

run it once

within one dependency chain

each phase runs once per module

each task runs at most once per build

conditional step

if and unless on a target; if:/unless: on a task

profiles, or the skip configuration property of a plugin

onlyIf predicates, enabled = false, StopExecutionException

a slot others can fill

<extension-point> and extensionOf

a phase: any plugin can bind a goal to it

a lifecycle task such as assemble, made to depend on other tasks

public targets

description and ant -p

lifecycle phases and plugin goals

./gradlew tasks, listing the tasks that have a group

The Maven lifecycle is on Maven: Build Lifecycle and its profiles on Maven: Profiles. The Gradle task graph is on Gradle: Build Lifecycle and the task API on Gradle: Tasks. The comparison of all the concepts is in Comparing Ant, Maven & Gradle.

Pitfalls and good practice

  • Do not assume the order inside a depends list means more than "left to right unless something already ran". Spell out real dependencies in each target.

  • A skipped if or unless target still runs its depends. Put expensive work in the target, not in its dependencies.

  • if="x" tests whether x is set, not whether it is true. -Dx=false still turns it on. Use if="${x}" (a boolean expansion) when the value matters, and remember that an unset property leaves the text ${x} in place.

  • Name every public target and give it a description; leave helpers undocumented, or start their names with a hyphen, so that ant -p shows the real interface.

  • Keep init first in the depends list and never put work that changes the build state into top-level tasks.

  • Prefer an <extension-point> in shared files over overriding a target with the same name in an importing file: an extension point has an explicit contract and a safe failure mode when the name is wrong.

  • bindtargets is top-level only; antcall is only for the rare re-run, and re-runs depends.

  • A target name with a comma or a space is legal but will be rejected by some IDEs and will not be supported in the future.