Embedding Ant
|
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. |
Ant is a Java library with a command line on top of it. Anything the ant script does can be done from Java: load a build file, run
a target, listen to the events, or skip the build file altogether and run one task as an object. IDEs and build servers do this, and so do
the Groovy AntBuilder and Gradle’s ant object (see below). This page shows the API in programs that were compiled and run, and the
two extension points that change how Ant starts: ProjectHelper (the build file front end) and ArgumentProcessor (the command line).
It is written against Apache Ant 1.10.18; every program was compiled with javac and run on 2026-10-16 with Oracle JDK 26 on macOS, with
the Ant 1.10.18 JARs from the distribution’s lib directory ($ANT_HOME below). Paths in console output were shortened to ..
The classes are documented in the Javadoc of the manual (API index). The books on Ant, and the manual page for using tasks outside Ant, predate several of the changes noted below; the code here is checked against the 1.10.18 JARs.
Which JARs
The core is two JARs in $ANT_HOME/lib:
| JAR | Contents |
|---|---|
|
The API: |
|
|
|
The optional tasks, one JAR for each group, and the third-party library each needs. See Installation. |
ant-launcher.jar is needed at run time even when the program never touches the launcher, because Project.init() loads AntMain. Run
with ant.jar alone, the first call to init() fails:
$ java -cp out:$ANT_HOME/lib/ant.jar EmbedProps props.xml
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/tools/ant/launch/AntMain
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:974)
...
(The stack goes on through Project.initProperties(Project.java:322) and Project.init.) This is the same JDK 26 run as every other example here and the message is
real; nothing else was missing. The class path of an embedding application is therefore ant.jar and ant-launcher.jar, plus whatever the build files it
runs use. An optional task without its JAR fails the way it does on the command line:
BuildException: Problem: failed to create task or type junitlauncher
Cause: the class org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.JUnitLauncherTask was not found.
This looks like one of Ant's optional components.
Action: Check that the appropriate optional JAR exists in
-ANT_HOME/lib
-the IDE Ant configuration dialogs
With ant-junitlauncher.jar added to the class path the same call found the class and the task ran. Compiling code that extends Main also needs
ant-launcher.jar on the compile class path (javac stops with "class file for org.apache.tools.ant.launch.AntMain not found" otherwise).
Source: Ant manual, running Ant via Java
Running a build file
The smallest embedding is five steps: create a Project, attach a logger, call init(), parse the build file into the project with
ProjectHelper.configureProject, and execute a target. The build file is the small tidepool one used on this page:
<project name="tidepool-embedded" default="check" basedir=".">
<property name="stations" value="data/stations.csv"/>
<property name="mode" value="strict"/>
<target name="check" description="Count the stations">
<available file="${stations}" property="stations.found"/>
<echo message="mode=${mode}, stations file present: ${stations.found}"/>
<echo level="verbose" message="this line is only visible at verbose level"/>
</target>
<target name="report" depends="check" description="Depends on check">
<echo message="report done"/>
</target>
<target name="broken" description="Always fails">
<fail message="station file is invalid"/>
</target>
</project>
and the program:
import java.io.File;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
public class EmbedBuild {
public static void main(String[] args) {
File buildFile = new File(args[0]);
String target = args[1];
Project project = new Project();
DefaultLogger logger = new DefaultLogger();
logger.setOutputPrintStream(System.out);
logger.setErrorPrintStream(System.err);
logger.setMessageOutputLevel(Project.MSG_INFO);
project.addBuildListener(logger);
project.init(); // register the default tasks and types
project.setUserProperty("mode", "embedded"); // user properties beat build file values
ProjectHelper.configureProject(project, buildFile);
project.fireBuildStarted();
Throwable failure = null;
try {
project.executeTarget(target);
} catch (Throwable t) {
failure = t;
} finally {
project.fireBuildFinished(failure);
}
System.out.println("failure = " + failure);
}
}
$ java -cp out:$ANT_HOME/lib/ant.jar:$ANT_HOME/lib/ant-launcher.jar EmbedBuild build.xml report
check:
[echo] mode=embedded, stations file present: true
report:
[echo] report done
BUILD SUCCESSFUL
Total time: 0 seconds
failure = null
$ java -cp out:$ANT_HOME/lib/ant.jar:$ANT_HOME/lib/ant-launcher.jar EmbedBuild build.xml broken
broken:
BUILD FAILED
./build.xml:16: station file is invalid
Total time: 0 seconds
failure = ./build.xml:16: station file is invalid
What each step does, and why it is that way:
-
new Project()is an empty project.DefaultLoggeris the same logger the command line installs; it writes the[task]prefixes, the target names and theBUILD SUCCESSFUL/BUILD FAILEDfooter. Without a listener nothing is printed, because aProjecthas no logger of its own. -
init()registers the built-in tasks and types and sets the properties derived from the environment (ant.versionamong them). -
setUserPropertyis the programmatic-Dmode=embedded. The build file saysmodeisstrict; the user property won, because a build file cannot override a user property (the output showsmode=embedded). See Properties. -
ProjectHelper.configureProject(project, file)parses the file with the project helper that supports it (the default is the XML one) and creates the targets. Tasks are not run yet, with one exception: top-level tasks outside of any target, such as<property>, are executed while the file is parsed. TheEmbedListenerrun below shows thepropertytasks starting before the targets are listed. -
fireBuildStarted()andfireBuildFinished(Throwable)are what the command line does aroundexecuteTarget. They are what makesBuildListener.buildStartedandbuildFinishedfire, and the logger’sBUILD SUCCESSFULfooter comes from the second one. A program that callsexecuteTargetalone gets the target and task events but no start or finish events. Pass the exception, ornull: thebrokenrun above printed the failure with the file name and line from the exception. -
executeTarget("report")runs the target and the targets it depends on, in order (checkfirst). For several,executeTargets(Vector<String>). A failing build raisesBuildException; thecatchkeeps it so that it can be passed tofireBuildFinished.
Other Project methods that matter to an embedder: setBaseDir(File), setKeepGoingMode(boolean) (the -keep-going switch),
setInputHandler(InputHandler) and setDefaultInputStream(InputStream) (where <input> reads from; see
Listeners, Loggers & Input Handlers), addReference(String, Object) (hand a Java object to the build as a
reference id), getTargets(), getDefaultTarget(), getProperty(String) and replaceProperties(String). Use one Project for one build; create a new one for the next run.
Source: Ant API, Project; Ant API, ProjectHelper
Listening to a build
A logger such as DefaultLogger is a BuildListener that also prints. Anything else (a progress bar, a test of what ran, a build-server integration)
implements BuildListener directly. It has seven methods: buildStarted, buildFinished, targetStarted, targetFinished, taskStarted, taskFinished and
messageLogged, each with a BuildEvent. BuildEvent gives the Project, the Target or Task the event is about, the message and its priority, and the exception on a finish event.
The next program traces the structure of the same build file and reads the results from the project afterwards:
import java.io.File;
import java.util.Vector;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
public class EmbedListener {
/** A BuildListener that only prints the structure of the build. */
static class Trace implements BuildListener {
public void buildStarted(BuildEvent e) { System.out.println("buildStarted"); }
public void buildFinished(BuildEvent e) {
System.out.println("buildFinished, exception = " + e.getException());
}
public void targetStarted(BuildEvent e) { System.out.println(" targetStarted " + e.getTarget().getName()); }
public void targetFinished(BuildEvent e) {
System.out.println(" targetFinished " + e.getTarget().getName() + ", exception = " + e.getException());
}
public void taskStarted(BuildEvent e) { System.out.println(" taskStarted " + e.getTask().getTaskName()); }
public void taskFinished(BuildEvent e) { System.out.println(" taskFinished " + e.getTask().getTaskName()); }
public void messageLogged(BuildEvent e) {
if (e.getPriority() <= Project.MSG_INFO) {
System.out.println(" [" + e.getPriority() + "] " + e.getMessage());
}
}
}
public static void main(String[] args) {
Project project = new Project();
project.addBuildListener(new Trace());
project.init();
project.setProperty("mode", "listener-demo");
ProjectHelper.configureProject(project, new File(args[0]));
System.out.println("targets: " + new java.util.TreeSet<>(project.getTargets().keySet()));
System.out.println("default target: " + project.getDefaultTarget());
project.executeTargets(new Vector<>(java.util.List.of(args[1])));
System.out.println("mode after build = " + project.getProperty("mode"));
System.out.println("stations.found = " + project.getProperty("stations.found"));
}
}
$ java -cp ... EmbedListener build.xml report
taskStarted property
taskFinished property
taskStarted property
taskFinished property
targets: [, broken, check, report]
default target: check
targetStarted check
taskStarted available
taskFinished available
taskStarted echo
[1] mode=listener-demo, stations file present: true
taskFinished echo
taskStarted echo
taskFinished echo
targetFinished check, exception = null
targetStarted report
taskStarted echo
[1] report done
taskFinished echo
targetFinished report, exception = null
mode after build = listener-demo
stations.found = true
Reading the trace:
-
The first four lines are the two
<property>tasks at the top ofbuild.xml, executed whileconfigureProjectparsed the file. They come before the program printed the target list, because tasks outside targets run at parse time. -
The list of targets contains an empty name: the implicit target that holds those top-level tasks.
-
There is no
buildStartedorbuildFinishedline. The program did not callfireBuildStarted()orfireBuildFinished(...); the listener only sees what is fired. -
The listener prints a message only when its priority is
MSG_INFO(2) or lower. Theechomessages show[1], which isMSG_WARN: theechotask defaults to the warning level, which is why a plain<echo>is visible even with-quiet. The verboseechoofcheckwas dropped by that test (the secondechotask start and finish have no line between them). -
The user-visible result of the build, here
stations.found, is read back withproject.getPropertyafterexecuteTargets.
Because BuildListener is an interface, the loggers shipped with Ant (BuildLogger subclasses) and listeners that
the command line loads with -listener and -logger are written the same way; they are described on
Listeners, Loggers & Input Handlers.
Using a task without a build file
A task is a Java class, and can be used as one. The manual has a page on the idea (a Project, a Target and a call to execute()). With 1.10.18 the
recipe is: give the task a Project (setProject), a name (setTaskName) and an owning Target (setOwningTarget), set its attributes with the Java setters, add
nested elements with addFileset and similar methods, and call execute(). The program builds a ZIP from the station CSV, unpacks it elsewhere, copies a file, prints a
message and deletes everything, using Zip, Expand, Copy, Echo and Delete:
import java.io.File;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Delete;
import org.apache.tools.ant.taskdefs.Echo;
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.ant.taskdefs.Zip;
import org.apache.tools.ant.types.FileSet;
public class EmbedTasks {
/** A Project with a logger and a Target, which is what a task needs to log and to find its owner. */
static Project newProject(File baseDir) {
Project project = new Project();
DefaultLogger logger = new DefaultLogger();
logger.setOutputPrintStream(System.out);
logger.setErrorPrintStream(System.err);
logger.setMessageOutputLevel(Project.MSG_INFO);
project.addBuildListener(logger);
project.init();
project.setBaseDir(baseDir);
return project;
}
static <T extends org.apache.tools.ant.Task> T bind(T task, Project project, String name) {
task.setProject(project);
task.setTaskName(name);
task.setOwningTarget(new Target());
return task;
}
public static void main(String[] args) {
File work = new File(args[0]).getAbsoluteFile();
Project project = newProject(work);
// 1. Build an archive from the CSV with <zip> ...
File zipFile = new File(work, "stations.zip");
Zip zip = bind(new Zip(), project, "zip");
zip.setDestFile(zipFile);
FileSet data = new FileSet();
data.setProject(project);
data.setDir(new File(work, "data"));
data.setIncludes("*.csv");
zip.addFileset(data);
zip.execute();
// 2. ... unpack it with <unzip> into another directory ...
Expand unzip = bind(new Expand(), project, "unzip");
unzip.setSrc(zipFile);
unzip.setDest(new File(work, "unpacked"));
unzip.execute();
// 3. ... copy it with <copy> using a property in the destination ...
project.setProperty("backup.dir", "backup");
Copy copy = bind(new Copy(), project, "copy");
copy.setFile(new File(work, "unpacked/stations.csv"));
copy.setTodir(new File(work, project.getProperty("backup.dir")));
copy.execute();
// 4. ... report with <echo>. Setters are called by Java, so no ${...} expansion happens by itself:
Echo echo = bind(new Echo(), project, "echo");
echo.setMessage("raw: backup written to ${backup.dir}");
echo.execute();
echo = bind(new Echo(), project, "echo");
echo.setMessage(project.replaceProperties("expanded: backup written to ${backup.dir}"));
echo.execute();
// 5. ... and clean up with <delete>.
Delete delete = bind(new Delete(), project, "delete");
delete.setFile(zipFile);
delete.execute();
Delete deleteDirs = bind(new Delete(), project, "delete");
deleteDirs.setDir(new File(work, "unpacked"));
deleteDirs.execute();
deleteDirs = bind(new Delete(), project, "delete");
deleteDirs.setDir(new File(work, "backup"));
deleteDirs.execute();
System.out.println("build date (Project.getBuildDate, since 1.10.18): " + project.getBuildDate());
}
}
$ java -cp ... EmbedTasks "$PWD"
[zip] Building zip: ./stations.zip
[unzip] Expanding: ./stations.zip into ./unpacked
[copy] Copying 1 file to ./backup
[echo] raw: backup written to ${backup.dir}
[echo] expanded: backup written to backup
[delete] Deleting: ./stations.zip
[delete] Deleting directory ./unpacked
[delete] Deleting directory ./backup
build date (Project.getBuildDate, since 1.10.18): Fri Oct 16 12:11:18 CEST 2026
The logger is what prints the [zip] and [copy] lines; each task logs under its task name. Three details are pitfalls:
-
Setters do not expand properties. The
raw:line shows${backup.dir}unexpanded, because Ant expands${...}when it configures a task from a build file, not when Java code calls a setter. Callproject.replaceProperties(String)yourself, as theexpanded:line does. -
The program sets a
Project, a task name and an owningTargeton every task, in thebindhelper. The manual’s example subclassesExpandand sets the protected fields in its constructor, which has the same effect. -
getBuildDate()is new in Ant 1.10.18. It is meant for tasks written in Java that need a "build date": it consults theSOURCE_DATE_EPOCHenvironment variable and the magicant.tstamp.nowandant.tstamp.now.isoproperties, in this order, so that such tasks honour reproducible builds the way<tstamp>does. Here it printed what looks like the current time, because none of them was set.
For more than a task or two, running a short build file from the program is less code than building the tasks by hand. The Groovy equivalent of this section is AntBuilder,
covered with a run on Groovy build tools; Gradle exposes the same
idea as the ant object in the build script, described on Ant Integration.
Starting Ant the way the command line does
Main is the class behind the ant script. It parses the same options (-f, -q, -D, …) and handles the logging and listeners that a real run has, so
embedding it is the shortest route to "the same as the command line". The API is:
-
public static void main(String[])is the traditional entry point and ends the JVM with the exit status. -
public void startAnt(String[] args, Properties additionalUserProperties, ClassLoader coreLoader)is theAntMainmethod the launcher calls: it runs the build and then callsexit(int). -
public static void start(String[], Properties, ClassLoader)is the static variant of the same call. -
protected void exit(int)is the hook: by default it ends the JVM. A subclass can override it to record the code and keep the JVM alive.
import java.util.Properties;
import org.apache.tools.ant.Main;
public class EmbedMain {
/** Main.exit() is protected and calls System.exit(); a subclass can keep the JVM alive and record the code. */
static class NoExitMain extends Main {
int code = -1;
@Override
protected void exit(int exitCode) {
this.code = exitCode;
}
}
public static void main(String[] args) {
Properties extra = new Properties();
extra.setProperty("mode", "from-startAnt");
NoExitMain ok = new NoExitMain();
ok.startAnt(new String[] {"-f", args[0], "-q", "report"}, extra, null);
System.out.println("exit code of the first build: " + ok.code);
NoExitMain bad = new NoExitMain();
bad.startAnt(new String[] {"-f", args[0], "-q", "broken"}, extra, null);
System.out.println("exit code of the second build: " + bad.code);
System.out.println("JVM is still alive");
}
}
$ java -cp ... EmbedMain build.xml
[echo] mode=from-startAnt, stations file present: true
[echo] report done
BUILD SUCCESSFUL
Total time: 0 seconds
exit code of the first build: 0
BUILD FAILED
./build.xml:16: station file is invalid
Total time: 0 seconds
exit code of the second build: 1
JVM is still alive
Two builds ran in one JVM; the exit codes (0 and 1) were captured by the override, and the program went on. Main.main itself, run
through java -cp ant.jar:ant-launcher.jar org.apache.tools.ant.Main -q broken on a failing build, exited with status 1 in the shell. The first argument
array is the command line: -q made the [echo] lines at warning level visible and nothing else, and the properties given as the second argument became user properties, so mode
came from Java, not from the build file.
The third argument of startAnt is the core class loader; the program passed null. When Ant
is started through Launcher instead (the class the script uses, with -Dant.home=<installation>), -lib works and the JARs of ANT_HOME/lib and ~/.ant/lib are
added; with Main directly they are not, and everything must be on the class path (see
Running Ant).
Properties of an embedded project
Several of the properties a build file may rely on are set by the launcher, and not by Project. The same build file, run through the script and embedded:
<project name="props" default="show">
<target name="show">
<echo>ant.version=${ant.version}</echo>
<echo>ant.home=${ant.home}</echo>
<echo>ant.library.dir=${ant.library.dir}</echo>
<echo>ant.core.lib=${ant.core.lib}</echo>
<echo>ant.file=${ant.file}</echo>
<echo>ant.project.name=${ant.project.name}</echo>
<echo>basedir=${basedir}</echo>
</target>
</project>
import java.io.File;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
public class EmbedProps {
public static void main(String[] a) {
Project p = new Project();
DefaultLogger l = new DefaultLogger();
l.setOutputPrintStream(System.out); l.setErrorPrintStream(System.err);
l.setMessageOutputLevel(Project.MSG_WARN);
p.addBuildListener(l);
p.init();
ProjectHelper.configureProject(p, new File(a[0]));
p.executeTarget("show");
}
}
$ ant -q -f props.xml # through the ant script
[echo] ant.version=Apache Ant(TM) version 1.10.18 compiled on September 3 2026
[echo] ant.home=$ANT_HOME
[echo] ant.library.dir=$ANT_HOME/lib
[echo] ant.core.lib=$ANT_HOME/lib/ant.jar
[echo] ant.file=./props.xml
[echo] ant.project.name=props
[echo] basedir=.
$ java -cp ... EmbedProps props.xml # embedded
[echo] ant.version=Apache Ant(TM) version 1.10.18 compiled on September 3 2026
[echo] ant.home=${ant.home}
[echo] ant.library.dir=${ant.library.dir}
[echo] ant.core.lib=$ANT_HOME/lib/ant.jar
[echo] ant.file=${ant.file}
[echo] ant.project.name=props
[echo] basedir=.
$ java -Dant.home=$ANT_HOME -cp ... EmbedProps props.xml
[echo] ant.version=Apache Ant(TM) version 1.10.18 compiled on September 3 2026
[echo] ant.home=$ANT_HOME
[echo] ant.library.dir=${ant.library.dir}
[echo] ant.core.lib=$ANT_HOME/lib/ant.jar
[echo] ant.file=${ant.file}
[echo] ant.project.name=props
[echo] basedir=.
ant.version, ant.project.name, basedir and ant.core.lib (the location of ant.jar, added in Ant 1.7, useful as a class path entry when compiling tasks) are present everywhere. ant.home
is there only when the JVM was started with -Dant.home, ant.library.dir is set by the launcher only, and ant.file, which the command line sets, is not set by configureProject. A build
file that runs embedded should not depend on those three, or the embedder should set them (setProperty("ant.file", ...)).
Source: Ant API, Main; Ant API, Launcher
Embedding fixes and 1.10.x history
-
1.10.17 fixed a regression in 1.10.16:
JavaEnvUtilsandFileUtilsdepended on each other statically, so loadingJavaEnvUtilsfirst caused aNullPointerExceptionin programmatic use on Windows. It affected Eclipse. If an embedding application is stuck on 1.10.16, upgrade. -
1.10.18 added
Project.getBuildDate()(above). -
Version history of Ant itself is on Versions & Release History.
Source: Ant 1.10.18 WHATSNEW
A different build file format: ProjectHelper
The ProjectHelper is the component that parses a build file and creates the Target and Task objects. The default one, ProjectHelper2, parses XML.
To read another format, extend org.apache.tools.ant.ProjectHelper and implement parse(Project, Object). Ant chooses a helper per file: it asks the registered helpers in order and
uses the first that returns true from canParseBuildFile(Resource); with no -f, the first helper whose getDefaultBuildFile() exists wins. The order is: the helper named by the
system property org.apache.tools.ant.ProjectHelper, the helpers declared in META-INF/services/org.apache.tools.ant.ProjectHelper on the class path (searched with Ant’s own class loader and with the
system class loader), then the default XML helper. The <projecthelper> task (Ant 1.8.2) installs one dynamically for <import>.
The manual’s text mentions a supportsBuildFile(File) method. In the 1.10.18 JARs the method is canParseBuildFile(Resource)
(javap on ant.jar lists no supportsBuildFile), and a class that overrides the old name does not compile. The toy helper below reads a format of its own, where target starts a target,
echo adds an Echo task, and default names the default target:
# a build file in a format of our own
default hello
target hello
echo Hello from a build file that is not XML
target bye
echo Goodbye
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.taskdefs.Echo;
import org.apache.tools.ant.types.Resource;
/**
* A toy front end: a ".tp" file has lines such as "target hello" and "echo Some text".
* "default hello" selects the default target.
*/
public class TpProjectHelper extends ProjectHelper {
@Override
public String getDefaultBuildFile() {
return "build.tp";
}
@Override
public boolean canParseBuildFile(Resource buildFile) {
return buildFile.getName().endsWith(".tp");
}
@Override
public void parse(Project project, Object source) throws BuildException {
File file = (File) source;
project.setUserProperty("ant.file", file.getAbsolutePath());
project.setBaseDir(file.getAbsoluteFile().getParentFile());
try {
Target current = null;
List<String> lines = Files.readAllLines(file.toPath());
for (String line : lines) {
line = line.strip();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] parts = line.split("\\s+", 2);
switch (parts[0]) {
case "default" -> project.setDefault(parts[1]);
case "target" -> {
current = new Target();
current.setName(parts[1]);
project.addTarget(current);
}
case "echo" -> {
if (current == null) {
throw new BuildException("echo outside of a target: " + line);
}
Echo echo = new Echo();
echo.setProject(project);
echo.setTaskName("echo");
echo.setMessage(parts[1]);
current.addTask(echo);
}
default -> throw new BuildException("cannot parse: " + line);
}
}
} catch (java.io.IOException e) {
throw new BuildException(e);
}
}
}
It is packaged as a JAR with the service file, META-INF/services/org.apache.tools.ant.ProjectHelper, containing one line: TpProjectHelper.
$ javac -cp $ANT_HOME/lib/ant.jar -d classes src/TpProjectHelper.java
$ mkdir -p classes/META-INF/services
$ echo TpProjectHelper > classes/META-INF/services/org.apache.tools.ant.ProjectHelper
$ (cd classes && jar cf ../tp-helper.jar .)
$ ant -lib tp-helper.jar -f hello.tp
Buildfile: ./hello.tp
hello:
[echo] Hello from a build file that is not XML
BUILD SUCCESSFUL
Total time: 0 seconds
$ ant -lib tp-helper.jar -f hello.tp bye
Buildfile: ./hello.tp
bye:
[echo] Goodbye
BUILD SUCCESSFUL
Total time: 0 seconds
The -lib option put the JAR on Ant’s class path, the service file registered the helper, and the .tp file was parsed by it. Without the JAR, Ant’s XML parser tried the file and failed
at the first character. The helper’s default build file name worked too: with a build.tp in the directory, no build.xml and no -f, Ant found it. (A build.xml in the directory also still
ran with the JAR installed, because the XML helper takes .xml files.) The runs below are the failure without the JAR, the default file name, and the registration with a system property
instead of the service file, using a directory plain that holds only TpProjectHelper.class:
$ ant -f hello.tp # without the helper jar
Buildfile: ./hello.tp
BUILD FAILED
./hello.tp:1: Content is not allowed in prolog.
Total time: 0 seconds
$ ant -lib tp-helper.jar # no -f, no build.xml in the directory
Buildfile: ./build.tp
hello:
[echo] Hello from a build file that is not XML
BUILD SUCCESSFUL
Total time: 0 seconds
$ ant -Dorg.apache.tools.ant.ProjectHelper=TpProjectHelper -lib plain -f hello.tp
Buildfile: ./hello.tp
BUILD FAILED
./hello.tp:1: Content is not allowed in prolog.
Total time: 0 seconds
$ ANT_OPTS=-Dorg.apache.tools.ant.ProjectHelper=TpProjectHelper ant -lib plain -f hello.tp
Buildfile: ./hello.tp
hello:
[echo] Hello from a build file that is not XML
BUILD SUCCESSFUL
Total time: 0 seconds
The helper can be registered with the system property org.apache.tools.ant.ProjectHelper instead of the service file, but it has to be a JVM system property. A -D on the ant command line sets an Ant property, not a system property, so the third run ignored it and Ant’s XML parser failed as without the JAR; passing the same -D in ANT_OPTS, which the ant script hands to the JVM, made the fourth run work. The manual notes that experiments with
a Java front end and a Groovy front end exist; they are not part of Ant. Since Ant 1.8.2, <import> also picks the right helper for the imported file, so an XML build can import a file in another format.
Extending the command line: ArgumentProcessor
An ArgumentProcessor adds options to the ant command line. It is registered in META-INF/services/org.apache.tools.ant.ArgumentProcessor (a file with the class name, in a JAR on Ant’s class path) or
with ArgumentProcessorRegistry.getInstance().registerArgumentProcessor(...) from embedding code. The interface has five methods: readArguments(String[], int),
handleArg(List<String>), prepareConfigure(Project, List<String>), handleArg(Project, List<String>) and printUsage(PrintStream). They are called at different moments: when
Ant meets an argument it does not know, after all arguments are read, just before the build file is parsed, and after it was parsed. The two handleArg methods return true to make Ant stop right away without an error (as -diagnostics and -projecthelp do).
How readArguments works is not obvious from the manual, and a probe processor that printed what it was given showed the following with ant -lib probe -probe one two:
-
readArgumentsis called with the whole argument array and the position of the unknown argument. Returning-1means "not mine" (then Ant reportsUnknown argument). Any other value means "handled". -
The value Ant uses is only that it is not
-1. Ant did not skip the words after-probe:oneandtwowere still treated as targets, and the build failed withTarget "one" does not exist. The words from the returned position onwards are handed to the processor later (handleArgreceived[one, two]whenreadArgumentsreturnedpos + 1). -
So an option that needs a value should be written as a single word,
-tidemode=spring, rather than-tidemode spring.
import java.io.PrintStream;
import java.util.List;
import org.apache.tools.ant.ArgumentProcessor;
import org.apache.tools.ant.Project;
/** Adds "-tidemode=name" to the Ant command line; the value becomes the user property tide.mode. */
public class TideModeProcessor implements ArgumentProcessor {
private static final String PREFIX = "-tidemode=";
private String mode;
@Override
public int readArguments(String[] args, int pos) {
if (args[pos].startsWith(PREFIX)) {
mode = args[pos].substring(PREFIX.length());
return pos + 1; // handled; any value other than -1 means "this word was mine"
}
return -1; // not mine
}
@Override
public boolean handleArg(List<String> args) {
return false; // true would make Ant stop right after the arguments are read
}
@Override
public void prepareConfigure(Project project, List<String> targets) {
if (mode != null) {
project.setUserProperty("tide.mode", mode);
}
}
@Override
public boolean handleArg(Project project, List<String> targets) {
return false; // true would make Ant stop after the build file has been parsed
}
@Override
public void printUsage(PrintStream out) {
out.println(" -tidemode=<name> sets the property tide.mode");
}
}
With the service file and the JAR, the new option works and shows up in the help text. Without the JAR, Ant rejects it, and a value in a separate word is rejected too:
$ ant -lib tide-args.jar -tidemode=spring
Buildfile: ./build.xml
show:
[echo] tide.mode=spring
BUILD SUCCESSFUL
Total time: 0 seconds
$ ant -tidemode=spring
Unknown argument: -tidemode=spring
ant [options] [target [target2 [target3] ...]]
$ ant -lib tide-args.jar -h
...
-main <class> override Ant's normal entry point
-tidemode=<name> sets the property tide.mode
$ ant -lib tide-args.jar -tidemode spring
Unknown argument: -tidemode
ant [options] [target [target2 [target3] ...]]
(The last two are shortened.) The processor set a user property, so ${tide.mode} is spring in the build file; a processor can equally add listeners or stop the run by returning
true from a handleArg method.
Source: Ant manual, ArgumentProcessor
Groovy, Gradle and the other tools
-
Groovy’s
AntBuildercreates aProjectfor you and turns method calls into tasks; its run is on Groovy build tools. -
Gradle’s
antobject is anAntBuilderover a Gradle-ownedProject, andant.importBuild("build.xml")turns Ant targets into Gradle tasks; see Ant Integration. -
Maven’s
maven-antrun-pluginruns Ant tasks inside a Maven build; Gradle 9.8.1 bundles Ant (1.10.17 in its distribution). -
The Maven Resolver Ant Tasks and Ivy are both Ant libraries loaded as antlibs; see Antlibs & Namespaces.
Where the books and older material are outdated
-
Material that shows
ProjectHelper.configureProjectwithoutinit()or withoutant-launcher.jaron the class path no longer runs:init()needsAntMain. -
supportsBuildFile(File)in the ProjectHelper manual iscanParseBuildFile(Resource)in 1.10.18. -
The task-outside-Ant page still says
ant.jaris all the application needs.ant-launcher.jaris needed too. -
Examples that call
Main.mainfrom application code end the JVM; useMain.start/startAntwith anexitoverride, or theProjectAPI.
References
-
Apache Ant manual: Using Apache Ant Tasks Outside of Ant
-
Apache Ant manual: The Apache Ant frontend: ProjectHelper
-
Apache Ant manual: The Command Line Processor Plugin: ArgumentProcessor
-
Apache Ant manual: Running Ant via Java
-
Apache Ant API: Javadoc index
-
Apache Ant source: WHATSNEW for 1.10.18