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.

flowchart TD master["build.xml (master): jar, clean"] -->|"subant, in file-list order"| model["tidepool-model/build.xml"] master -->|subant| engine["tidepool-engine/build.xml"] master -->|subant| cli["tidepool-cli/build.xml"] model -->|import| common["common.xml: init, ready-to-compile, compile, jar, clean"] engine -->|import| common cli -->|import| common cli -.->|"extensionOf: check-data, override: jar"| common

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.

The attributes

Attribute Meaning Default

file

The file to import. A relative name is resolved against the importing file, not against basedir, which is unlike most Ant file attributes. Instead of file a nested resource or resource collection may be given (since Ant 1.8.0).

required, or a nested resource

optional

If true, a missing file does not stop the build.

false

as

The prefix prepended to target names for the second, prefixed name of each target.

the name of the imported <project>

prefixSeparator

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.

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 a that itself includes a file as b exposes that file’s targets as a.b.name. An import contributes to the nested prefix only when it has an explicit as.

  • as is required when the included file’s <project> has no name.

  • The same attributes file, optional, as and prefixSeparator and the same nested resources as import apply, and include must also be a top-level task.

  • ant.file.<project> and ant.file.type.<project> are defined exactly as for import.

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 as is given, the prefixed name

only the prefixed name

depends of the imported targets

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 common.xml whose steps modules adjust

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.

The attributes

Attribute Meaning Default

antfile

The build file, relative to dir.

build.xml

dir

The directory used as the child’s basedir (and for resolving antfile and output).

the current project’s basedir, unless inheritAll is false, in which case there is no default

target

The target of the child to run. Several targets can be given as nested <target name="..."/> elements (since Ant 1.6.3), run, in the manual’s words, "as if Ant had been invoked with a single target whose dependencies are the targets so specified": a dependency they share runs once, which is not what ant b d does (that runs it again for d).

the child’s default target

output

A file for the child’s output, relative to dir.

none

inheritAll

Pass every property of the parent to the child. The inherited properties override those the child sets, because properties are immutable.

true

inheritRefs

Copy every reference (path, fileset, …​) to the child. References already defined in the child are not overridden.

false

useNativeBasedir

Use the basedir the child would have when run from the command line (since Ant 1.8.0).

false

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.

  • subant defaults to inheritall="false" (the opposite of ant), so parent.prop is 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.child set 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 (with echoproperties or propertyfile) which the parent loads with loadproperties.

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 the property task, and is kept for compatibility.

  • The properties passed to the child override the ones the child sets itself (the manual says so for both ant and antcall), so a parent value wins over a property task in the child.

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

true or false

the value of dir

omitted

true

the basedir of the calling project

omitted

false

the basedir attribute of the child’s <project>

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.

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.

Attribute or element Meaning Default

target

The target to run in each sub-build. Nested <target name="..."> elements give several (Ant 1.7 and later).

each build’s default target

antfile

The build file name inside each directory.

build.xml

genericantfile

One build file run with different base directories. When set, antfile is ignored.

none

buildpath, buildpathref, nested <buildpath>, <buildpathelement>

The build path.

none

nested <fileset>, <dirset>, <filelist>, any filesystem resource collection

Add to the implicit build path.

none

inheritall, inheritrefs, output

As for ant.

false, false, none

failonerror

Fail the whole build when a sub-build fails; with false, report and go on.

true

verbose

Log when each sub-build is entered and left.

false

nested <property>, <propertyset>

As for ant: always passed on.

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), use antfile (or the default). subant does not set the base directory in this case: each build file sets its own.

  • To run one build file with different base directories, use genericantfile with 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".

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).

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.

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

target

The target to run, with its depends.

required (or nested <target> elements)

inheritAll

Pass the caller’s properties.

true

inheritRefs

Pass the caller’s references.

false

nested <param>, <reference>, <propertyset>

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:

  • init ran three times: antcall re-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’s env=dev was not passed, so the <param> env=ignored took effect. With the default inheritAll="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 -D property 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.

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:

common.xml
<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:

tidepool-model/build.xml
<project name="tidepool-model" default="jar" basedir=".">
  <import file="../common.xml"/>
  <path id="module.classpath"/>
</project>
tidepool-engine/build.xml
<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:

tidepool-cli/build.xml
<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 clean and jar targets ran their sub-builds in the order of the file list.

  • check-data ran for the CLI module only, between init and ready-to-compile: the extension point collected it.

  • common.jar ran before the CLI’s own jar, 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 jar works because ant.file.common and the relative import do 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.

Pitfalls

  • depends and import. An imported target’s depends use plain names. A module that defines a target with the same name as one in common.xml silently changes what every shared target that depends on it does. This is the feature, and also the most common surprise; give private helpers in common.xml distinctive 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 set level before the import and mode after it, with common.xml defining both, printed level=from module (before import) mode=common-default: the module’s later value was ignored. Put per-module overrides before the import.

  • include cannot be overridden. If a module needs to adjust a target, use import.

  • Relative paths in imported files resolve against the importing project’s basedir; use ${ant.file.<name>} with dirname for files next to the shared build file.

  • subant default inheritall=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 uses inheritall="true".

  • antcall re-runs depends. A step called three times runs init three 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. include and extension-point need Ant 1.8.0 or later; nested <target> in ant needs 1.6.3, in subant 1.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

<import> of a common.xml

a parent POM (<parent>), with pluginManagement and dependencyManagement

convention plugins in build-logic, or subprojects {} / allprojects {}

overriding a shared step

redefine the target; the original is name.target

redefine the same plugin configuration in the child, with combine.children or combine.self attributes for merging

a task or extension configured after the convention plugin has applied

a slot that others fill

<extension-point> and extensionOf

any phase of the lifecycle

a lifecycle task such as assemble and dependsOn

building several modules

<subant> over a file list; order written by hand

<modules> in an aggregator POM; the reactor orders by dependencies

include(":model") in settings.gradle.kts; project dependencies give the order

passing values to a sub-build

inheritAll, <property>, <propertyset>, <reference>

properties and the parent POM; -D reaches all modules

gradle.properties, extra properties, project accessors

building another project in the same command

<ant dir="...">

the reactor, or a separate build started by the Invoker or Exec plugin

a composite build (includeBuild)

independent re-run of a target

<antcall>

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.

Where to go next