Ant Listeners, Loggers & Input Handlers
|
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 marked as run was executed on JDK 26 (macOS) on 2026-10-16
in a scratch directory; the output is shown as it was printed, with the long absolute path of the scratch directory
shortened to <scratch>. Anything that could not be run here is labelled "not run here" in the text.
An Ant build produces two kinds of information for the outside world: a stream of events ("the target compile started",
"the task javac logged this line") and, now and then, a question for the person running the build ("publish the tide
tables, y or n?"). Ant does not print either directly. Events go to listeners and loggers, questions go to an input
handler, and the command line chooses which implementation is used. That indirection is a deliberate design: the same
build file runs on a developer’s terminal, in a CI server that wants XML, inside an IDE that wants no prompts on the
console and in an unattended job that must never wait for a keyboard, and none of those environments needs to change the
build file. This page covers the three extension points, the loggers that ship with Ant, and how to write your own of each
kind. The running data is the tidepool scenario described on the
section landing page, reduced to a small standalone build file
(build.xml) with targets prepare, copy-data, quiet-target and all, one input target and one failing target.
Build events and who receives them
The core of Ant fires seven events, and everything on this page is a consumer of them:
| Event | Fired when |
|---|---|
build started / build finished |
once per Ant run; the finished event carries the exception when the build failed |
target started / target finished |
around every target of the build sequence (a target skipped by |
task started / task finished |
around every task |
message logged |
whenever a task, a target or Ant itself logs a line at one of the five levels (error, warning, info, verbose, debug) |
A project started by ant, antcall or subant is a new "project" that sends its own target and task events but never
a build started or finished event. Since Ant 1.6.2 the SubBuildListener extension of BuildListener adds two events,
sub-build started and sub-build finished, for exactly those cases.
Logging is synchronous: the loggers and listeners are called one after the other and the build blocks until the last one has returned, so a slow logger makes a slow build.
Listener or logger
A listener implements org.apache.tools.ant.BuildListener (seven methods, one per event). A logger implements
org.apache.tools.ant.BuildLogger, a BuildListener with four extra setters that Ant calls before the build starts:
public interface BuildLogger extends BuildListener {
void setMessageOutputLevel(int); // -quiet, -verbose, -debug choose this
void setOutputPrintStream(PrintStream); // the console or the -logfile
void setErrorPrintStream(PrintStream);
void setEmacsMode(boolean); // -emacs
}
That is the whole difference, and it has consequences:
-
A logger is told the output level, so it can drop messages the user did not ask for. A listener receives every message at every level and filters itself, if it wants to.
-
A logger is handed the output streams, so it honours
-logfile. A listener that prints has to decide where. -
A logger is selected with
-loggerand there can be only one, because it is the voice of the build. Listeners are selected with-listener, any number of them, and they add to the logger.
The two switches are not interchangeable. Asking for a plain listener as the logger fails at startup, and so does naming two loggers; both messages below were printed by Ant 1.10.18, and in both cases no target ran:
$ ant -lib build/classes -logger io.tidepool.ant.EventCounterListener all
Buildfile: <scratch>/build.xml
The specified logger class io.tidepool.ant.EventCounterListener could not be used because Class of unexpected Type: io.tidepool.ant.EventCounterListener expected : interface org.apache.tools.ant.BuildLogger
$ ant -logger a.B -logger c.D all
Only one logger class may be specified.
The command-line switches
| Switch | Effect |
|---|---|
|
the |
|
adds an instance of a |
|
sends the logger’s output to a file instead of the console |
|
logs "without adornments": no |
|
be extra quiet: only warnings and errors from tasks (message level warning) |
|
print nothing but task outputs and build failures |
|
be extra verbose: adds the verbose level (what a task decided and why) |
|
print debugging information: adds the debug level, which includes how Ant parsed the build file and every property it set |
|
adds jars and directories to the classpath Ant uses, which is how a custom logger, listener or input handler is found |
|
do not allow interactive input |
|
the |
Everything the command line accepts can also be put in the ANT_ARGS environment variable, which is the natural place to
choose a logger for a whole CI job without touching the build files: ANT_ARGS="-lib build/classes -logger
io.tidepool.ant.TimingLogger" produced the same output as the equivalent command line in the run shown later on this page.
-listener, -logger and -inputhandler all need the class on Ant’s own classpath, so a custom class must be reachable
through -lib, ANT_HOME/lib or ~/.ant/lib, not through a classpath attribute inside the build file; the build file
is parsed after the logger has already been created.
The same build at five verbosity levels
The all target logs at several levels: prepare and copy-data run tasks, and the build file contains one echo at
the verbose level, one at the debug level and one at the warning level. A subtle point of the echo task is that its
default level is warning, not info, which is why plain echo lines survive -quiet. The number of lines printed by
ant <switch> all on this build was 3 for -S, 5 for -q, 16 for the default, 29 for -v and 58 for -d. The extremes
look like this:
$ ant -S all
Buildfile: <scratch>/build.xml
preparing
check the stations file
$ ant -q all
[echo] preparing
[echo] check the stations file
BUILD SUCCESSFUL
Total time: 0 seconds
With -S the task output loses its [echo] prefix and the final banner disappears, but the failure message does not:
$ ant -S fail
Buildfile: <scratch>/build.xml
BUILD FAILED
<scratch>/build.xml:31: stations file is stale
Total time: 0 seconds
Under -v the build log gains lines such as Skipping <scratch>/build/out because it already exists. from mkdir and
stations.csv omitted as <scratch>/build/out/stations.csv is up to date. from copy; those are the lines that explain why a rebuild did nothing. Under
-d it additionally shows one line per built-in property (for example Setting ro project property: ant.file -> <scratch>/build.xml), the lines that report the
parsing of the build file, the dependency order of the targets (Build sequence for target(s) `all' is [prepare, copy-data, quiet-target, all]) and
the class that executes the build (DefaultExecutor). Use -d when a property or a path does not have the value you expect.
Emacs mode and the log file
-emacs removes the [task] prefix of every message, so the error lines of a compiler reach an editor
or a CI problem matcher untouched:
$ ant -e all
Buildfile: <scratch>/build.xml
prepare:
Created dir: <scratch>/build/out
preparing
copy-data:
Copying 1 file to <scratch>/build/out
check the stations file
quiet-target:
all:
BUILD SUCCESSFUL
Total time: 0 seconds
-logfile sends the logger’s output to a file. With the default logger the console receives only the Buildfile: line
(Ant prints it before the logger takes over); everything else, including BUILD SUCCESSFUL, goes to the file:
$ ant -logfile build/ant.log all
Buildfile: <scratch>/build.xml
$ head -5 build/ant.log
prepare:
[mkdir] Created dir: <scratch>/build/out
[echo] preparing
A build that logs to a file still needs its failure to be visible: the exit code of ant is non-zero when the build fails
(see Running Ant), so CI should look at the exit code
and publish the file as an artifact.
The loggers that ship with Ant
| Class | Type | What it does |
|---|---|---|
|
logger |
the logger used unless |
|
logger |
as |
|
logger |
as |
|
logger |
as |
|
logger |
writes the build to an XML file (as a listener) or to the console or |
|
logger |
as |
|
logger |
prefixes every target with its project name and brackets each sub-project with Entering/Exiting lines |
|
logger |
the project-qualified target names of |
|
logger |
|
|
listener |
passes events to Log4j 1.x; deprecated because Log4j 1.x has been unmaintained since 2012 and has known vulnerabilities (the manual says so) |
All the following runs use the same all target.
NoBannerLogger
NoBannerLogger is the same output without the lines of empty targets, which in a build with a hundred small
targets (init, check-xyz) is a large part of the noise. quiet-target: and all: have disappeared:
$ ant -logger org.apache.tools.ant.NoBannerLogger all
Buildfile: <scratch>/build.xml
prepare:
[mkdir] Created dir: <scratch>/build/out
[echo] preparing
copy-data:
[copy] Copying 1 file to <scratch>/build/out
[echo] check the stations file
BUILD SUCCESSFUL
Total time: 0 seconds
TimestampedLogger
TimestampedLogger changes only the last line. Its main use is a log that is read hours later and has to say when the
build ended. The format follows the default locale of the JVM, which is why this machine printed a two-digit year:
BUILD SUCCESSFUL - at 16/10/26, 12:00
Total time: 0 seconds
BigProjectLogger and SimpleBigProjectLogger
Both loggers exist for builds made of many sub-builds started with subant or ant (see
Multi-File Builds),
where the plain log does not say which project a target belongs to. BigProjectLogger also hides targets without output
and adds the Entering and Exiting banners:
$ ant -logger org.apache.tools.ant.listener.BigProjectLogger all
Buildfile: <scratch>/build.xml
======================================================================
Entering project
In <scratch>
======================================================================
tidepool-logging.prepare:
[mkdir] Created dir: <scratch>/build/out
[echo] preparing
tidepool-logging.copy-data:
[copy] Copying 1 file to <scratch>/build/out
[echo] check the stations file
======================================================================
Exiting project "tidepool-logging"
======================================================================
BUILD SUCCESSFUL - at 16/10/26, 12:00
Total time: 0 seconds
Two details are worth noticing. The top-level Entering project line has an empty name in 1.10.18, because the
logger announces the entry on the build-started event, which is fired before the build file has been parsed and the
project has a name; the sub-project banners (see below) do show the name. And the target names are prefixed by the project name, which is the point of
the logger. SimpleBigProjectLogger prints only the qualified target names and none of the banners:
$ ant -logger org.apache.tools.ant.listener.SimpleBigProjectLogger all
Buildfile: <scratch>/build.xml
tidepool-logging.prepare:
[mkdir] Created dir: <scratch>/build/out
[echo] preparing
tidepool-logging.copy-data:
[copy] Copying 1 file to <scratch>/build/out
[echo] check the stations file
BUILD SUCCESSFUL
Total time: 0 seconds
With a nested target that runs the same file through ant and subant, BigProjectLogger prints one Entering and
Exiting pair per sub-build, each with the project name, inside the pair of the outer build, whose Entering banner has no
name for the reason given above:
$ ant -logger org.apache.tools.ant.listener.BigProjectLogger nested
Buildfile: <scratch>/build.xml
======================================================================
Entering project
In <scratch>
======================================================================
======================================================================
Entering project "tidepool-logging"
In <scratch>
======================================================================
======================================================================
Exiting project "tidepool-logging"
======================================================================
======================================================================
Entering project "tidepool-logging"
In <scratch>
======================================================================
======================================================================
Exiting project "tidepool-logging"
======================================================================
======================================================================
Exiting project "tidepool-logging"
======================================================================
BUILD SUCCESSFUL - at 17/10/26, 12:00
Total time: 0 seconds
ProfileLogger
ProfileLogger is the cheapest profiler a build can have. It prints a "started" and a "finished" line for every target and
task, with the time it took. The number in parentheses is milliseconds; the manual’s example shows an ms suffix, Ant
1.10.18 printed the bare number. This is the first part of the real output for the all target:
$ ant -logger org.apache.tools.ant.listener.ProfileLogger all
Buildfile: <scratch>/build.xml
Target prepare: started Fri Oct 16 12:00:27 CEST 2026
mkdir: started Fri Oct 16 12:00:27 CEST 2026
[mkdir] Created dir: <scratch>/build/out
mkdir: finished Fri Oct 16 12:00:27 CEST 2026 (48)
echo: started Fri Oct 16 12:00:27 CEST 2026
[echo] preparing
echo: finished Fri Oct 16 12:00:27 CEST 2026 (2)
Target prepare: finished Fri Oct 16 12:00:27 CEST 2026 (56)
The target prepare took 56 ms, 48 of them in mkdir, the first task of the run. For long builds, pipe the output through a filter for the "finished" lines and sort by the number.
Source: Apache Ant manual: ProfileLogger
AnsiColorLogger
AnsiColorLogger wraps each message in an ANSI colour escape sequence chosen by the message’s level. Piped through
cat -v, which makes the escape character visible as ^[, the first lines are:
$ ant -logger org.apache.tools.ant.listener.AnsiColorLogger all | cat -v
Buildfile: <scratch>/build.xml
^[[2;32m
prepare:^[[m
^[[2;36m [mkdir] Created dir: <scratch>/build/out^[[m
^[[2;35m [echo] preparing^[[m
^[[2;32m
copy-data:^[[m
2;32 is "dim, green" (the target line), 2;36 "dim, cyan" (info) and 2;35 "dim, magenta" (the warning level of
echo). The colours are the defaults of the logger; five keys override them (AnsiColorLogger.ERROR_COLOR,
WARNING_COLOR, INFO_COLOR, VERBOSE_COLOR, DEBUG_COLOR), each in the form attribute;foreground;background, in a
properties file named by the system property ant.logger.defaults. A system property is not an Ant property, so it goes
through ANT_OPTS, not -D on the ant command line:
$ cat ansi.properties
AnsiColorLogger.WARNING_COLOR=1;33
AnsiColorLogger.INFO_COLOR=0;37
$ ANT_OPTS="-Dant.logger.defaults=ansi.properties" ant -logger org.apache.tools.ant.listener.AnsiColorLogger all | cat -v
Buildfile: <scratch>/build.xml
^[[2;32m
prepare:^[[m
^[[0;37m [mkdir] Created dir: <scratch>/build/out^[[m
^[[1;33m [echo] preparing^[[m
The attribute codes are 0 reset, 1 bright, 2 dim, 3 underline, 5 blink, 7 reverse and 8 hidden; the foreground codes are
30 to 37 and the background codes 40 to 47. With -logfile the escape codes go into the file too, which is readable
with cat or less -R but unreadable in most CI log viewers; choose the colour logger for terminals only. The manual
states that it does not work on Windows NT and its successors even with ANSI.SYS.
XmlLogger
XmlLogger writes the build as an XML document, which a CI server can turn into a report. It works in two modes, with
different consequences:
-
As a logger (
-logger org.apache.tools.ant.XmlLogger) it writes to the console, or to-logfile, and honours-quiet,-verboseand-debug. -
As a listener (
-listener org.apache.tools.ant.XmlLogger) it runs beside the normal logger and writeslog.xml, or the file named by the propertyXmlLogger.file, in the current directory, and it records the messages of every level, including the debug ones, whatever the level of the console.
In both modes the file is written only when the build ends, because it buffers the events to compute the timings. By
default the file refers to a style sheet log.xsl, which is shipped in ANT_HOME/etc; the system property
ant.XmlLogger.stylesheet.uri changes the reference, and an empty value omits the processing instruction. This is the
real output of the logger mode on the all target:
$ ant -logger org.apache.tools.ant.XmlLogger -logfile build/build_log.xml all
Buildfile: <scratch>/build.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="log.xsl"?>
<build time="0 seconds">
<target name="prepare" time="0 seconds">
<task location="<scratch>/build.xml:3: " name="mkdir" time="0 seconds">
<message priority="info"><![CDATA[Created dir: <scratch>/build/out]]></message>
</task>
<task location="<scratch>/build.xml:4: " name="echo" time="0 seconds">
<message priority="warn"><![CDATA[preparing]]></message>
</task>
</target>
<target name="copy-data" time="0 seconds">
<task location="<scratch>/build.xml:7: " name="copy" time="0 seconds">
<message priority="info"><![CDATA[Copying 1 file to <scratch>/build/out]]></message>
</task>
<task location="<scratch>/build.xml:8: " name="echo" time="0 seconds" />
<task location="<scratch>/build.xml:9: " name="echo" time="0 seconds" />
<task location="<scratch>/build.xml:10: " name="echo" time="0 seconds">
<message priority="warn"><![CDATA[check the stations file]]></message>
</task>
</target>
<target name="quiet-target" time="0 seconds" />
<target name="all" time="0 seconds" />
</build>
The two echo tasks at lines 8 and 9 have no message because their levels (verbose and debug) were below the level of
the run. Run as a listener with the default console logger, the same build produced a log.xml of 7175 bytes, because it
also contained every debug message (Adding reference: ant.PropertyHelper, Detected Java version: 26, …). A listener
that you want for CI therefore usually writes more than a logger would; choose the mode by that.
Because the output is XML, an xslt task can turn it into a report. The following target applies the style sheet from
ANT_HOME/etc/log.xsl, then extracts the warnings from the generated HTML with a filter chain (see
Filters & Mappers and
Documentation & XML):
<target name="report">
<xslt in="build/build_log.xml" out="build/build_log.html" style="${ant.home}/etc/log.xsl" force="true"/>
<loadfile srcfile="build/build_log.html" property="html.hits">
<filterchain>
<linecontains><contains value='class="warn"'/></linecontains>
<tokenfilter>
<replaceregex pattern="^\s*<td[^>]*>([^<]*)</td>.*class="([a-z]+)"[^>]*>([^<]*)</td>.*$" replace="\1 | \2 | \3"/>
</tokenfilter>
</filterchain>
</loadfile>
<echo message="${html.hits}"/>
</target>
report:
[xslt] Processing <scratch>/build/build_log.xml to <scratch>/build/build_log.html
[xslt] Loading stylesheet log.xsl
[echo] prepare | warn | preparing
[echo] copy-data | warn | check the stations file
The report is an ordinary HTML table with one row per message and a CSS class per level (info, warn, error,
verbose, debug), which is why the filter could pick the warnings. Most CI servers have their own parsers; the XML is
mainly useful when no such parser exists.
Source: Apache Ant manual: XmlLogger
MailLogger
MailLogger captures the output that DefaultLogger would print and sends it by e-mail when the build ends, with
separate recipients and subjects for success and failure. All its settings are properties; the manual lists them all, and
the ones used most are below. They can be given as Ant properties (-D on the command line, or in the build file) or in a
properties file named by MailLogger.properties.file, whose values override the others.
| Property | Meaning |
|---|---|
|
the SMTP server (defaults |
|
sender (required when mail is sent) and reply-to addresses |
|
recipients, comma-separated; mail is sent only if the matching |
|
turn one kind of mail off (default true) |
|
subjects (defaults |
|
a fixed body instead of the full log (since 1.8.0) |
|
|
|
SMTP authentication and SSL; these need JavaMail (the |
|
STARTTLS, and the server-identity check that Ant 1.10.16 added and that defaults to true |
The run below sends the real mail to a throwaway SMTP sink on localhost:2525. The sink is a few lines of Python (not
part of Ant) that accepts one message and writes it to mail.txt; no real host, user or password is involved:
import socketserver
class Handler(socketserver.StreamRequestHandler):
def handle(self):
w = lambda s: self.wfile.write((s + "\r\n").encode())
w("220 localhost sink")
data, lines = False, []
for raw in self.rfile:
line = raw.decode(errors="replace").rstrip("\r\n")
if data:
if line == ".":
data = False
w("250 queued")
else:
lines.append(line)
continue
if line.upper().startswith("DATA"):
data = True
w("354 go ahead")
elif line.upper().startswith("QUIT"):
w("221 bye")
break
else:
w("250 ok")
open("mail.txt", "w").write("\n".join(lines) + "\n")
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("127.0.0.1", 2525), Handler) as s:
s.handle_request()
$ cat mail.properties
MailLogger.mailhost=localhost
MailLogger.port=2525
MailLogger.from=build@example.invalid
MailLogger.failure.to=team@example.invalid
MailLogger.success.to=team@example.invalid
MailLogger.success.subject=Tidepool build OK
MailLogger.failure.subject=Tidepool build FAILED
A failing build with a fixed failure body, so that the mail does not carry the whole log:
$ ant -logger org.apache.tools.ant.listener.MailLogger -DMailLogger.properties.file=mail.properties \
-DMailLogger.failure.body="The tide build failed; see the CI log." fail
$ cat mail.txt
Date: Fri, 16 Oct 2026 12:02:55 +0200
Subject: Tidepool build FAILED
Content-Type: text/plain
From: build@example.invalid
To: team@example.invalid
X-Mailer: org.apache.tools.mail.MailMessage (ant.apache.org)
The tide build failed; see the CI log.
The successful run sent Subject: Tidepool build OK with the complete log as the body. Authentication, SSL and STARTTLS
need JavaMail and a real SMTP server, so they were not run here. The mail goes out only when the build finishes, so a
build that is killed (CI timeout) sends nothing: pair it with a watchdog if the mail is the only alarm.
Source: Apache Ant manual: MailLogger
Log4jListener
Log4jListener passes each event to Apache Log4j 1.x, using the class name of the generator of the event as the category
(org.apache.tools.ant.Project for build events, org.apache.tools.ant.Target for targets, the task class for tasks).
It is documented as deprecated, because Log4j 1.x has not been developed since the release of 1.2.17 in 2012 and has
known vulnerabilities (Ant 1.10.2 also deprecated the listener, as the release history notes).
The manual describes a bridge: put log4j-1.2-api, log4j-api and log4j-core of Log4j 2 and a log4j2.xml on the
classpath with -lib. The listener needs ant-apache-log4j.jar (shipped in lib) and the Log4j classes, and without them
the start fails with a message that does not name the missing jar. This was printed on JDK 26 with no Log4j on the
classpath; it was the only part of the listener that could be run here:
$ ant -listener org.apache.tools.ant.listener.Log4jListener all
Buildfile: <scratch>/build.xml
BUILD FAILED
Could not instantiate org.apache.tools.ant.listener.Log4jListener. Specified class should have a public constructor.
Total time: 0 seconds
A build with the Log4j 2 bridge was not run here. New work should write a listener of its own (next section) that delegates to whichever logging library the organisation uses, as the manual itself suggests.
Source: Apache Ant manual: Log4jListener
Writing a listener
A listener is a plain Java class with a public no-argument constructor that implements BuildListener. The one below
counts events and prints a summary when the build ends. It implements SubBuildListener to count the sub-builds, uses
thread-safe counters because Ant may call every method except the build-started and build-finished ones from several
threads at once (the parallel task), and never prints from messageLogged.
package io.tidepool.ant;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.SubBuildListener;
/** A listener (not a logger): it counts build events and prints a summary when the build ends. */
public class EventCounterListener implements SubBuildListener {
private final AtomicInteger targets = new AtomicInteger();
private final AtomicInteger tasks = new AtomicInteger();
private final AtomicInteger subBuilds = new AtomicInteger();
private final Map<Integer, AtomicInteger> messages = new ConcurrentSkipListMap<>();
private final Map<String, AtomicInteger> perTask = new ConcurrentSkipListMap<>();
@Override public void buildStarted(BuildEvent event) { }
@Override public void buildFinished(BuildEvent event) {
// buildFinished runs after Ant restored System.out, so printing here is safe
System.out.println("[counter] targets started: " + targets.get());
System.out.println("[counter] tasks started: " + tasks.get() + " " + perTask);
System.out.println("[counter] sub-builds started: " + subBuilds.get());
System.out.println("[counter] messages by level (0=error 1=warn 2=info 3=verbose 4=debug): " + messages);
Throwable failure = event.getException();
System.out.println("[counter] outcome: " + (failure == null ? "success" : "failed: " + failure.getMessage()));
}
@Override public void subBuildStarted(BuildEvent event) { subBuilds.incrementAndGet(); }
@Override public void subBuildFinished(BuildEvent event) { }
@Override public void targetStarted(BuildEvent event) { targets.incrementAndGet(); }
@Override public void targetFinished(BuildEvent event) { }
@Override public void taskStarted(BuildEvent event) {
tasks.incrementAndGet();
perTask.computeIfAbsent(event.getTask().getTaskName(), k -> new AtomicInteger()).incrementAndGet();
}
@Override public void taskFinished(BuildEvent event) { }
@Override public void messageLogged(BuildEvent event) {
// never print here: Ant captures System.out and System.err and would call this method again
messages.computeIfAbsent(event.getPriority(), k -> new AtomicInteger()).incrementAndGet();
}
}
The only compile-time dependency is ant.jar from the Ant installation; Ant itself is not needed in the build of the
listener, and no Maven or Gradle project is needed for a class this small:
$ javac -cp $ANT_HOME/lib/ant.jar -d build/classes src/io/tidepool/ant/*.java
$ ant -lib build/classes -listener io.tidepool.ant.EventCounterListener all
Buildfile: <scratch>/build.xml
prepare:
[mkdir] Created dir: <scratch>/build/out
[echo] preparing
copy-data:
[copy] Copying 1 file to <scratch>/build/out
[echo] check the stations file
quiet-target:
all:
BUILD SUCCESSFUL
Total time: 0 seconds
[counter] targets started: 4
[counter] tasks started: 6 {copy=1, echo=4, mkdir=1}
[counter] sub-builds started: 0
[counter] messages by level (0=error 1=warn 2=info 3=verbose 4=debug): {1=2, 2=2, 3=11, 4=27}
[counter] outcome: success
The listener sits next to the normal logger: the console output is untouched, and the summary follows BUILD SUCCESSFUL
because the listeners receive each event after the logger, in registration order. The message counts show why a listener must filter
by level itself: it received 27 debug and 11 verbose messages although the run was a plain one, and only two warnings
(the echo tasks) and two info messages (mkdir and copy) were visible. A failing build reports the exception, and
sub-builds are counted by the extension interface (the nested target runs the build file through ant and subant):
$ ant -lib build/classes -listener io.tidepool.ant.EventCounterListener fail
...
BUILD FAILED
<scratch>/build.xml:18: stations file is stale
Total time: 0 seconds
[counter] targets started: 1
[counter] tasks started: 1 {fail=1}
[counter] sub-builds started: 0
[counter] messages by level (0=error 1=warn 2=info 3=verbose 4=debug): {3=7, 4=26}
[counter] outcome: failed: stations file is stale
$ ant -lib build/classes -listener io.tidepool.ant.EventCounterListener nested
...
[counter] targets started: 3
[counter] tasks started: 2 {ant=1, subant=1}
[counter] sub-builds started: 2
...
targets started: 3 is the nested target plus the quiet-target of each of the two sub-builds; the ant and subant
tasks are the two tasks started in the parent project, and each of them started one sub-build.
The rules that the manual states, and that the run above respects, are:
-
A listener or logger must not write to
System.outorSystem.errfrommessageLogged: Ant redirects both streams to the event system while a task runs. Since Ant 1.6.3 the message is silently ignored; a test listener that printed frommessageLoggednever showed its lines on Ant 1.10.18, so the failure mode today is silence rather than a stack overflow. Older versions threw an exception. A logger must use the streams it was handed (outanderrinDefaultLogger). -
When
buildStartedis called the project is not usable: it has not parsed the build file,getPropertyreturns nothing andgetNamereturnsnull, which is exactly whyBigProjectLoggerprinted an empty name for the top-level project. -
All methods except
buildStartedandbuildFinishedmay be called concurrently. Do not keep unsynchronised state. -
A slow listener slows the build, because the calls are synchronous.
Writing a logger
A logger that only changes the layout should extend DefaultLogger, which already implements every method and keeps
the streams (out, err), the level and the emacs flag in protected fields; override only what differs. The logger below
drops the target banner, prints one line when the target finishes, with the elapsed time, and leaves everything else to
the default behaviour:
package io.tidepool.ant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
/** A logger: it extends DefaultLogger, drops the target banner and adds the duration of each target. */
public class TimingLogger extends DefaultLogger {
private final Map<String, Long> started = new ConcurrentHashMap<>();
@Override public void targetStarted(BuildEvent event) {
started.put(event.getTarget().getName(), System.nanoTime());
}
@Override public void targetFinished(BuildEvent event) {
String name = event.getTarget().getName();
Long t0 = started.remove(name);
long ms = t0 == null ? -1 : (System.nanoTime() - t0) / 1_000_000;
// out is the PrintStream Ant gives the logger (the console, or the -logfile file)
printMessage("== " + name + (ms >= 0 ? " took " + ms + " ms" : ""), out, Project.MSG_INFO);
}
}
$ ant -lib build/classes -logger io.tidepool.ant.TimingLogger all
Buildfile: <scratch>/build.xml
[mkdir] Created dir: <scratch>/build/out
[echo] preparing
== prepare took 54 ms
[copy] Copying 1 file to <scratch>/build/out
[echo] check the stations file
== copy-data took 13 ms
== quiet-target took 0 ms
== all took 0 ms
BUILD SUCCESSFUL
Total time: 0 seconds
Because it extends DefaultLogger, the new logger honours the other switches without extra code. With -q and -logfile
the verbose messages disappear and the whole output, including the summary, goes to the file:
$ ant -lib build/classes -logger io.tidepool.ant.TimingLogger -q -logfile build/t.log all
$ cat build/t.log
[echo] preparing
== prepare took 55 ms
[echo] check the stations file
== copy-data took 22 ms
== quiet-target took 0 ms
== all took 0 ms
BUILD SUCCESSFUL
Total time: 0 seconds
A logger written from scratch (implementing BuildLogger directly) must honour the level (setMessageOutputLevel), the
streams and the emacs mode itself, or -q and -logfile stop working with it. Subclassing DefaultLogger is almost
always the right choice.
Installing a custom listener or logger
Ant has to find the class before the build file is read, so the class goes on Ant’s own classpath. In order of increasing permanence:
-
-lib build/classes(or-libpointing at a jar) on the command line. This is what the runs on this page did, and a jar works the same:jar --create --file build/tidepool-ant.jar -C build/classes .followed byant -lib build/tidepool-ant.jar -listener io.tidepool.ant.EventCounterListener quiet-targetprinted the same counters. -
ANT_ARGS, set per environment, containing the same switches, so the build file and the command do not change. -
~/.ant/lib/*.jar(a user-wide library directory that Ant adds automatically unless-nouserlibis given) orANT_HOME/lib, for a listener every build of a machine should use.
A listener or logger packaged with tasks and types in an antlib is covered in Antlibs & Namespaces; the tasks and types that such a jar usually also contains are in Writing Custom Tasks and Custom Types, Conditions & Selectors. The same event API can be used from a Java program that embeds Ant, see Embedding Ant.
Input handlers
The input task prompts the user and optionally stores the answer in a property. It does not read the console itself,
because that would make Ant unusable inside an IDE or a CI server. It builds an InputRequest (the prompt, an optional default
value and, with validargs, a list of acceptable answers) and passes it to the single InputHandler of the Ant process.
The handler’s contract is one method, handleInput(InputRequest), with the rule that it must not return until the request
considers the answer valid; it may throw a BuildException instead.
Attribute of input |
Meaning |
|---|---|
|
the prompt (alternatively the character data of the element) |
|
comma-separated list of accepted answers; the comparison is case sensitive, so |
|
the property that receives the answer; it behaves like |
|
the value used if no input is received |
That "does not prompt when the property is set" rule is the standard way to make a build run unattended without a
special handler: ant -Dpublish=y ask never asks.
The distribution contains four handlers, selected with -inputhandler <class> for the whole run or with a nested
handler element in a single input task. The nested element takes type (default, propertyfile, greedy and, since Ant
1.8, secure), or refid, or classname with classpath, classpathref or loaderref.
| Handler | Behaviour |
|---|---|
|
prints the prompt to the log and reads a line from the console, repeating until the answer is valid |
|
reads answers from the properties file named by the Java system property |
|
reads all available input from standard input (since Ant 1.7), for pipes |
|
calls |
The ask target of the example build asks one question with validargs="y,n":
<target name="ask">
<input message="Publish the tide tables (y/n)?" validargs="y,n" addproperty="publish"/>
<echo message="publish=${publish}"/>
</target>
With the default handler and an answer on standard input, the question is printed with the accepted answers appended:
$ echo y | ant ask
Buildfile: <scratch>/build.xml
ask:
[input] Publish the tide tables (y/n)? (y, n)
[echo] publish=y
BUILD SUCCESSFUL
Total time: 0 seconds
Source: Apache Ant manual: InputHandler and Input
Unattended builds: -noinput and the property file handler
-noinput does not make the question go away; it makes it unanswerable. Even a piped answer is ignored, and the build
fails at the input task, which is the desired behaviour for a CI job that must never block:
$ echo y | ant -noinput ask
Buildfile: <scratch>/build.xml
ask:
[input] Publish the tide tables (y/n)? (y, n)
BUILD FAILED
<scratch>/build.xml:15: Failed to read input from Console.
PropertyFileInputHandler answers from a file and is the officially supported route for unattended builds that do have to
answer. Two traps were hit while preparing this page and both are real. The file is named by a Java system property, so
it is passed through ANT_OPTS, not with -D after ant (which creates an Ant property that the handler never reads).
And the key is the full prompt text, so the spaces in it must be escaped in the properties file, or the key ends at the
first space and nothing matches:
$ printf 'Publish the tide tables (y/n)?=y\n' > answers.properties
$ ANT_OPTS="-Dant.input.properties=answers.properties" ant -inputhandler org.apache.tools.ant.input.PropertyFileInputHandler ask
...
BUILD FAILED
<scratch>/build.xml:15: Unable to find input for 'Publish the tide tables (y/n)?'
$ cat answers.properties
Publish\ the\ tide\ tables\ (y/n)?=y
$ ANT_OPTS="-Dant.input.properties=answers.properties" ant -inputhandler org.apache.tools.ant.input.PropertyFileInputHandler ask
ask:
[echo] publish=y
BUILD SUCCESSFUL
A prompt is a poor key because any edit of the message changes it. For unattended builds the more robust pattern is a
property with a default (-Dpublish=n), and a handler for the questions that remain.
Greedy and secure handlers
GreedyInputHandler reads everything that is available on standard input as the answer. With two lines on the pipe the
answer is y plus the newline plus extra, which validargs rejects:
$ printf 'y\nextra\n' | ant -inputhandler org.apache.tools.ant.input.GreedyInputHandler ask
...
BUILD FAILED
<scratch>/build.xml:15: Received invalid console input
SecureInputHandler needs a real terminal. Without one, which is every CI job, Java returns no console and Ant 1.10.18
fails with a stack trace instead of a message; this is the first line of the failure, with the frames omitted:
$ echo y | ant -inputhandler org.apache.tools.ant.input.SecureInputHandler ask
...
BUILD FAILED
<scratch>/build.xml:15: java.lang.NullPointerException: Cannot invoke "java.io.Console.readPassword(String, Object[])" because the return value of "java.lang.System.console()" is null
at org.apache.tools.ant.input.SecureInputHandler.handleInput(SecureInputHandler.java:45)
...
Typing a password into an input therefore belongs to interactive runs only. The documentation recommends, for IDEs that
cannot provide a console, to keep a password in a protected property file and load it before the task.
Writing an input handler
An input handler is a class with a public no-argument constructor that implements InputHandler. The one below is for
unattended runs: it answers every question from the environment variable TIDEPOOL_ANSWER, falls back to the request’s
default value, logs what it did and rejects an invalid answer by throwing, as the contract requires:
package io.tidepool.ant;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.input.InputHandler;
import org.apache.tools.ant.input.InputRequest;
/** An input handler for unattended builds: it answers every prompt from the environment variable TIDEPOOL_ANSWER. */
public class AnswerInputHandler implements InputHandler {
@Override public void handleInput(InputRequest request) throws BuildException {
String answer = System.getenv("TIDEPOOL_ANSWER");
if (answer == null || answer.isEmpty()) {
answer = request.getDefaultValue();
}
request.setInput(answer);
System.out.println("[answer] \"" + request.getPrompt() + "\" -> " + answer);
if (!request.isInputValid()) {
throw new BuildException("Invalid answer '" + answer + "' for prompt: " + request.getPrompt());
}
}
}
$ TIDEPOOL_ANSWER=y ant -lib build/classes -inputhandler io.tidepool.ant.AnswerInputHandler ask
ask:
[input] [answer] "Publish the tide tables (y/n)?" -> y
[echo] publish=y
BUILD SUCCESSFUL
$ TIDEPOOL_ANSWER=maybe ant -lib build/classes -inputhandler io.tidepool.ant.AnswerInputHandler ask
ask:
[input] [answer] "Publish the tide tables (y/n)?" -> maybe
BUILD FAILED
<scratch>/build.xml:15: Invalid answer 'maybe' for prompt: Publish the tide tables (y/n)?
The validation is done by the request object: isInputValid() is stricter for a MultipleChoiceInputRequest, the class
the input task creates when validargs is set. Different handlers can also be used for different questions in the same
build, with a nested handler. In the target below the first question uses the custom class (found with a classpath
here, because the nested element, unlike the -inputhandler switch, is read after the build file is parsed) and the
second the property file handler:
<target name="ask-nested">
<input message="Publish the tide tables (y/n)?" validargs="y,n" addproperty="publish">
<handler classname="io.tidepool.ant.AnswerInputHandler" classpath="build/classes"/>
</input>
<input message="Region" addproperty="region" defaultvalue="north-sea">
<handler type="propertyfile"/>
</input>
<echo message="publish=${publish} region=${region}"/>
</target>
$ TIDEPOOL_ANSWER=n ANT_OPTS="-Dant.input.properties=answers.properties" ant ask-nested
ask-nested:
[input] [answer] "Publish the tide tables (y/n)?" -> n
BUILD FAILED
<scratch>/build.xml:22: Unable to find input for 'Region'
$ printf 'Region=baltic\n' >> answers.properties
$ TIDEPOOL_ANSWER=y ANT_OPTS="-Dant.input.properties=answers.properties" ant ask-nested
ask-nested:
[input] [answer] "Publish the tide tables (y/n)?" -> y
[echo] publish=y region=baltic
BUILD SUCCESSFUL
The first run shows a detail the manual does not stress: defaultvalue="north-sea" did not rescue the second question,
because PropertyFileInputHandler fails the request when the key is missing, before the task gets the chance to use its
default. Whether a default value is honoured is therefore a property of the handler, not of the input task.
The same ideas in Maven and Gradle
| Need | Maven | Gradle |
|---|---|---|
the default console log |
the console logger of Maven, controlled by |
the rich console, controlled by |
a log file |
|
shell redirection; the console option |
|
|
|
a custom listener |
an |
the listener interfaces ( |
the per-build XML report |
the Surefire and Failsafe reports for tests only; build-level XML comes from an extension |
build scans (Develocity) and the HTML test and build reports |
asking the user |
none in the core; the |
none in the core; a task or plugin reads the console itself, and non-interactive runs pass values as project properties ( |
The practical difference is that Ant’s event consumers are chosen on the command line and need no build file change, which is rarely true of the other two tools: the Maven and Gradle equivalents are configured in the build or in an extension. The comparison of the three tools is in Comparing Ant, Maven & Gradle, and the CI side in CI & IDE Integration.
Classic mistakes
-
A listener passed as the logger, or two loggers. The first fails with "Class of unexpected Type", the second with "Only one logger class may be specified". Use
-listenerfor additional consumers. -
The custom class cannot be found.
-logger,-listenerand-inputhandlerare resolved before the build file is read, so aclasspathin the build file cannot help; add-lib,ANT_ARGSor~/.ant/lib. -
Printing from
messageLogged. The output is silently lost (since Ant 1.6.3; the manual still warns about an infinite loop). -
ant.input.propertiesas an Ant property. It is a Java system property: put it inANT_OPTS. The prompt is the key, and its spaces need a backslash in the properties file. -
Case in
validargs.Yis noty. List both if both should work. -
Expecting
-noinputto answer. It makes every prompt fail; pass the answers as properties or use a handler. -
Colour in a CI log.
AnsiColorLoggerwrites the escape codes to the log file as well. Keep it for terminals. -
Expecting the mail of
MailLoggerfrom a killed build. Mail is sent when the build-finished event arrives. -
The
XmlLoggerlistener and thelog.xmlfile. It is written to the current directory of the process (setXmlLogger.file), so two concurrent builds in one directory overwrite each other.