Ant Multi-File Builds: import, include & subant
|
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 build file and every console output below comes from a real run of
that release on JDK 26 on 2026-10-16, in a scratch copy of the tidepool project (the three modules tidepool-model,
tidepool-engine and tidepool-cli, plus the commons-csv and picocli JARs in a shared lib directory). Paths in the
output were shortened to /work/mf for the whole project and /work/demo for the small demonstration files, and the
closing Total time line is omitted. Features newer than Ant 1.6 name their release in the text.
A real project is never one build.xml. Ant offers two different kinds of reuse for build files, and they answer
different questions:
-
Sharing build logic: several build files want the same targets and properties. This is what
<import>and<include>do: they read another build file into the same project, as if its targets had been written in place. -
Calling other builds: a master build wants to run the build of another project, in another directory and with its own properties. This is what
<ant>,<subant>and<antcall>do: they start a new project and run targets in it.
The tidepool build uses both. Each module has a tiny build.xml that imports one shared common.xml, and a master
build.xml at the root calls the three module builds, in order, with <subant>. That layout is the one the manual
itself recommends for subant, and it gives very small module files, one maintainable shared file and a root build that
can run the whole thing.
The layout
tidepool/ build.xml master build common.xml shared build logic lib/ commons-csv-1.14.1.jar, picocli-4.7.7.jar data/stations.csv tidepool-model/ build.xml + src/main/java tidepool-engine/ build.xml + src/main/java tidepool-cli/ build.xml + src/main/java
The next sections explain each mechanism on small files, then the last two sections assemble the tidepool build and show the same ideas in Maven and Gradle.
import
<import> reads another build file into the current project. The manual describes it as working "like the Entity
Includes" described in the FAQ (see the XML entity trick on
Build File Structure), as if the imported file
were contained in the importing file, minus its <project> tag. Two features make it more than a text include: target
overriding and special properties.
Source: Ant manual: Import task
The attributes
| Attribute | Meaning | Default |
|---|---|---|
|
The file to import. A relative name is resolved against the importing file, not against |
required, or a nested resource |
|
If |
|
|
The prefix prepended to target names for the second, prefixed name of each target. |
the |
|
The separator between the prefix and the target name. |
|
A nested resource lets a build file be imported from a JAR, which is how a company ships shared build logic as an artifact:
<import>
<javaresource name="common/targets.xml">
<classpath location="common.jar"/>
</javaresource>
</import>
(The manual’s own example; it was not run here.) The task must be a top-level task. Calling it inside a target fails:
Buildfile: /work/demo/bad-inc.xml
go:
BUILD FAILED
/work/demo/bad-inc.xml:3: import only allowed as a top-level task
<include> prints the same message, with the word import in it.
Source: Ant manual: Import task
Target overriding
If a target of the importing file has the same name as a target of an imported file, the importing file’s target wins.
The original is not lost: it is still available under the prefixed name, made of the imported project’s name (or the
as attribute) and the target name. Because the depends lists of imported targets are not rewritten, a target in the
imported file that depends on the overridden one runs the overriding version. That is the point of import: shared files
define the skeleton and the importing file replaces the steps it needs to.
<project name="shared">
<dirname property="shared.dir" file="${ant.file.shared}"/>
<target name="setup">
<property name="who" value="shared"/>
<echo>shared.setup: who=${who}</echo>
</target>
<target name="report" depends="setup">
<echo>shared.report: who=${who}, file dir=${shared.dir}</echo>
</target>
</project>
<project name="app" default="report">
<import file="shared.xml"/>
<target name="setup">
<property name="who" value="app"/>
<echo>app.setup (overrides) then calls the original</echo>
<antcall target="shared.setup"/>
</target>
</project>
Running ant in the directory of the second file:
Buildfile: /work/demo/imp.xml
setup:
[echo] app.setup (overrides) then calls the original
shared.setup:
[echo] shared.setup: who=app
report:
[echo] shared.report: who=app, file dir=/work/demo
BUILD SUCCESSFUL
report comes from the shared file, but its depends="setup" ran the overriding setup. The overriding target then
called the original with <antcall target="shared.setup"/>. The who property stayed app because properties are
immutable: the overriding target set it first (see
Properties).
Calling the original through antcall works, but antcall starts a new project and runs the called target with its own
dependencies again (see the section on antcall below). The cleaner way is to depend on the prefixed target. The
tidepool-cli module overrides jar and keeps the original behaviour with depends="common.jar"; the example is in the
last section.
An imported file can also be given a different prefix with as, which is needed when the file has no name or when two
imported files use the same name. The prefix applies only to the second name; the plain name is still there.
Special properties: where is the imported file?
An imported file is parsed as if it were part of the main file, so the basedir of its <project> is ignored and a
relative path inside it resolves against the importing project’s basedir. To find resources that live next to the
shared file, Ant defines a property for every imported project that has a name:
<project name="app" default="go">
<import file="shared.xml"/>
<target name="go">
<echo>ant.file = ${ant.file}</echo>
<echo>ant.file.app = ${ant.file.app}</echo>
<echo>ant.file.shared = ${ant.file.shared}</echo>
<echo>ant.file.type.shared = ${ant.file.type.shared}</echo>
<echo>ant.project.name = ${ant.project.name}</echo>
</target>
</project>
go:
[echo] ant.file = /work/demo/props2.xml
[echo] ant.file.app = /work/demo/props2.xml
[echo] ant.file.shared = /work/demo/shared.xml
[echo] ant.file.type.shared = file
[echo] ant.project.name = app
BUILD SUCCESSFUL
ant.file.<project> holds the path of the file that defines the project called <project>; note that <project> is the
name attribute, not the file name. ant.file.type.<project> is file or url (Ant 1.8.0 and later), because an
import may now come from a URL or a classpath resource, in which case the path trick below does not work. A file with
no name gets no property at all. ant.project.name, in contrast, is that of the main project, which is exactly what
the shared common.xml needs to name the JAR after the module that imported it.
The idiom for "the directory of the shared file" is the first line of shared.xml above:
<dirname property="shared.dir" file="${ant.file.shared}"/>
It also keeps the file usable on its own, because ant.file.shared is defined when the file is the main build file too.
The tidepool common.xml uses it to locate the shared lib directory (ant.file.common).
A missing file, a repeated file
By default a missing file fails the build, and the message names both files:
Buildfile: /work/demo/missing.xml
BUILD FAILED
/work/demo/missing.xml:2: Cannot find /work/demo/nothere.xml imported from /work/demo/missing.xml
Total time: 0 seconds
With optional="true" the build runs normally, and -v shows the only trace of the miss: the line
Cannot find /work/demo/nothere.xml imported from /work/demo/missing.xml. Optional imports are how a build offers a hook
for per-developer or per-site targets (local.xml) without requiring the file to exist.
The same file cannot be imported twice. The second <import> of shared.xml is silently ignored and -v prints
Skipped already imported file: followed by the name. include is the tool for using one file more than once.
include
<include> (since Ant 1.8.0) also reads a build file into the current project, but it is the opposite design: the included
file is self-contained. Every target of the included file is renamed to prefix.name, and every depends list of the
included file is rewritten with the same prefix. The plain name is not available, so nothing can be overridden by
accident, and nothing in the including project can change what the included targets depend on.
<project name="app" default="go">
<include file="shared.xml"/>
<include file="shared.xml" as="again" prefixSeparator=":"/>
<target name="setup">
<echo>app.setup: the included targets are not affected</echo>
</target>
<target name="go" depends="shared.report, again:report"/>
</project>
Buildfile: /work/demo/inc.xml
shared.setup:
[echo] shared.setup: who=shared
shared.report:
[echo] shared.report: who=shared, file dir=/work/demo
again:setup:
[echo] shared.setup: who=shared
again:report:
[echo] shared.report: who=shared, file dir=/work/demo
go:
BUILD SUCCESSFUL
The including file’s own setup never ran: shared.report depended on shared.setup. The file was included twice under
two prefixes (the second with the separator :), and each copy ran its own chain. Asking for the plain name fails:
BUILD FAILED
Target "report" does not exist in the project "app".
Other rules of include:
-
Prefixes nest: a file included as
athat itself includes a file asbexposes that file’s targets asa.b.name. Animportcontributes to the nested prefix only when it has an explicitas. -
asis required when the included file’s<project>has noname. -
The same attributes
file,optional,asandprefixSeparatorand the same nested resources asimportapply, andincludemust also be a top-level task. -
ant.file.<project>andant.file.type.<project>are defined exactly as forimport.
Source: Ant manual: Include task
import or include?
The manual’s rule of thumb: "use import if you intend to override a target, otherwise use include". The manual’s
nested.xml example (a setUp target and an echo target that depends on it) shows the difference. The main file
defines its own setUp, brings nested.xml in with as="nested", and its test target depends on nested.echo:
<project name="app" default="test">
<target name="setUp">
<property name="prop" value="in importing"/>
</target>
<import file="nested.xml" as="nested"/> <!-- or: <include file="nested.xml" as="nested"/> -->
<target name="test" depends="nested.echo"/>
</project>
With import:
setUp:
nested.echo:
[echo] prop has the value in importing
test:
BUILD SUCCESSFUL
With include:
nested.setUp:
nested.echo:
[echo] prop has the value in nested
test:
BUILD SUCCESSFUL
The summary:
<import> |
<include> |
|
|---|---|---|
names of the targets |
the plain name and, if the project has a name or |
only the prefixed name |
|
unchanged, so they can be redirected by overriding |
rewritten to the prefixed names, so the file is self-contained |
overriding a target |
yes: the importing file’s target wins; the original is the prefixed one |
no |
the same file more than once |
no, a second import is skipped |
yes, with different prefixes |
available since |
Ant 1.6 |
Ant 1.8.0 |
typical use |
a shared |
a library of independent utility targets, or one file used by several sub-areas |
The tidepool modules use import, because they need to override jar and add to the ready-to-compile extension point.
Extension points and import
The reason <extension-point> exists is exactly the shared-file pattern: the shared file declares an empty state
(ready-to-compile) that other targets join with extensionOf, so the importing file can add steps without overriding
anything. The syntax, onMissingExtensionPoint and bindtargets are on
Targets & Extension Points. The Ant FAQ
records one historical bug in this combination: in Ant 1.8.0 an extensionOf in the importing file failed with "no
extension point named" when the extension point was declared in the imported file; it was fixed in Ant 1.8.1, so any
current release is unaffected. The tidepool build below relies on exactly that combination.
The ant task
<ant> runs Ant on another build file in a new project and waits for it. Every child project has its own properties and
references, which is why the task has so many switches for what to pass down.
Source: Ant manual: Ant task
The attributes
| Attribute | Meaning | Default |
|---|---|---|
|
The build file, relative to |
|
|
The directory used as the child’s |
the current project’s |
|
The target of the child to run. Several targets can be given as nested |
the child’s default target |
|
A file for the child’s output, relative to |
none |
|
Pass every property of the parent to the child. The inherited properties override those the child sets, because properties are immutable. |
|
|
Copy every reference (path, fileset, …) to the child. References already defined in the child are not overridden. |
|
|
Use the |
|
Nested elements: <property> (always passed to the child and to everything the child starts, with the priority of a
command-line -D), <propertyset> (since Ant 1.6), <reference refid="..." torefid="..."> (copy one reference, optionally
under another id) and <target>.
What the child sees
The next file prints what a child build sees. The child, child/build.xml:
<project name="child" default="show">
<target name="show">
<echo>child basedir = ${basedir}</echo>
<echo>parent.prop = ${parent.prop}</echo>
<echo>passed.prop = ${passed.prop}</echo>
<echo>path ref = ${toString:shared.path}</echo>
<property name="from.child" value="set by child"/>
</target>
<target name="fail"><fail message="child failed on purpose"/></target>
</project>
and the parent, which calls it five ways:
<project name="parent" default="main">
<property name="parent.prop" value="parent value"/>
<path id="shared.path"><pathelement location="x.jar"/></path>
<target name="main">
<echo>--- ant (inheritAll defaults to true)</echo>
<ant dir="child"/>
<echo>--- ant inheritAll=false, nested property</echo>
<ant dir="child" inheritAll="false">
<property name="passed.prop" value="passed explicitly"/>
</ant>
<echo>--- subant (inheritall defaults to false)</echo>
<subant>
<property name="passed.prop" value="via subant"/>
<fileset dir="." includes="child/build.xml"/>
</subant>
<echo>--- ant inheritRefs=true</echo>
<ant dir="child" inheritRefs="true"/>
<echo>--- ant reference with torefid</echo>
<ant dir="child" inheritAll="false">
<reference refid="shared.path" torefid="shared.path"/>
</ant>
<echo>from.child in parent = ${from.child}</echo>
</target>
</project>
main:
[echo] --- ant (inheritAll defaults to true)
show:
[echo] child basedir = /work/demo/child
[echo] parent.prop = parent value
[echo] passed.prop = ${passed.prop}
[echo] path ref = ${toString:shared.path}
[echo] --- ant inheritAll=false, nested property
show:
[echo] child basedir = /work/demo/child
[echo] parent.prop = ${parent.prop}
[echo] passed.prop = passed explicitly
[echo] path ref = ${toString:shared.path}
[echo] --- subant (inheritall defaults to false)
show:
[echo] child basedir = /work/demo/child
[echo] parent.prop = ${parent.prop}
[echo] passed.prop = via subant
[echo] path ref = ${toString:shared.path}
[echo] --- ant inheritRefs=true
show:
[echo] child basedir = /work/demo/child
[echo] parent.prop = parent value
[echo] passed.prop = ${passed.prop}
[echo] path ref = /work/demo/x.jar
[echo] --- ant reference with torefid
show:
[echo] child basedir = /work/demo/child
[echo] parent.prop = ${parent.prop}
[echo] passed.prop = ${passed.prop}
[echo] path ref = /work/demo/x.jar
[echo] from.child in parent = ${from.child}
BUILD SUCCESSFUL
What the output shows:
-
By default a child sees all of the parent’s properties (
parent.prop) but none of the parent’s references. -
With
inheritAll="false"only the nested<property>values are passed. -
subantdefaults toinheritall="false"(the opposite ofant), soparent.propis not visible, but the nested<property>is. -
inheritRefs="true"copies every reference, and<reference>copies exactly one. Without either, the path is the unexpanded${toString:shared.path}text, which is how a missing reference shows up. -
The property
from.childset in the child is not visible in the parent. Child builds cannot return values; if the parent needs a result, the child can write a properties file (withechopropertiesorpropertyfile) which the parent loads withloadproperties.
Three rules about properties follow from the manual:
-
A property given on the command line (
-Dname=value) cannot be overridden by a nested<property>, even several<ant>calls deep (since Ant 1.8.0). -
When several nested
<property>elements set the same name in one<ant>or<subant>, the last one wins. This is the opposite of thepropertytask, and is kept for compatibility. -
The properties passed to the child override the ones the child sets itself (the manual says so for both
antandantcall), so a parent value wins over apropertytask in the child.
Source: Ant manual: Ant task
The basedir of the child
The basedir rules are the part of <ant> the manual itself calls "confusing". Summarised for a task in the top-level
build (not nested inside another <ant>):
dir |
inheritAll |
basedir of the child |
|---|---|---|
given |
|
the value of |
omitted |
|
the |
omitted |
|
the |
When an <ant> is itself called from an <ant> the parent’s dir and inheritAll come into play, and the manual gives a
second table. Two ways out of the confusion: always pass dir, or set useNativeBasedir="true" (Ant 1.8.0), which makes the
child use "the basedir it would have if run from the command line" whatever the nesting. A -Dbasedir on the command line
beats everything.
The ant task cannot call its own file at the top level
An <ant> outside a target cannot invoke the file it is part of:
BUILD FAILED
/work/demo/selfant.xml:2: ant task at the top level must not invoke its own build file.
Inside a target the same call is legal (it is how antcall is implemented in effect) and runs the target in a new
project.
Source: Ant manual: Ant task, reference
The subant task
<subant> (since Ant 1.6) calls one target (or several, with nested <target> elements) in every sub-build of a build
path. It is built on <ant>, so most of the ant page applies, with two different defaults: inheritall is false and
the build path is a nested resource collection instead of a single file.
Source: Ant manual: Subant task
| Attribute or element | Meaning | Default |
|---|---|---|
|
The target to run in each sub-build. Nested |
each build’s default target |
|
The build file name inside each directory. |
|
|
One build file run with different base directories. When set, |
none |
|
The build path. |
none |
nested |
Add to the implicit build path. |
none |
|
As for |
|
|
Fail the whole build when a sub-build fails; with |
|
|
Log when each sub-build is entered and left. |
|
nested |
As for |
none |
antfile or genericantfile?
The manual’s section "Use with directories" names the two modes:
-
To run different build files (
directory1/build.xml,directory2/build.xml), useantfile(or the default).subantdoes not set the base directory in this case: each build file sets its own. -
To run one build file with different base directories, use
genericantfilewith a<dirset>.
The second form, run against two directories:
<subant target="info" genericantfile="${basedir}/generic.xml">
<dirset dir="." includes="child,child2"/>
</subant>
info:
[echo] generic.xml running in /work/demo/child
info:
[echo] generic.xml running in /work/demo/child2
generic.xml has a single info target that echoes ${basedir}. This is the shape for "run the same clean build over
every module that has no build file of its own".
Source: Ant manual: Subant task
Order matters: filelist, not fileset
For a <fileset> or <dirset> the manual warns that the entries are added to the build path "in no particular order".
For a <filelist> the order is the one written. tidepool-engine needs the JAR of tidepool-model before it compiles,
so the master build uses a file list. A file list may also name files that do not exist yet, which a fileset cannot.
<project name="tidepool" default="jar" basedir=".">
<filelist id="modules" dir="."
files="tidepool-model/build.xml,tidepool-engine/build.xml,tidepool-cli/build.xml"/>
<target name="jar" description="build every module, in dependency order">
<subant target="jar">
<filelist refid="modules"/>
</subant>
</target>
<target name="clean" description="clean every module">
<subant target="clean">
<filelist refid="modules"/>
</subant>
</target>
</project>
The module order is written once, by hand. Ant has no equivalent of the Maven reactor or Gradle project dependencies that
works out an order from dependency declarations; Ivy’s buildlist task does (see
Ivy Publishing & Multi-Project Builds).
Source: Ant manual: Subant task
failonerror and verbose
By default the first failing sub-build stops everything and the error shows both build files and their line numbers. With
verbose="true" the entry is logged. These runs use child/build.xml and a copy child2/build.xml, whose fail target
calls <fail>:
<project name="parent" default="strict">
<target name="strict">
<subant target="fail" verbose="true">
<fileset dir="." includes="child/build.xml,child2/build.xml"/>
</subant>
</target>
<target name="lenient">
<subant target="fail" failonerror="false">
<fileset dir="." includes="child/build.xml,child2/build.xml"/>
</subant>
<echo>still running</echo>
</target>
</project>
$ ant strict
Buildfile: /work/demo/fail.xml
strict:
[subant] Executing: /work/demo/child/build.xml
fail:
BUILD FAILED
/work/demo/fail.xml:3: The following error occurred while executing this line:
/work/demo/child/build.xml:9: child failed on purpose
$ ant lenient
Buildfile: /work/demo/fail.xml
lenient:
fail:
[subant] Failure for target 'fail' of: /work/demo/child/build.xml
[subant] The following error occurred while executing this line:
[subant] /work/demo/child/build.xml:9: child failed on purpose
fail:
[subant] Failure for target 'fail' of: /work/demo/child2/build.xml
[subant] The following error occurred while executing this line:
[subant] /work/demo/child2/build.xml:9: child failed on purpose
[echo] still running
BUILD SUCCESSFUL
failonerror="false" turns each failure into a logged message and keeps the build green, which hides real breakage in a
CI job; it is for "best effort" tasks such as cleaning. The nested <target> form runs several targets per sub-build, here
show and then fail:
<subant>
<target name="show"/>
<target name="fail"/>
<fileset dir="." includes="child/build.xml"/>
</subant>
and the run prints the show output followed by fail: and the failure above. The targets run in that order as the
dependencies of one implied target, so a target that both depend on would run once per sub-build, not once per nested
<target>.
The manual also shows how to avoid repeating the subant call in every master target: a small macro (see
Macros & Presets) that takes the target name and
iterates over modules/*/build.xml. It adds that, because build files are XML, a master file could be generated from
common.xml with XSLT and names a stylesheet etc/common2master.xsl; that file is not in the etc directory of the
1.10.18 distribution, so treat the hint as historical.
Source: Ant manual: Subant task
antcall
<antcall> runs a target of the same build file in a new project, with its dependencies, and optional parameters.
| Attribute or element | Meaning | Default |
|---|---|---|
|
The target to run, with its |
required (or nested |
|
Pass the caller’s properties. |
|
|
Pass the caller’s references. |
|
nested |
Values and references to pass. |
none |
<project name="acall" default="main">
<property name="env" value="dev"/>
<target name="init"><echo>init</echo></target>
<target name="deploy" depends="init">
<echo>deploy to ${where} (env=${env})</echo>
<property name="result" value="deployed"/>
</target>
<target name="main" depends="init">
<antcall target="deploy" inheritAll="false">
<param name="where" value="staging"/>
<param name="env" value="ignored"/>
</antcall>
<echo>result in caller: ${result}</echo>
<antcall target="deploy"><param name="where" value="prod"/></antcall>
</target>
</project>
init:
[echo] init
main:
init:
[echo] init
deploy:
[echo] deploy to staging (env=ignored)
[echo] result in caller: ${result}
init:
[echo] init
deploy:
[echo] deploy to prod (env=dev)
BUILD SUCCESSFUL
Four facts show in this output, and each one is a classic source of confusion:
-
initran three times:antcallre-evaluates the dependency chain of the called target in a new project. (The page on targets has the same observation for the case where the target is "called like a function".) -
With
inheritAll="false"the caller’senv=devwas not passed, so the<param>env=ignoredtook effect. With the defaultinheritAll="true"an inherited property is fixed, and a<param>of the same name would have been ignored (the manual says inherited and passed properties "will be fixed and not overridable" in the called targets). -
${result}, set in the called target, is not visible in the caller: results do not flow back. -
A
-Dproperty on the command line cannot be overridden by a<param>.
Because every call is a new project, antcall is also slower and makes the log harder to read. Use it only when a target
really has to run again with different properties. For "a reusable step with arguments" use a macro
(Macros & Presets shows the same example with and
without antcall), and for "another build file" use ant, subant or import.
Source: Ant manual: AntCall task
Putting it together: the tidepool build
The shared file common.xml is the build logic of all three modules. It locates itself with ant.file.common, names the
JAR after the importing module with ant.project.name, declares the ready-to-compile extension point, and expects each
module to define a path with the id module.classpath:
<project name="common" xmlns:if="ant:if">
<dirname property="common.dir" file="${ant.file.common}"/>
<property name="lib.dir" location="${common.dir}/lib"/>
<property name="src.dir" location="src/main/java"/>
<property name="build.dir" location="build"/>
<property name="classes.dir" location="${build.dir}/classes"/>
<property name="jar.file" location="${build.dir}/${ant.project.name}.jar"/>
<target name="init">
<echo>init: ${ant.project.name} (basedir ${basedir})</echo>
<mkdir dir="${classes.dir}"/>
</target>
<extension-point name="ready-to-compile" depends="init"/>
<target name="compile" depends="ready-to-compile">
<javac srcdir="${src.dir}" destdir="${classes.dir}" release="21" includeantruntime="false"
classpathref="module.classpath"/>
</target>
<target name="jar" depends="compile">
<jar destfile="${jar.file}" basedir="${classes.dir}"/>
</target>
<target name="clean">
<delete dir="${build.dir}"/>
</target>
</project>
Two details deserve attention. Relative locations such as src/main/java resolve against the module’s basedir, because
the imported file is part of the importing project; only the lib directory needs ant.file.common. And module.classpath
is not defined in common.xml: references are looked up when compile runs, so a shared file can use a reference the
importing file defines later in its own text. The model module is as small as a module can be:
<project name="tidepool-model" default="jar" basedir=".">
<import file="../common.xml"/>
<path id="module.classpath"/>
</project>
<project name="tidepool-engine" default="jar" basedir=".">
<import file="../common.xml"/>
<path id="module.classpath">
<pathelement location="../tidepool-model/build/tidepool-model.jar"/>
<fileset dir="${lib.dir}" includes="commons-csv-*.jar"/>
</path>
</project>
The CLI module uses all three import features: it joins the extension point with extensionOf, it overrides jar, and it
reaches the original jar through the prefixed name common.jar, without antcall:
<project name="tidepool-cli" default="jar" basedir=".">
<import file="../common.xml"/>
<path id="module.classpath">
<pathelement location="../tidepool-model/build/tidepool-model.jar"/>
<pathelement location="../tidepool-engine/build/tidepool-engine.jar"/>
<fileset dir="${lib.dir}" includes="*.jar"/>
</path>
<!-- joins the extension point declared in common.xml -->
<target name="check-data" extensionOf="ready-to-compile" depends="init">
<fail message="data/stations.csv is missing">
<condition>
<not><available file="${common.dir}/data/stations.csv"/></not>
</condition>
</fail>
</target>
<!-- overrides common.xml's jar and reuses the original through its prefixed name -->
<target name="jar" depends="common.jar">
<echo>cli.jar: adding the runtime libraries next to the jar</echo>
<copy todir="${build.dir}/lib">
<fileset dir="${lib.dir}" includes="*.jar"/>
</copy>
</target>
</project>
Running ant clean jar in the project root builds all three modules in order. The first clean: and the first jar:
are the master’s own, empty targets; every later block comes from a sub-build:
Buildfile: /work/mf/build.xml
clean:
clean:
[delete] Deleting directory /work/mf/tidepool-model/build
clean:
[delete] Deleting directory /work/mf/tidepool-engine/build
clean:
[delete] Deleting directory /work/mf/tidepool-cli/build
jar:
init:
[echo] init: tidepool-model (basedir /work/mf/tidepool-model)
[mkdir] Created dir: /work/mf/tidepool-model/build/classes
ready-to-compile:
compile:
[javac] Compiling 4 source files to /work/mf/tidepool-model/build/classes
jar:
[jar] Building jar: /work/mf/tidepool-model/build/tidepool-model.jar
init:
[echo] init: tidepool-engine (basedir /work/mf/tidepool-engine)
[mkdir] Created dir: /work/mf/tidepool-engine/build/classes
ready-to-compile:
compile:
[javac] Compiling 2 source files to /work/mf/tidepool-engine/build/classes
jar:
[jar] Building jar: /work/mf/tidepool-engine/build/tidepool-engine.jar
init:
[echo] init: tidepool-cli (basedir /work/mf/tidepool-cli)
[mkdir] Created dir: /work/mf/tidepool-cli/build/classes
check-data:
ready-to-compile:
compile:
[javac] Compiling 1 source file to /work/mf/tidepool-cli/build/classes
common.jar:
[jar] Building jar: /work/mf/tidepool-cli/build/tidepool-cli.jar
jar:
[echo] cli.jar: adding the runtime libraries next to the jar
[copy] Copying 2 files to /work/mf/tidepool-cli/build/lib
BUILD SUCCESSFUL
Notes on the output:
-
The master’s
cleanandjartargets ran their sub-builds in the order of the file list. -
check-dataran for the CLI module only, betweeninitandready-to-compile: the extension point collected it. -
common.jarran before the CLI’s ownjar, because the overriding target depends on it. The log names the project of the prefix (common), not the module. -
Each module is also a stand-alone build:
ant -f tidepool-cli/build.xml jarworks becauseant.file.commonand the relativeimportdo not depend on the caller.
If data/stations.csv is missing, the CLI fails before any compilation and the error names the nested file, both when run
alone and when run from the master (which adds its own line):
$ ant jar
jar:
init:
[echo] init: tidepool-cli (basedir /work/mf/tidepool-cli)
check-data:
BUILD FAILED
/work/mf/build.xml:6: The following error occurred while executing this line:
/work/mf/tidepool-cli/build.xml:11: data/stations.csv is missing
(The earlier modules were already built in that run, so the output shows only the CLI part, the tail of the log.)
ant -p on the master lists just its two documented targets (clean, jar); on the CLI module it also lists the
imported ones, including common.jar and the ready-to-compile point.
Source: Ant manual: Import task
Pitfalls
-
dependsandimport. An imported target’sdependsuse plain names. A module that defines a target with the same name as one incommon.xmlsilently changes what every shared target that depends on it does. This is the feature, and also the most common surprise; give private helpers incommon.xmldistinctive names. -
Properties are first-wins across files too. The top-level tasks of an imported file run at the position of the
import. In a test, a module that setlevelbefore theimportandmodeafter it, withcommon.xmldefining both, printedlevel=from module (before import) mode=common-default: the module’s later value was ignored. Put per-module overrides before theimport. -
includecannot be overridden. If a module needs to adjust a target, useimport. -
Relative paths in imported files resolve against the importing project’s
basedir; use${ant.file.<name>}withdirnamefor files next to the shared build file. -
subantdefaultinheritall=false. A property defined in the master is not seen by the modules unless it is passed with a nested<property>or<propertyset>, or the whole call usesinheritall="true". -
antcallre-runsdepends. A step called three times runsinitthree times; a macro does not. -
A fileset as the build path gives an unspecified order; use
<filelist>whenever modules depend on each other. -
failonerror="false"can leave a red module hidden inside a green build. -
Old Ant.
includeandextension-pointneed Ant 1.8.0 or later; nested<target>inantneeds 1.6.3, insubant1.7;<import>and<subant>need 1.6.
The same ideas in Maven and Gradle
Ant’s two kinds of reuse are two separate mechanisms in Ant; in the other tools they are part of the project model.
| Need | Ant | Maven | Gradle |
|---|---|---|---|
shared build logic |
|
a parent POM ( |
convention plugins in |
overriding a shared step |
redefine the target; the original is |
redefine the same plugin configuration in the child, with |
a task or extension configured after the convention plugin has applied |
a slot that others fill |
|
any phase of the lifecycle |
a lifecycle task such as |
building several modules |
|
|
|
passing values to a sub-build |
|
properties and the parent POM; |
|
building another project in the same command |
|
the reactor, or a separate build started by the Invoker or Exec plugin |
a composite build ( |
independent re-run of a target |
|
rerun the phase |
not offered: tasks run once per build |
The Maven reactor and aggregation are on Maven: Multi-Module Builds & the Reactor and parent POMs on Maven: Inheritance, Aggregation & the Effective POM. Gradle’s equivalents are on Gradle: Multi-Project Builds, Gradle: Convention Plugins & Sharing Build Logic and Gradle: Composite Builds. Gradle can also import an Ant build file and expose its targets as tasks: see Gradle: Ant Integration. The full concept comparison is on Comparing Ant, Maven & Gradle.
Source: Maven: Project aggregation
Source: Gradle: Multi-project builds
Source: Gradle: Composite builds
Where to go next
-
Macros & Presets for reusable steps with parameters, the replacement for most
antcalluses. -
Antlibs & Namespaces for shipping build logic as a JAR with a namespace URI.
-
Ivy Publishing & Multi-Project Builds for module ordering from dependency data.
-
Targets & Extension Points for the extension point rules in depth.
-
Properties for how values flow between files.