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 if/unless still produces both events, as a run with the counting listener below showed)

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.

flowchart LR main["Main: parses the command line"] --> project["Project: runs targets and tasks"] project -->|"seven build events"| fanout(("fires to every registered listener")) fanout --> logger["BuildLogger: exactly one, -logger, default DefaultLogger"] fanout --> listeners["BuildListener: any number, -listener"] logger --> console["console or -logfile"] listeners --> side["files, mail, metrics, IDE view"] project -->|"input task"| handler["InputHandler: exactly one, -inputhandler"] handler --> user["console, properties file, password prompt"]

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 -logger and 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

-logger <class>

the BuildLogger that prints the build (default org.apache.tools.ant.DefaultLogger); only one

-listener <class>

adds an instance of a BuildListener as a project listener; may be repeated

-logfile <file>, -l <file>

sends the logger’s output to a file instead of the console

-emacs, -e

logs "without adornments": no [task] prefix, so editors can parse the lines

-quiet, -q

be extra quiet: only warnings and errors from tasks (message level warning)

-silent, -S

print nothing but task outputs and build failures

-verbose, -v

be extra verbose: adds the verbose level (what a task decided and why)

-debug, -d

print debugging information: adds the debug level, which includes how Ant parsed the build file and every property it set

-lib <path>

adds jars and directories to the classpath Ant uses, which is how a custom logger, listener or input handler is found

-noinput

do not allow interactive input

-inputhandler <class>

the InputHandler implementation for the input task

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

org.apache.tools.ant.DefaultLogger

logger

the logger used unless -logger says otherwise: the Buildfile: line, a banner per target, a [task] prefix per message, BUILD SUCCESSFUL/BUILD FAILED and the total time

org.apache.tools.ant.NoBannerLogger

logger

as DefaultLogger but omits the banner of a target that produced no output

org.apache.tools.ant.listener.MailLogger

logger

as DefaultLogger, and mails the log when the build finishes

org.apache.tools.ant.listener.AnsiColorLogger

logger

as DefaultLogger, with ANSI colour escape sequences per message level

org.apache.tools.ant.XmlLogger

logger

writes the build to an XML file (as a listener) or to the console or -logfile (as a logger)

org.apache.tools.ant.listener.TimestampedLogger

logger

as DefaultLogger, with the time the build finished on the last line

org.apache.tools.ant.listener.BigProjectLogger

logger

prefixes every target with its project name and brackets each sub-project with Entering/Exiting lines

org.apache.tools.ant.listener.SimpleBigProjectLogger

logger

the project-qualified target names of BigProjectLogger, otherwise as quiet as NoBannerLogger (since Ant 1.8.1)

org.apache.tools.ant.listener.ProfileLogger

logger

DefaultLogger plus start time, end time and duration of every target and task (since Ant 1.8.0)

org.apache.tools.ant.listener.Log4jListener

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.

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, -verbose and -debug.

  • As a listener (-listener org.apache.tools.ant.XmlLogger) it runs beside the normal logger and writes log.xml, or the file named by the property XmlLogger.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*&lt;td[^&gt;]*&gt;([^&lt;]*)&lt;/td&gt;.*class=&quot;([a-z]+)&quot;[^&gt;]*&gt;([^&lt;]*)&lt;/td&gt;.*$" 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.

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

MailLogger.mailhost, MailLogger.port

the SMTP server (defaults localhost and 25)

MailLogger.from, MailLogger.replyto

sender (required when mail is sent) and reply-to addresses

MailLogger.success.to, .cc, .bcc and MailLogger.failure.to, .cc, .bcc

recipients, comma-separated; mail is sent only if the matching to is set

MailLogger.success.notify, MailLogger.failure.notify

turn one kind of mail off (default true)

MailLogger.success.subject, MailLogger.failure.subject

subjects (defaults Build Success and Build Failure)

MailLogger.success.body, MailLogger.failure.body

a fixed body instead of the full log (since 1.8.0)

MailLogger.mimeType, MailLogger.charset

text/plain by default (since 1.8.0)

MailLogger.user, MailLogger.password, MailLogger.ssl

SMTP authentication and SSL; these need JavaMail (the ant-jakartamail.jar or ant-javamail.jar of the distribution plus the mail library)

MailLogger.starttls.enable, MailLogger.starttls.require, MailLogger.tls.checkserveridentity

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.

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.

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.out or System.err from messageLogged: 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 from messageLogged never 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 (out and err in DefaultLogger).

  • When buildStarted is called the project is not usable: it has not parsed the build file, getProperty returns nothing and getName returns null, which is exactly why BigProjectLogger printed an empty name for the top-level project.

  • All methods except buildStarted and buildFinished may 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:

  1. -lib build/classes (or -lib pointing 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 by ant -lib build/tidepool-ant.jar -listener io.tidepool.ant.EventCounterListener quiet-target printed the same counters.

  2. ANT_ARGS, set per environment, containing the same switches, so the build file and the command do not change.

  3. ~/.ant/lib/*.jar (a user-wide library directory that Ant adds automatically unless -nouserlib is given) or ANT_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

message

the prompt (alternatively the character data of the element)

validargs

comma-separated list of accepted answers; the comparison is case sensitive, so y,n rejects Y

addproperty

the property that receives the answer; it behaves like property, so an existing property is never overridden, and since Ant 1.6 the task does not prompt at all when that property is already set

defaultvalue

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

DefaultInputHandler

prints the prompt to the log and reads a line from the console, repeating until the answer is valid

PropertyFileInputHandler

reads answers from the properties file named by the Java system property ant.input.properties, using the prompt as the key; fails the build if the key is missing

GreedyInputHandler

reads all available input from standard input (since Ant 1.7), for pipes

SecureInputHandler

calls System.console().readPassword(), so that nothing is echoed (since Ant 1.7.1)

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

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 -q, -X, -e, --color auto|always|never and -ntp (no transfer progress)

the rich console, controlled by --console=auto|plain|rich|verbose and the log levels -q, -w, -i, -d

a log file

-l/--log-file, which also disables colour

shell redirection; the console option plain keeps the file readable

-logfile plus -emacs for a machine-read log

-B/--batch-mode, which also disables colour and never prompts

--console=plain (and --warning-mode for the deprecation warnings)

a custom listener

an ExecutionListener set on the execution request, or an EventSpy/AbstractMavenLifecycleParticipant in a core extension (see Extensions)

the listener interfaces (BuildListener, TaskExecutionListener, …​), build services and the build events registry; the logging page states that custom loggers cannot see events when the configuration cache is reused (see Directories & Logging and Worker API & Build Services)

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 -B flag disables any prompt, and plugins such as the release plugin prompt for versions

none in the core; a task or plugin reads the console itself, and non-interactive runs pass values as project properties (-P)

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 -listener for additional consumers.

  • The custom class cannot be found. -logger, -listener and -inputhandler are resolved before the build file is read, so a classpath in the build file cannot help; add -lib, ANT_ARGS or ~/.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.properties as an Ant property. It is a Java system property: put it in ANT_OPTS. The prompt is the key, and its spaces need a backslash in the properties file.

  • Case in validargs. Y is not y. List both if both should work.

  • Expecting -noinput to answer. It makes every prompt fail; pass the answers as properties or use a handler.

  • Colour in a CI log. AnsiColorLogger writes the escape codes to the log file as well. Keep it for terminals.

  • Expecting the mail of MailLogger from a killed build. Mail is sent when the build-finished event arrives.

  • The XmlLogger listener and the log.xml file. It is written to the current directory of the process (set XmlLogger.file), so two concurrent builds in one directory overwrite each other.