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.
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 |
|---|---|---|
|
|
a project per directory, named in |
|
lifecycle phase, or a plugin goal bound to one |
task and its |
task element and its attributes |
plugin goal and its |
task configured in the build script |
property ( |
POM property ( |
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 |
|---|---|
|
The name of the project. Ant exposes it as the built-in property |
|
The target that runs when none is named on the command line. It is optional, but without it a plain |
|
The directory from which every relative path in the file is resolved. A relative |
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:
<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:
<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
Source: Apache Ant manual: Projects
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
Source: Apache Ant manual: Targets
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.
Source: Apache Ant manual: Tasks
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,pathand theirpathelementchildren, covered on Paths, Filesets & Patterns; -
resource collections such as
filesetandzipfileset; -
command-line
argelements, described below; -
task-specific elements, for example
manifestandattributeinsidejar, orformatterandbatchtestinsidejunit.
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 |
|---|---|
|
A unique identifier for this task instance. A script or a Java task can find the instance later through it ( |
|
A different name for this task instance, shown in the square-bracket prefix of the log output instead of the task’s own name. |
|
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.
Source: Apache Ant manual: Tasks
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
idsilently replace each other and the last one wins. With two paths both namedp, pointing atfirstandsecond, 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 -
refidis not always a reference. The manual states that some tasks deliberately giverefidanother meaning, and thepropertytask is the example:<property name="x" refid="p"/>turns a reference into a property value. ThetoString:property prefix turns a path reference into a string, and theant.refid:prefix reads any reference; both are on the Properties page.
Source: Apache Ant manual: References
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 |
|---|---|
|
One argument, which may contain spaces and stays one argument. |
|
A file name, replaced by its absolute path (resolved against the base directory). |
|
A path-like string; |
|
A reference to a path defined elsewhere, converted to the platform’s conventions. |
|
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:
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]);
}
}
}
<?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:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE project [
<!ENTITY common SYSTEM "./common-targets.xml">
]>
<project name="entities" default="banner" basedir=".">
&common;
</project>
<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.
<?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>
<?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.
Source: Apache Ant manual: Import
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:
<?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. -
locationmakes a property absolute against the base directory, wherevaluestores the text as written. Paths almost always wantlocation; the difference is explained on the Properties page. -
Properties are immutable: the first definition wins, so
ant -Dbuild.dir=outon the command line would override the file’s value. -
Only
init,compile,jarandcleanare targets.inithas no description, so it is internal; thedependschainjartocompiletoinitguarantees the directories exist by the timejavacruns. -
release="21"sets the language level and the platform API together, likejavac --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 attributejavacprints 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.
Source: Apache Ant manual: javac
Source: Apache Ant manual: jar
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:
<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.
<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>
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.
Source: Apache Ant tutorial: JUnit
Common mistakes
-
A misspelt task or attribute. The parser reports
failed to create task or type javaqormkdir 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 underbasediris the sign. -
A
defaultpointing at nothing. The build fails withTarget "build" does not exist in the project "bad". -
A
destdirthat nobody created.javacfails withdestination directory ... does not exist or is not a directory; that is whymkdirsits in front ofjavacin every build file on this page. -
A green build that did not work.
javaandexecdo not fail the build on a non-zero exit code unlessfailonerroris set, andjunitcarries on after a failing test unlesshaltonfailureis 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,taskdefandtypedefas 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
javacexamples setsourceandtargetfor old JDKs. Thereleaseattribute (Ant 1.9.8) is the current way, andincludeantruntime="false"is now expected on everyjavac. -
Build files from that era put the JUnit JAR into
ANT_HOME/lib, or relied onCLASSPATH. The manual’s advice today is to leave theCLASSPATHenvironment variable alone and use-lib,~/.ant/liborANT_HOME/lib; see Installation.
Source: Apache Ant manual: javac
Where to go next
-
Targets & Extension Points for the full target rules.
-
Properties for
${...}expansion, immutability and the built-in properties. -
Paths, Filesets & Patterns for the path and fileset structures used above.
-
Running Ant for every command-line option.
-
Apache Ant for the rest of the Ant pages.