Build File Structure

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Ant 1.10.18. Every example was run on JDK 26 on 2026-10-16 with the Ant 1.10.18 binary distribution, and the console output under each example is what that run printed, with the long scratch-directory prefix shortened to /work. The examples build the tidepool-model library of the running scenario (four Java records and no dependencies), plus a few minimal standalone build files where a single concept is easier to see without the scenario around it.

An Ant build file is an XML document with a fixed, small vocabulary: one project, some target elements, and inside the targets the tasks that do the work. This page takes that vocabulary apart, one element at a time, then puts it back together in a first build.xml for tidepool-model and in the official Hello World tutorial. The deeper pages cover each part in turn: Targets & Extension Points, Properties and Paths, Filesets & Patterns. The command line used below is described on Running Ant.

The shape of a build file

Ant reads a build file with an ordinary XML parser, so a build file has to be well-formed XML before it can be anything else. Its default name is build.xml, and ant looks for that file in the current directory unless -f (or -buildfile) names another one. The manual summarises the structure in one sentence: each build file contains one project and at least one (default) target, and targets contain task elements.

flowchart TD P["project: name, default, basedir"] --> D["description (optional)"] P --> TL["top-level tasks, types and properties run before any target"] P --> T1["target: name, depends, description"] P --> T2["target: name, depends, description"] T1 --> K1["task: attributes and nested elements"] T1 --> K2["task: attributes and nested elements"] T2 --> K3["task: attributes and nested elements"] K1 --> N["nested elements: fileset, classpath, arg, manifest"]

Three levels, then, and nothing deeper in the grammar itself: the project, its targets, and the tasks inside a target. What looks like nesting below that level (a classpath inside javac, a manifest inside jar) is not new structure but the configuration of one task.

The same shape in the other two tools, for orientation:

Ant Maven Gradle

project with name, default, basedir

project element of the POM (coordinates groupId, artifactId, version); the directory of pom.xml is the base directory

a project per directory, named in settings.gradle(.kts) with rootProject.name

target and its depends list

lifecycle phase, or a plugin goal bound to one

task and its dependsOn

task element and its attributes

plugin goal and its configuration

task configured in the build script

property (${name})

POM property (${name})

project property or extra property

The Maven side is on POM Fundamentals and the Gradle side on Settings & Build Scripts.

The project element

project is the root element, and every attribute is optional.

Attribute Meaning

name

The name of the project. Ant exposes it as the built-in property ant.project.name, which is why the tutorial below names its JAR file ${ant.project.name}.jar. It also prefixes the names of targets overridden through import, and it is how a build file is told apart from another one in log output.

default

The target that runs when none is named on the command line. It is optional, but without it a plain ant runs only the project’s initialisation (see below) and then reports success; the verified output is further down.

basedir

The directory from which every relative path in the file is resolved. A relative basedir is itself resolved against the directory that contains the build file. If it is missing, the base directory is that directory, unless the basedir property was set on the command line (-Dbasedir=...).

Two details trip people up. The first is that the default target must exist, otherwise Ant stops before running any target. The second is that basedir defaults to the directory of the build file, not to the directory you typed ant in, so ant -f sub/build.xml resolves src against sub/. A file that lives in a subdirectory but wants to work with its parent sets basedir="..".

The following build file puts both to the test. It lives in sub/build.xml, declares basedir="..", and has one top-level echo outside every target:

sub/build.xml
<project name="toplevel" default="where" basedir="..">
  <echo>Top-level echo runs before any target</echo>
  <property name="greeting" value="hello"/>
  <target name="where">
    <echo>basedir is ${basedir}</echo>
    <echo>build file is ${ant.file}</echo>
    <echo>project is ${ant.project.name}, default target is ${ant.project.default-target}</echo>
  </target>
  <target name="other"/>
</project>
Buildfile: /work/toplevel/sub/build.xml
     [echo] Top-level echo runs before any target

where:
     [echo] basedir is /work/toplevel
     [echo] build file is /work/toplevel/sub/build.xml
     [echo] project is toplevel, default target is where

BUILD SUCCESSFUL
Total time: 0 seconds

The base directory is the parent of sub/, and the build file is still found where -f pointed. The echo outside the targets ran first, before the where target. Since Ant 1.6 any task or type may be declared outside a target; those declarations run when the file is parsed, in order, before the first target executes. This is how the property lines near the top of nearly every build file work. A few tasks, antcall for example, are refused there because they could loop forever. Because the top-level section runs even if you only ask for -p, the echo appears there too:

     [echo] Top-level echo runs before any target
Main targets:

Other targets:

 other
 where

Without a default attribute nothing at all runs, and Ant does not complain; naming a target explicitly still works:

nodefault.xml
<project name="nodefault">
  <target name="a"><echo>a</echo></target>
</project>
Buildfile: /work/toplevel/nodefault.xml

BUILD SUCCESSFUL
Total time: 0 seconds
Buildfile: /work/toplevel/nodefault.xml

a:
     [echo] a

BUILD SUCCESSFUL
Total time: 0 seconds

A default that names a missing target is a hard error, and the message names both the target and the project:

BUILD FAILED
Target "build" does not exist in the project "bad".

Total time: 0 seconds

The description element

An optional top-level <description> carries a sentence about the project. ant -p prints it above the target list (see the first example below), and tools such as IDEs show it too. It is a child of the project, not an attribute, so it may contain a few lines of text. The same word is also an attribute on targets and on every task, with different jobs, covered below.

Targets

A target is a named set of tasks. It has a name, an optional depends list of other targets that must have run first, and an optional description. Only the name is required. depends states order, and the manual is careful to say that it only specifies the order in which targets execute: it does not make Ant run a prerequisite that "did not need to run". Each target runs at most once per build, however many others depend on it. if and unless conditions, the extension-point element and the rules for resolving a dependency chain belong to Targets & Extension Points.

The one thing this page needs from targets is the public/internal split. A target with a description is a public ("main") target and is listed by ant -p; one without is internal and is listed, under "Other targets", only when you add -v. That is why the tidepool-model file below describes compile, jar and clean but not init: the init target is plumbing that nobody should run by hand.

Two more mistakes are caught when the file is parsed rather than run. Two targets with the same name in one file are rejected, and a target name that does not exist is reported with the project it was looked up in:

BUILD FAILED
/work/errors/bad-order.xml:3: Duplicate target 'compile'

Total time: 0 seconds

Tasks

A task is a piece of Java code that can be executed. Its XML form is the task name followed by attributes and, for tasks that accept them, nested elements:

<taskname attribute1="value1" attribute2="value2"/>

Ant ships well over a hundred tasks, and you can write your own; the task categories each have a page under the Apache Ant index. Attribute values are strings, and Ant converts them to the type the task expects (a boolean, a file, an integer, an enumeration). Before a task runs, every ${property} reference in its attributes and its nested text is expanded. Three real error messages show what the parser and the task check:

An unknown task name:

BUILD FAILED
/work/errors/bad-task.xml:3: Problem: failed to create task or type javaq
Cause: The name is undefined.
Action: Check the spelling.
Action: Check that any custom tasks/types have been declared.
Action: Check that any <presetdef>/<macrodef> declarations have taken place.


Total time: 0 seconds

An attribute that the task does not have:

BUILD FAILED
/work/errors/bad-attr.xml:3: mkdir doesn't support the "dirr" attribute

Total time: 0 seconds

An attribute that is spelt right but names a directory that does not exist (javac does not create its destdir):

BUILD FAILED
/work/errors/bad-dir.xml:4: destination directory "/work/errors/build" does not exist or is not a directory

Total time: 0 seconds

The first message is the one you see when a third-party task has not been declared (see Antlibs & Namespaces) or when the JAR that contains an optional task is missing from Ant’s library path. Build-file errors of this kind surface when the target runs, not when the file is parsed. This build file has a misspelt task in the rare target, and the default target runs without complaint:

     [echo] ok target ran

BUILD SUCCESSFUL
Total time: 0 seconds

A typo in a rarely used target therefore stays hidden until someone needs that target.

Attributes and nested elements

Simple settings go in attributes. Structured settings go in nested elements, and the same few nested element types come back in task after task:

  • path-like structures: classpath, sourcepath, path and their pathelement children, covered on Paths, Filesets & Patterns;

  • resource collections such as fileset and zipfileset;

  • command-line arg elements, described below;

  • task-specific elements, for example manifest and attribute inside jar, or formatter and batchtest inside junit.

Free text between the start and end tag is, for tasks that accept it, a text attribute: <echo>Hello</echo> is the same as <echo message="Hello"/>, and the text can be spread over several lines.

Common attributes of all tasks

Every task accepts three attributes in addition to its own.

Attribute Meaning

id

A unique identifier for this task instance. A script or a Java task can find the instance later through it (project.getReference("task1")). The manual warns that until the task has run it has not been configured yet, and that future Ant versions are likely to drop task instances in favour of proxies, so treat id on a task as an advanced feature.

taskname

A different name for this task instance, shown in the square-bracket prefix of the log output instead of the task’s own name.

description

Room for a comment.

The text of using.html says that tasks can have a name attribute that appears in the log; the attribute is in fact called taskname, as the page on common attributes states, and the next example uses it. A name attribute on echo would be an unknown attribute.

References: id and refid

Every project element, not just tasks, can be given an id. A later element of the same type can then point at it with refid instead of repeating the structure. A path is the usual case: declare the classpath once at the top level and reuse it in several tasks, with classpathref on tasks that take a single classpath, or with <classpath refid="..."/>.

<path id="project.class.path">
  <pathelement location="lib/"/>
  <pathelement path="${java.class.path}/"/>
</path>

<javac ...>
  <classpath refid="project.class.path"/>
</javac>

Three properties of references are worth remembering:

  • The reference namespace is shared by the whole project, and it is flat. Two elements with the same id silently replace each other and the last one wins. With two paths both named p, pointing at first and second, the build prints the second and raises no warning at the default log level:

    Buildfile: /work/errors/dupid.xml
    
    show:
         [echo] /work/errors/second
    
    BUILD SUCCESSFUL
    Total time: 0 seconds
  • A reference to something that does not exist is an error at the point of use:

    BUILD FAILED
    /work/errors/bad-ref2.xml:3: Reference libs not found.
    
    Total time: 0 seconds
  • refid is not always a reference. The manual states that some tasks deliberately give refid another meaning, and the property task is the example: <property name="x" refid="p"/> turns a reference into a property value. The toString: property prefix turns a path reference into a string, and the ant.refid: prefix reads any reference; both are on the Properties page.

Command-line arguments: the arg element

Tasks that start another process (exec, java, apply) pass it command-line arguments through nested <arg> elements, which is how Ant gets quoting and path separators right on every operating system. An arg takes exactly one of five attributes:

Attribute Meaning

value

One argument, which may contain spaces and stays one argument.

file

A file name, replaced by its absolute path (resolved against the base directory).

path

A path-like string; : and ; are both accepted as separators and are converted to the platform’s own.

pathref

A reference to a path defined elsewhere, converted to the platform’s conventions.

line

A space-separated list of arguments that Ant splits roughly the way a Unix shell would.

prefix and suffix are optional on top of those five (Ant 1.8 and later) and are placed in front of or behind every part. The manual recommends avoiding line when possible, because the splitting rules can differ from what you expect. The program in this example prints each argument it receives on its own line, so the difference shows:

src/demo/ShowArgs.java
package demo;

public class ShowArgs {
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.println("arg[" + i + "] = " + args[i]);
        }
    }
}
anatomy/build.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="anatomy" default="show-args" basedir=".">
  <description>Attributes, nested elements, ids and references in one file.</description>

  <path id="demo.classpath">
    <pathelement location="build/classes"/>
  </path>

  <target name="compile" description="Compile the demo class">
    <mkdir dir="build/classes"/>
    <javac srcdir="src" destdir="build/classes" release="21" includeantruntime="false"/>
  </target>

  <target name="show-args" depends="compile" description="Run a program with every kind of arg">
    <echo taskname="note" message="classpath is ${toString:demo.classpath}"/>
    <java classname="demo.ShowArgs" classpathref="demo.classpath"
          fork="true" failonerror="true" taskname="demo">
      <arg value="--station SF"/>
      <arg file="../data/stations.csv"/>
      <arg path="src:build/classes"/>
      <arg pathref="demo.classpath"/>
      <arg line="--format json  --verbose"/>
    </java>
  </target>
</project>
Buildfile: /work/anatomy/build.xml

compile:
    [mkdir] Created dir: /work/anatomy/build/classes
    [javac] Compiling 1 source file to /work/anatomy/build/classes

show-args:
     [note] classpath is /work/anatomy/build/classes
     [demo] arg[0] = --station SF
     [demo] arg[1] = /work/data/stations.csv
     [demo] arg[2] = /work/anatomy/src:/work/anatomy/build/classes
     [demo] arg[3] = /work/anatomy/build/classes
     [demo] arg[4] = --format
     [demo] arg[5] = json
     [demo] arg[6] = --verbose

BUILD SUCCESSFUL
Total time: 0 seconds

value="--station SF" arrived as one argument, with its space; file became an absolute path; path and pathref turned into a single argument each; and line="--format json --verbose" was split into three arguments, the double space included. The example also shows taskname: the echo log lines are labelled [note] and the program’s output [demo]. In that build file ${toString:demo.classpath} is expanded by the property mechanism, not by the task. Note fork="true": it runs the program in a separate JVM, with its own system properties, exit code and jvmarg options (classpathref works without it too, through Ant’s own class loader); forking is covered on Running Programs.

Splitting a build file: XML entities versus import

A build file that grows past a few hundred lines is usually split. Ant offers two mechanisms, and they differ in a way that matters.

XML entity includes are a feature of XML itself rather than of Ant. A DOCTYPE declares an external entity that points at a file, and an &name; reference pastes the file’s content into the document before Ant ever sees it:

entities/build.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE project [
  <!ENTITY common SYSTEM "./common-targets.xml">
]>
<project name="entities" default="banner" basedir=".">
  &common;
</project>
entities/common-targets.xml
<target name="banner">
  <echo>Shared target pulled in as an XML entity</echo>
</target>
Buildfile: /work/entities/build.xml

banner:
     [echo] Shared target pulled in as an XML entity

BUILD SUCCESSFUL
Total time: 0 seconds

The included file is a fragment, not a project: it has no <project> element and cannot be run alone. Ant has no idea an include happened, which is the mechanism’s limit: no target overriding, no way for the fragment to find out where it lives, and no error message that names the fragment. The parser also warns when an entity path starts with file: (an earlier attempt at this example printed 'file:./common-targets.xml' in ... should be expressed simply as './common-targets.xml'), so the example uses the plain relative form. The Ant FAQ describes the technique.

The import task is the Ant-aware version. The imported file is a complete project, and Ant treats it as if it were pasted in, minus the top <project> element, with two extras that entity includes cannot give: target overriding and special properties. A target of the same name in the importing file wins over the imported one, which stays reachable under projectname.targetname; and for every imported file Ant defines ant.file.<projectname> so the file can find resources relative to its own location. import is allowed only at the top level, not inside a target.

entities/common-import.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="common">
  <target name="banner">
    <echo>Shared target pulled in with import (file ${ant.file.common})</echo>
  </target>
</project>
entities/build-import.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="importing" default="banner" basedir=".">
  <import file="common-import.xml"/>
</project>
Buildfile: /work/entities/build-import.xml

banner:
     [echo] Shared target pulled in with import (file /work/entities/common-import.xml)

BUILD SUCCESSFUL
Total time: 0 seconds

The imported target ran, and ant.file.common pointed at the imported file, not at the file named on the command line. Real projects use import for shared build logic: the tidepool modules will import a common.xml this way. The rest of that story (include versus import, subant, antcall, overriding and the as attribute) is on Multi-File Builds: import, include & subant.

A first build.xml for tidepool-model

tidepool-model is a library of four records and has no dependencies, so its build needs only four targets: create the output directories, compile, package, clean. Here is the file, with the properties declared once at the top and every path built from them:

tidepool-model/build.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool-model" default="jar" basedir=".">
  <description>The tidepool domain model: Station, Constituent, TideEvent and Height.</description>

  <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}/libs/tidepool-model.jar"/>

  <target name="init">
    <mkdir dir="${classes.dir}"/>
    <mkdir dir="${build.dir}/libs"/>
  </target>

  <target name="compile" depends="init" description="Compile the sources">
    <javac srcdir="${src.dir}" destdir="${classes.dir}"
           release="21" includeantruntime="false"/>
  </target>

  <target name="jar" depends="compile" description="Package the classes">
    <jar destfile="${jar.file}" basedir="${classes.dir}"/>
  </target>

  <target name="clean" description="Delete everything under build/">
    <delete dir="${build.dir}"/>
  </target>
</project>

Points worth reading in this short file:

  • basedir="." is written out although it is the default; it documents that the paths below are relative to this file.

  • location makes a property absolute against the base directory, where value stores the text as written. Paths almost always want location; the difference is explained on the Properties page.

  • Properties are immutable: the first definition wins, so ant -Dbuild.dir=out on the command line would override the file’s value.

  • Only init, compile, jar and clean are targets. init has no description, so it is internal; the depends chain jar to compile to init guarantees the directories exist by the time javac runs.

  • release="21" sets the language level and the platform API together, like javac --release. The attribute exists since Ant 1.9.8 and is ignored when Ant runs on JDK 8 or earlier. The library compiles on JDK 26 and targets Java 21.

  • includeantruntime="false" keeps Ant’s own classes off the compile classpath. Without the attribute javac prints this warning on every build: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds.

ant -p prints the project description, then the targets that have a description, then the default. Adding -v also lists the internal ones (the lines before the description, which report the Java version and the parsing of the build file, are left out here):

Buildfile: /work/tidepool-model/build.xml
The tidepool domain model: Station, Constituent, TideEvent and Height.
Main targets:

 clean    Delete everything under build/
 compile  Compile the sources
 jar      Package the classes
Default target: jar
The tidepool domain model: Station, Constituent, TideEvent and Height.
Main targets:

 clean    Delete everything under build/
 compile  Compile the sources
 jar      Package the classes
Other targets:

 init
Default target: jar

The default target runs the dependency chain once from the left:

Buildfile: /work/tidepool-model/build.xml

init:
    [mkdir] Created dir: /work/tidepool-model/build/classes
    [mkdir] Created dir: /work/tidepool-model/build/libs

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

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

BUILD SUCCESSFUL
Total time: 0 seconds

Run it again and the same targets are listed, but no task prints anything. mkdir finds the directories, javac finds class files newer than the sources, and jar finds an up-to-date archive:

Buildfile: /work/tidepool-model/build.xml

init:

compile:

jar:

BUILD SUCCESSFUL
Total time: 0 seconds

That up-to-date check is the build-tool equivalent of incremental builds, and it is per task and per file timestamp: the rules are on Compiling Java and Packaging Archives. The build file is the smallest useful one; the tidepool-engine and tidepool-cli builds will add classpaths and a main class on top of it.

The Hello World tutorial, walked through

The official tutorial is the best-known first Ant build, and it is worth following once in order, because each step introduces one idea. This walk-through reruns the tutorial’s steps on Ant 1.10.18 with the same package and file names (oata.HelloWorld, build/classes, build/jar), and adapts the later steps to the scenario: the external library is Commons CSV, which reads the tidepool station list, and the configuration file is a properties file instead of a logger configuration. All files were run, and the output is real.

Preparing the project and doing it by hand

The sources are in src; everything generated goes into build, split into classes for compiled files and jar for the archive. The class prints a fixed message. Doing it without Ant first makes the build file readable afterwards, because every task in it replaces one of these commands:

mkdir -p build/classes build/jar
javac --release 21 -d build/classes src/oata/HelloWorld.java
jar cfe build/jar/HelloWorld.jar oata.HelloWorld -C build/classes .
java -jar build/jar/HelloWorld.jar
Hello World

Here jar cfe writes the Main-Class manifest entry for you, where the tutorial creates the manifest file by hand. In the build file that job belongs to the manifest element.

Four steps to a running application

Four targets cover the whole life cycle of the command list: clean deletes generated files, compile calls mkdir and javac, jar packages with a manifest, and run starts the archive with fork="true" in its own JVM. The tutorial’s first version has no properties and no dependencies, so a clean build is ant clean compile jar run, typed in that order.

Enhance the build file

Three improvements remove the repetition. Properties name the directories and the main class once (src.dir, build.dir, classes.dir, jar.dir, main-class). The default attribute on project makes a plain ant do the right thing, and depends makes each target pull in its predecessors. The main target depends on clean and run, so a bare ant always builds from scratch, and clean-build is the same without the run. This is the build file after this step, with release and includeantruntime added for a current JDK:

helloworld/build.xml (step 3)
<project name="HelloWorld" basedir="." default="main">

    <property name="src.dir"     value="src"/>

    <property name="build.dir"   value="build"/>
    <property name="classes.dir" value="${build.dir}/classes"/>
    <property name="jar.dir"     value="${build.dir}/jar"/>

    <property name="main-class"  value="oata.HelloWorld"/>

    <target name="clean">
        <delete dir="${build.dir}"/>
    </target>

    <target name="compile">
        <mkdir dir="${classes.dir}"/>
        <javac srcdir="${src.dir}" destdir="${classes.dir}" release="21" includeantruntime="false"/>
    </target>

    <target name="jar" depends="compile">
        <mkdir dir="${jar.dir}"/>
        <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="${main-class}"/>
            </manifest>
        </jar>
    </target>

    <target name="run" depends="jar">
        <java jar="${jar.dir}/${ant.project.name}.jar" fork="true"/>
    </target>

    <target name="clean-build" depends="clean,jar"/>

    <target name="main" depends="clean,run"/>

</project>
Buildfile: /work/hello3/build.xml

clean:

compile:
    [mkdir] Created dir: /work/hello3/build/classes
    [javac] Compiling 1 source file to /work/hello3/build/classes

jar:
    [mkdir] Created dir: /work/hello3/build/jar
      [jar] Building jar: /work/hello3/build/jar/HelloWorld.jar

run:
     [java] Hello World

main:

BUILD SUCCESSFUL
Total time: 0 seconds

The line main: at the end is a target with no tasks of its own: it exists only for its depends. This "alias" idiom comes back on the Targets & Extension Points page.

Using external libraries

Ant has no dependency resolution of its own. Libraries are JAR files that you put somewhere (here the lib directory) and list in a path. The tutorial’s step is a path with a fileset, which includes every JAR it finds:

<path id="classpath">
    <fileset dir="${lib.dir}" includes="**/*.jar"/>
</path>

The javac task gets classpathref="classpath", and the java task needs the library JARs and the application JAR in its classpath, which the tutorial builds from the path plus a second path named application. Here the library is Commons CSV 1.14.1, copied by hand into lib, and the program counts the rows of data/stations.csv. Only commons-csv was copied at first, and the compile succeeded, but the run did not (output from the run target on):

run:
     [java] Hello World
     [java] Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/io/output/AppendableOutputStream
     [java] 	at oata.HelloWorld.countStations(Unknown Source)
     [java] 	at oata.HelloWorld.main(Unknown Source)
     [java] Caused by: java.lang.ClassNotFoundException: org.apache.commons.io.output.AppendableOutputStream
     [java] 	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
     [java] 	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:502)
     [java] 	... 2 more
     [java] Java Result: 1

BUILD SUCCESSFUL
Total time: 0 seconds

The build still printed BUILD SUCCESSFUL, because the java task’s failonerror attribute is off by default, so the child JVM’s non-zero exit code is only logged as Java Result: 1. The class that is missing belongs to Commons IO, which the Commons CSV POM lists as a dependency. That is the classic Ant lesson in one failure: the compile classpath may need only the library you call, but the run classpath needs everything that library calls, and Ant will not find those JARs for you. Fetching them by hand, as here (Commons IO 2.21.0 and Commons Codec 1.19.0), works for a tutorial. The real answers are Ivy (Ivy Overview) or the Maven Resolver Ant Tasks (Maven Resolver Ant Tasks), and the transitive rules they implement are on Dependency Management Fundamentals. Setting failonerror="true" on java is the first defence against a green build that did not work.

Configuration files

The tutorial’s next step puts a configuration file on the classpath. The idea is to copy files that are not Java into the classes directory so that they end up inside the JAR, and to read them with getResourceAsStream. In this version the file is src/resources/hello.properties with the single line greeting=Hello, the compile target gains a copy task, and the program builds its message from the property:

<copy todir="${classes.dir}">
    <fileset dir="${src.dir}/resources"/>
</copy>

Testing with JUnit

The last tutorial step adds a junit target that runs the tests and writes an XML report. The tests are compiled into their own directory, and the junit task runs the compiled classes with a classpath made of the libraries, the application JAR and the test classes. This is the complete build file after the library, configuration and test steps, using JUnit 4.13.2 (with Hamcrest Core 1.3) because that is the framework the junit task drives; JUnit Jupiter is run with junitlauncher and has its own page, Testing with JUnit & junitlauncher.

helloworld/build.xml
<project name="HelloWorld" basedir="." default="main">

    <property name="src.dir"     value="src"/>
    <property name="test.dir"    value="test"/>
    <property name="lib.dir"     value="lib"/>

    <property name="build.dir"   value="build"/>
    <property name="classes.dir" value="${build.dir}/classes"/>
    <property name="jar.dir"     value="${build.dir}/jar"/>
    <property name="test.classes.dir" value="${build.dir}/test-classes"/>
    <property name="report.dir"  value="${build.dir}/junitreport"/>

    <property name="main-class"  value="oata.HelloWorld"/>

    <path id="classpath">
        <fileset dir="${lib.dir}" includes="**/*.jar"/>
    </path>

    <path id="application" location="${jar.dir}/${ant.project.name}.jar"/>

    <target name="clean">
        <delete dir="${build.dir}"/>
    </target>

    <target name="compile">
        <mkdir dir="${classes.dir}"/>
        <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"
               release="21" includeantruntime="false"/>
        <copy todir="${classes.dir}">
            <fileset dir="${src.dir}/resources"/>
        </copy>
    </target>

    <target name="jar" depends="compile">
        <mkdir dir="${jar.dir}"/>
        <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="${main-class}"/>
            </manifest>
        </jar>
    </target>

    <target name="run" depends="jar">
        <java fork="true" classname="${main-class}">
            <classpath>
                <path refid="classpath"/>
                <path refid="application"/>
            </classpath>
            <arg file="../data/stations.csv"/>
        </java>
    </target>

    <target name="junit" depends="jar">
        <mkdir dir="${test.classes.dir}"/>
        <javac srcdir="${test.dir}" destdir="${test.classes.dir}" release="21" includeantruntime="false">
            <classpath>
                <path refid="classpath"/>
                <path refid="application"/>
            </classpath>
        </javac>
        <mkdir dir="${report.dir}"/>
        <junit printsummary="yes" haltonfailure="yes">
            <classpath>
                <path refid="classpath"/>
                <path refid="application"/>
                <pathelement location="${test.classes.dir}"/>
            </classpath>

            <formatter type="xml"/>

            <batchtest fork="yes" todir="${report.dir}">
                <fileset dir="${test.classes.dir}" includes="**/*Test.class"/>
            </batchtest>
        </junit>
    </target>

    <target name="clean-build" depends="clean,jar"/>

    <target name="main" depends="clean,run"/>

</project>
test/oata/HelloWorldTest.java
package oata;

import static org.junit.Assert.assertEquals;

import java.nio.file.Path;
import org.junit.Test;

public class HelloWorldTest {

    @Test
    public void greetingComesFromConfiguration() throws Exception {
        assertEquals("Hello World", HelloWorld.greeting());
    }

    @Test
    public void countsTheStationsInTheCsv() throws Exception {
        assertEquals(3, HelloWorld.countStations(Path.of("../data/stations.csv")));
    }
}

Running ant clean junit compiles, packages and then tests; ant clean run shows the configuration and the library working together:

Buildfile: /work/helloworld/build.xml

clean:
   [delete] Deleting directory /work/helloworld/build

compile:
    [mkdir] Created dir: /work/helloworld/build/classes
    [javac] Compiling 1 source file to /work/helloworld/build/classes
     [copy] Copying 1 file to /work/helloworld/build/classes

jar:
    [mkdir] Created dir: /work/helloworld/build/jar
      [jar] Building jar: /work/helloworld/build/jar/HelloWorld.jar

junit:
    [mkdir] Created dir: /work/helloworld/build/test-classes
    [javac] Compiling 1 source file to /work/helloworld/build/test-classes
    [mkdir] Created dir: /work/helloworld/build/junitreport
    [junit] Running oata.HelloWorldTest
    [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0,047 sec

BUILD SUCCESSFUL
Total time: 0 seconds
Buildfile: /work/helloworld/build.xml

clean:
   [delete] Deleting directory /work/helloworld/build

compile:
    [mkdir] Created dir: /work/helloworld/build/classes
    [javac] Compiling 1 source file to /work/helloworld/build/classes
     [copy] Copying 1 file to /work/helloworld/build/classes

jar:
    [mkdir] Created dir: /work/helloworld/build/jar
      [jar] Building jar: /work/helloworld/build/jar/HelloWorld.jar

run:
     [java] Hello World
     [java] Stations in /work/data/stations.csv: 3

BUILD SUCCESSFUL
Total time: 0 seconds

The junit task itself lives in ant-junit.jar, which ships in Ant’s lib directory, so no -lib option was needed; the JUnit and Hamcrest JARs the tests use sit in lib and in the task’s classpath. The decimal comma in Time elapsed: 0,047 sec comes from the default locale of the machine that ran it. The haltonfailure="yes" attribute fails the build on a failing test, which is what a CI job wants; the default is no, which reports the failure and carries on. The XML written to build/junitreport is the input to the junitreport task.

Common mistakes

  • A misspelt task or attribute. The parser reports failed to create task or type javaq or mkdir doesn't support the "dirr" attribute, with the build file and line, but only when the target runs.

  • A relative path resolved against the wrong directory. Paths resolve against basedir, which is the directory of the build file, not the shell’s working directory. The first line of every run, Buildfile: ..., tells you which file Ant read; a "does not exist" error followed by a path under basedir is the sign.

  • A default pointing at nothing. The build fails with Target "build" does not exist in the project "bad".

  • A destdir that nobody created. javac fails with destination directory ... does not exist or is not a directory; that is why mkdir sits in front of javac in every build file on this page.

  • A green build that did not work. java and exec do not fail the build on a non-zero exit code unless failonerror is set, and junit carries on after a failing test unless haltonfailure is set.

  • A build file that is not well-formed XML. The parser reports the line, here a missing end tag:

BUILD FAILED
/work/errors/bad-xml.xml:4: The element type "echo" must be terminated by the matching end-tag "</echo>".

Total time: 0 seconds

Where the books are outdated

  • Java Development with Ant (Manning, 2002) was written for Ant 1.5, before Ant 1.6 allowed any task outside a target; it therefore treats property, taskdef and typedef as the only top-level elements. Today top-level tasks are normal and run at parse time, as the first example above shows.

  • The book’s javac examples set source and target for old JDKs. The release attribute (Ant 1.9.8) is the current way, and includeantruntime="false" is now expected on every javac.

  • Build files from that era put the JUnit JAR into ANT_HOME/lib, or relied on CLASSPATH. The manual’s advice today is to leave the CLASSPATH environment variable alone and use -lib, ~/.ant/lib or ANT_HOME/lib; see Installation.

Where to go next