Compiling Java with Ant

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

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

This page is written against Apache Ant 1.10.18. Every example was run on JDK 26 on 2026-10-16 with the Ant 1.10.18 distribution unpacked in a scratch directory and the ant launch script on the PATH; the sources compile with release="21", so the class files they produce target Java 21 even though the compiler is JDK 26. The sources are the tidepool scenario used across this section: tidepool-model (records), tidepool-engine (reads data/stations.csv with Commons CSV) and tidepool-cli (a picocli command, which exercises annotation processing). Paths in console output were shortened to /work/.... Ant does not download libraries, so the Commons CSV, Commons IO, Commons Codec and picocli JARs sit in a lib/ directory; the page on Ivy Overview and the page on Maven Resolver Ant Tasks show how to fetch them from a repository instead.

Compiling is the first thing every Ant build does after cleaning, and the <javac> task is the most used task in the tool. The page covers it in the order a build grows: a first compile, which Java release to compile for, which files Ant decides to compile, the class path of a multi-module project, compiler selection and forking, warnings and failures, annotation processors, modules, native headers, the dependency-checking <depend> task, the tasks that were removed from the JDK (javah, rmic), native2ascii and JSP precompilation. The build files use targets, properties and paths that are explained on Build File Structure, Properties and Paths, Filesets & Patterns.

A first compile

The smallest useful build file compiles one module. It sets every attribute that should never be left to a default:

build.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool-model" default="compile" basedir=".">
  <property name="src.dir" location="tidepool-model/src/main/java"/>
  <property name="classes.dir" location="build/model/classes"/>

  <target name="compile" description="Compile the model library">
    <mkdir dir="${classes.dir}"/>
    <javac srcdir="${src.dir}"
           destdir="${classes.dir}"
           release="21"
           encoding="UTF-8"
           debug="true"
           debuglevel="lines,vars,source"
           includeantruntime="false"
           listfiles="true"/>
  </target>

  <target name="clean">
    <delete dir="build"/>
  </target>
</project>

Running ant compile twice shows the first idea of Ant compilation: the first run compiles the four files and lists them (because of listfiles), the second finds the class files newer than the sources and does nothing.

$ ant compile
Buildfile: /work/ex1/build.xml

compile:
    [mkdir] Created dir: /work/ex1/build/model/classes
    [javac] Compiling 4 source files to /work/ex1/build/model/classes
    [javac] /work/ex1/tidepool-model/src/main/java/io/tidepool/model/Constituent.java
    [javac] /work/ex1/tidepool-model/src/main/java/io/tidepool/model/Height.java
    [javac] /work/ex1/tidepool-model/src/main/java/io/tidepool/model/Station.java
    [javac] /work/ex1/tidepool-model/src/main/java/io/tidepool/model/TideEvent.java

BUILD SUCCESSFUL
Total time: 0 seconds
$ ant compile
Buildfile: /work/ex1/build.xml

compile:

BUILD SUCCESSFUL
Total time: 0 seconds

javap confirms what release and debug did to the class files: major version 65 is Java 21, and the SourceFile and LineNumberTable attributes are present.

$ javap -v -cp build/model/classes io.tidepool.model.Height | grep -E "major|SourceFile"
  major version: 65
SourceFile: "Height.java"

What Ant decides to compile

<javac> is a directory-based task. It scans the source directory (the srcdir attribute or one or more nested <src> elements), applies the includes, excludes and default excludes, and maps every X.java to destdir/X.class in the same package directory. A source file whose class file is missing, or older than the source, goes on the list; everything else is skipped. The list is then given to one javac invocation (in the same JVM unless fork is set), and javac may compile further classes it finds through the source path, which Ant sets to the source directories by default.

flowchart TD S["srcdir or nested src elements, with includes and excludes"] --> M["map each X.java to destdir/X.class"] M --> Q{"class file missing or older than the source?"} Q -- no --> SK["skipped: up to date"] Q -- yes --> L["list of files to compile"] L --> J["one javac run, sourcepath = srcdir"] J --> E["javac also compiles sources it finds through the sourcepath"] E --> O["class files written to destdir"]

The consequences are the same in every build:

  • Only names and timestamps are compared. Ant does not parse the sources, so it knows nothing about nested classes, classes whose name differs from the file name, or what a changed class means for its callers. That is the job of the <depend> task, covered below.

  • The source directory must be the root of the package tree. The manual warns that putting part of the package structure in srcdir makes Ant recompile on every run, because the class file is looked up at the wrong path. The experiment below points srcdir one level too deep and then, for contrast, restricts the right directory with includes; the first build file compiles four files on each of two consecutive runs, the second compiles them once.

  • Compile only part of the tree with includes and excludes, never with a longer srcdir.

<javac srcdir="tidepool-model/src/main/java/io/tidepool" destdir="build/classes"
       release="21" includeantruntime="false"/>
$ ant compile
    [javac] Compiling 4 source files to /work/ex5/build/classes
$ ant compile
    [javac] Compiling 4 source files to /work/ex5/build/classes
<javac srcdir="tidepool-model/src/main/java" destdir="build/classes"
       release="21" includeantruntime="false" includes="io/tidepool/model/**"/>
$ ant compile
    [javac] Compiling 4 source files to /work/ex5/build/classes
$ ant compile
(no javac line: every class file is up to date)

The javac task, attribute by attribute

The tables list every attribute of the 1.10.18 <javac> task; the "since" column names the release only where the manual gives one. Attributes that take a path (classpath, sourcepath, bootclasspath, extdirs, modulepath, modulesourcepath, upgrademodulepath and srcdir) can also be written as nested <classpath>, <sourcepath>, <bootclasspath>, <extdirs>, <modulepath>, <modulesourcepath>, <upgrademodulepath> and <src> elements (note that srcdir becomes <src>), and each has a ...ref variant that takes the id of a path defined elsewhere.

Table 1. Source files and output
Attribute Meaning Default

srcdir

Location of the Java files. Required unless nested <src> elements or modulesourcepath are present.

none

destdir

Where the class files go; Ant creates the package directories below it.

none (classes next to the sources)

includes, includesfile, excludes, excludesfile, defaultexcludes

Pattern selection of the files to compile; same syntax as any directory-based task.

all .java files

sourcepath

Where javac looks for sources of classes that are not in the list. Set it to the empty string to suppress the switch and compile only the listed files.

the srcdir, unless nested <src> elements are used

encoding

Encoding of the source files.

the platform default of the JVM running Ant

Table 2. Class path and environment
Attribute Meaning Default

classpath, classpathref

The class path used to compile. Entries that point to non-existent files or directories are dropped.

none

bootclasspath, bootclasspathref, extdirs

Boot class path and extension directories; ignored when release is used on JDK 9 or later.

none

includeAntRuntime

Add the Ant libraries to the class path. Set it to false to keep the build independent of the Ant installation it runs on.

yes, unless build.sysclasspath is set

includeJavaRuntime

Add the default run-time libraries of the executing JVM to the class path.

no

includeDestClasses

Put the destination directory on the class path (since Ant 1.7.1). Set it to false for the edge cases around generics in the bug report cited by the manual; the compiler then recompiles dependent classes.

true

Table 3. Java release and language level
Attribute Meaning Default

release

The value of the --release switch (since Ant 1.9.8); on JDK 9 or later it makes Ant ignore source, target and bootclasspath.

none; ignored on JDK 8 or earlier

source

The -source switch: which language features the compiler accepts.

none; the compiler’s own default, which changes with the JDK

target

The -target switch: the class file version to generate.

none; depends on the JDK and on source

Table 4. Diagnostics and debug information
Attribute Meaning Default

debug

Compile with debug information. Off passes -g:none; on lets debuglevel decide.

off

debuglevel

The keywords appended to -g: none or a comma-separated list of lines, vars and source.

nothing appended

deprecation, nowarn, verbose

Pass -deprecation, -nowarn and -verbose.

off, off, no

optimize

Ignored by modern javac (optimization has been the job of the JIT compiler since JDK 1.3).

off

listfiles

List the source files to be compiled.

no

failonerror

Fail the build on a compile error.

true

errorProperty, updatedProperty

Properties set to true when compilation failed, and when compilation took place and succeeded (both since Ant 1.7.1).

unset

depend

Dependency tracking for the jikes and classic compilers only; it does nothing for the modern compiler (use the <depend> task).

unset

createMissingPackageInfoClass

Since Ant 1.8 Ant creates an empty package-info.class for every package-info.java for which the compiler created none, so that the same file is not recompiled forever; set it to false if that class causes trouble (since Ant 1.8.3).

true

Table 5. Compiler selection and forking
Attribute Meaning Default

compiler

The compiler implementation; the values are in the next section.

the build.compiler property, or the compiler matching the JDK running Ant

fork

Run javac as an external process. It overrides the build.compiler property and the compiler attribute.

no

executable

Full path of the javac to run when fork is set; also used for jikes, jvc, gcj and sj.

the compiler of the current JDK

memoryInitialSize, memoryMaximumSize

Memory settings (80m, 81920k or a byte count) of the forked JVM.

the JVM defaults

tempdir

Where Ant puts temporary files when the forked command line exceeds 4 kB.

java.io.tmpdir

Table 6. Modules and native headers
Attribute Meaning Since

modulepath, modulepathref

Where to find the application modules: directories of modules, module files or exploded modules.

Ant 1.9.7

modulesourcepath, modulesourcepathref

Input sources for a multiple-module compilation; replaces srcdir.

Ant 1.9.7

upgrademodulepath, upgrademodulepathref

Modules that replace upgradeable modules in the runtime image.

Ant 1.9.7

nativeheaderdir

Where javac -h writes the JNI headers; ignored on JDK 7 or earlier.

Ant 1.9.8

The task also accepts nested <include>, <exclude> and <patternset> elements, nested <compilerarg> elements for extra compiler switches, <compilerclasspath> for loading a custom compiler adapter, and a nested element of any defined type that implements CompilerAdapter. There is no proc, parameters or lint attribute: those switches go through <compilerarg>, as the sections below show.

release versus source and target

source and target are the historical pair of switches, and they have a trap: the compiler of the JDK that runs Ant compiles against its own API, not against the API of the release named. A class compiled with source="21" target="21" on JDK 26 can call a method that exists only in JDK 26 and fail at run time on Java 21. release closes the trap because javac --release also restricts the compiled API to that of the chosen release. javac says so itself when the old pair is used:

build.xml (fragment)
<javac srcdir="tidepool-model/src/main/java" destdir="build/classes"
       source="21" target="21" includeantruntime="false"/>
    [javac] Compiling 4 source files to /work/ex2/build/classes
    [javac] warning: [options] location of system modules is not set in conjunction with -source 21
    [javac]   not setting the location of system modules may lead to class files that cannot run on JDK 21
    [javac]     --release 21 is recommended instead of -source 21 -target 21 because it sets the location of system modules automatically
    [javac] 1 warning

With release set, source and target are not used at all, and Ant reports it:

<javac srcdir="tidepool-model/src/main/java" destdir="build/classes"
       release="21" source="11" target="11" includeantruntime="false"/>
    [javac] Compiling 4 source files to /work/ex2/build/classes
    [javac] Ignoring source, target and bootclasspath as release has been set

The JDK itself decides which releases --release accepts. JDK 26 no longer supports 7, and 8 still works but is obsolete; compiling the records of tidepool for 8 also fails because records need Java 16:

    [javac] error: release version 7 not supported
    [javac] Usage: javac <options> <source files>
    [javac] use --help for a list of possible options
    [javac] warning: [options] source value 8 is obsolete and will be removed in a future release
    [javac] warning: [options] target value 8 is obsolete and will be removed in a future release
    [javac] warning: [options] To suppress warnings about obsolete options, use -Xlint:-options.
    [javac] /work/ex2/tidepool-model/src/main/java/io/tidepool/model/Constituent.java:4: error: records are not supported in -source 8
    [javac] public record Constituent(String name, double amplitude, double phase) {
    [javac]        ^
    [javac]   (use -source 16 or higher to enable records)

Some build files cannot be edited, for example those inherited from an old project. For them Ant offers two properties that supply defaults for the source and target attributes when the attributes are not set (the properties are ignored when the attributes are present): ant.build.javac.source and ant.build.javac.target, both since Ant 1.7. They are passed with -D or set in a properties file, and they do not fix the trap above, because they feed -source and -target, not --release.

$ ant compile -Dant.build.javac.source=21 -Dant.build.javac.target=21
    [javac] Compiling 1 source file to /work/ex10/build/classes
    [javac] warning: [options] location of system modules is not set in conjunction with -source 21
    ...

release needs Ant 1.9.8 or later and JDK 9 or later to run. In a Maven build the same decision is maven.compiler.release (Compiling & Toolchains); in Gradle it is options.release or a toolchain (Java Projects, Toolchains).

includeantruntime

By default <javac> puts the Ant libraries on the class path of the compilation, so that a build file that compiles custom tasks does not need to name ant.jar. It also makes the result depend on which Ant is installed. Ant 1.10.18 warns when the attribute is left out:

<javac srcdir="tidepool-model/src/main/java" destdir="build/classes"/>
    [javac] /work/ex2/build.xml:4: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds
    [javac] Compiling 4 source files to /work/ex2/build/classes

Set includeantruntime="false" on every <javac>. The one case that needs true is a task that compiles a class that extends Ant classes, for example a custom task such as the <checkstations> task of Writing Custom Tasks; there, name the Ant jar on the class path instead (${ant.core.lib} is the built-in property that points at it). A build file can also set the build.sysclasspath property, which decides where the Ant run-time class path goes: last, the default the warning names, appends it after the build’s own class path, first puts it before, only uses it alone, and ignore leaves it out.

A multi-module compile: class paths between modules

tidepool has three modules, and each depends on the previous one. Ant has no project model, so the dependency between modules is a class path pointing at the previous module’s output directory, and the order is the order of the targets' depends. The same file shows the nested <compilerarg> and a processor path for the picocli annotation processor (explained in the next sections):

build.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool" default="compile" basedir=".">
  <property name="java.release" value="21"/>
  <property name="build.dir" location="build"/>

  <path id="processor.path">
    <fileset dir="lib" includes="picocli-4.7.7.jar, picocli-codegen-4.7.7.jar"/>
  </path>

  <target name="compile-model">
    <mkdir dir="${build.dir}/model/classes"/>
    <javac srcdir="tidepool-model/src/main/java"
           destdir="${build.dir}/model/classes"
           release="${java.release}" encoding="UTF-8" debug="true"
           includeantruntime="false"/>
  </target>

  <path id="engine.classpath">
    <pathelement location="${build.dir}/model/classes"/>
    <pathelement location="lib/commons-csv-1.14.1.jar"/>
    <pathelement location="lib/commons-io-2.20.0.jar"/>
    <pathelement location="lib/commons-codec-1.19.0.jar"/>
  </path>

  <target name="compile-engine" depends="compile-model">
    <mkdir dir="${build.dir}/engine/classes"/>
    <javac srcdir="tidepool-engine/src/main/java"
           destdir="${build.dir}/engine/classes"
           classpathref="engine.classpath"
           release="${java.release}" encoding="UTF-8" debug="true"
           includeantruntime="false">
      <compilerarg value="-Xlint:all"/>
    </javac>
  </target>

  <path id="cli.classpath">
    <path refid="engine.classpath"/>
    <pathelement location="${build.dir}/engine/classes"/>
    <pathelement location="lib/picocli-4.7.7.jar"/>
  </path>

  <target name="compile-cli" depends="compile-engine">
    <mkdir dir="${build.dir}/cli/classes"/>
    <javac srcdir="tidepool-cli/src/main/java"
           destdir="${build.dir}/cli/classes"
           classpathref="cli.classpath"
           release="${java.release}" encoding="UTF-8" debug="true"
           includeantruntime="false">
      <compilerarg value="-processorpath"/>
      <compilerarg pathref="processor.path"/>
      <compilerarg value="-Aproject=io.tidepool/tidepool-cli"/>
    </javac>
  </target>

  <target name="compile" depends="compile-cli"/>
</project>
$ ant
Buildfile: /work/tidepool/build.xml

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

compile-engine:
    [mkdir] Created dir: /work/tidepool/build/engine/classes
    [javac] Compiling 2 source files to /work/tidepool/build/engine/classes

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

compile:

BUILD SUCCESSFUL
Total time: 0 seconds
$ ant
Buildfile: /work/tidepool/build.xml

compile-model:

compile-engine:

compile-cli:

compile:

BUILD SUCCESSFUL
Total time: 0 seconds

Three things in this build file are classic sources of bugs:

  • Missing transitive libraries. Commons CSV needs Commons IO and Commons Codec at run time. Compiling does not need them, so javac is happy with commons-csv-1.14.1.jar alone, and the build breaks only when the program runs, with NoClassDefFoundError: org/apache/commons/io/output/AppendableOutputStream. Ant does not read the POM of a dependency; a tool that does (Ivy, or the Maven Resolver Ant Tasks) is the answer, and the Maven and Gradle equivalents resolve transitive dependencies themselves (Dependency Management Fundamentals).

  • Output directories on the class path. ${build.dir}/model/classes is a directory, not a JAR, and it exists only after the compile-model target ran. Ant silently drops non-existent entries from the class path of <javac>, so a build file with a wrong directory name compiles with a shorter class path and fails with package io.tidepool.model does not exist (and package org.apache.commons.csv does not exist, if a JAR is missing too) instead of a message about the path.

  • One lib/ directory for everything. A fileset over lib/*.jar puts both compile-time and run-time libraries, and processors, on every class path. Separate paths by role (compile, processor, test, run) as the example does; Maven scopes and Gradle configurations give the same separation by name.

Choosing the compiler: the compiler attribute and build.compiler

The compiler attribute (or the build.compiler property, which affects every <javac> in the build) selects the implementation of the CompilerAdapter interface that builds the command line. The values the manual lists:

Value Compiler

classic, javac1.1, javac1.2

The compiler of JDK 1.1 and 1.2.

modern

The standard compiler of JDK 1.3 and later. The aliases javac1.3 to javac1.7 (1.7 since Ant 1.8.2), javac1.8 (since Ant 1.8.3), javac1.9 (since Ant 1.9.5), javac9 (since Ant 1.9.8) and javac10+ (since Ant 1.10.2) choose the command-line switches for that JDK generation.

jikes

The Jikes compiler (the Jikes notes in the manual describe extra properties).

jvc, microsoft

The command-line compiler of Microsoft’s SDK for Java / Visual J++.

kjc

The Kopi compiler.

gcj

The compiler from GCC.

sj, symantec

The Symantec Java compiler.

extJavac

Run either the modern or the classic compiler in a JVM of its own.

a class name

A class that implements org.apache.tools.ant.taskdefs.compilers.CompilerAdapter; a typedef’d nested element works too.

With nothing set, the default is the javacX adapter that matches the JDK running Ant, which is why release="21" works on JDK 26 without a compiler attribute. The old values of the table (jikes, jvc, kjc, gcj, sj, classic) are listed for completeness; their compilers are not maintained, and no current build needs them. A value that is neither a known name nor a class fails with a message about the class:

BUILD FAILED
/work/ex5/build.xml:4: Class not found: nonsense

javac10+ and extJavac both worked on this installation; the second one starts a separate JVM, as the manual says, and the build output is the same.

The compiler attribute is also how the command-line switches are chosen when fork is set: compiler="javac1.1" with a forked JDK 1.1 javac makes Ant write -depend instead of -Xdepend. Today the useful part is the reverse, fork.

Forking and choosing a JDK

By default <javac> calls the compiler API inside the JVM that runs Ant. fork="true" starts the javac executable of the JDK instead, or the one named by executable. Forking is the way to compile with a JDK other than the one running Ant, to give the compiler more memory (memoryMaximumSize), and to avoid the file locking the manual describes for Windows (an unforked modern compiler keeps the class path JARs open, so a later delete or move of them fails). The -v flag shows the command that is built; the arguments below also show the defaults Ant chooses: the class path is the output directory, the source path is the srcdir, and -g:none appears because debug is off.

<javac srcdir="tidepool-model/src/main/java" destdir="build/classes"
       release="21" includeantruntime="false" fork="true"/>
$ ant -v compile
...
compile:
    [javac] io/tidepool/model/Constituent.java added as io/tidepool/model/Constituent.class doesn't exist.
    [javac] io/tidepool/model/Height.java added as io/tidepool/model/Height.class doesn't exist.
    [javac] io/tidepool/model/Station.java added as io/tidepool/model/Station.class doesn't exist.
    [javac] io/tidepool/model/TideEvent.java added as io/tidepool/model/TideEvent.class doesn't exist.
    [javac] Compiling 4 source files to /work/ex5/build/classes
    [javac] Using external javac compiler
    [javac] Compilation arguments:
    [javac] '-d'
    [javac] '/work/ex5/build/classes'
    [javac] '-classpath'
    [javac] '/work/ex5/build/classes'
    [javac] '-sourcepath'
    [javac] '/work/ex5/tidepool-model/src/main/java'
    [javac] '-g:none'
    [javac] '--release'
    [javac] '21'

The first four lines also explain why a file was compiled: with -v, Ant says which class file did not exist or was older. A wrong executable fails early with a short message:

BUILD FAILED
/work/ex5/build.xml:4: Error running /opt/none/bin/javac compiler

When the compile is expected to use another installed JDK, prefer a build property such as jdk.home (set from the JAVA_HOME environment variable or from a properties file) over a hard-coded path, and read the build’s JDK from ant -version and ant -diagnostics (Running Ant). In Maven and Gradle the same need is met by toolchains, which select the JDK by version and vendor instead of by path.

Warnings, lint and failures

Without deprecation, javac prints only two summary lines for a deprecated API. The deprecation attribute and the -Xlint switches give the details. The next experiments compile a class that calls new Date(126, 0, 1):

    [javac] Note: /work/ex10/src/Old.java uses or overrides a deprecated API.
    [javac] Note: Recompile with -Xlint:deprecation for details.
<javac srcdir="src" destdir="build/classes2" release="21" deprecation="true" includeantruntime="false"/>
    [javac] /work/ex10/src/Old.java:5: warning: [deprecation] Date(int,int,int) in Date has been deprecated
    [javac]     Date d = new Date(126, 0, 1);
    [javac]              ^
    [javac] 1 warning

Lint categories and "warnings are errors" go through <compilerarg>; the -Werror switch turns the build into a gate:

<javac srcdir="src" destdir="build/classes3" release="21" includeantruntime="false">
  <compilerarg value="-Xlint:all"/>
  <compilerarg value="-Werror"/>
</javac>
    [javac] /work/ex10/src/Old.java:5: warning: [deprecation] Date(int,int,int) in Date has been deprecated
    [javac]     Date d = new Date(126, 0, 1);
    [javac]              ^
    [javac] error: warnings found and -Werror specified
    [javac] 1 error
    [javac] 1 warning

BUILD FAILED
/work/ex10/build.xml:12: Compile failed; see the compiler error output for details.

A compile error fails the build with Compile failed; see the compiler error output for details., because failonerror defaults to true. Set failonerror="false" together with errorProperty when the build must go on, for example to collect all failures before reporting; updatedProperty is set only when a compile took place and succeeded, which is why it stays unset below:

<javac srcdir="broken" destdir="build/classes" release="21" includeantruntime="false"
       failonerror="false" errorProperty="compile.failed" updatedProperty="compile.updated"/>
<echo>failed=${compile.failed} updated=${compile.updated}</echo>
    [javac] Compiling 1 source file to /work/ex5/build/classes
    [javac] /work/ex5/broken/Bad.java:1: error: incompatible types: String cannot be converted to int
    [javac] public class Bad { int x = "text"; }
    [javac]                            ^
    [javac] 1 error
    [javac] Compile failed; see the compiler error output for details.
     [echo] failed=true updated=${compile.updated}

Annotation processors

Annotation processors need three decisions in an Ant build: where the processor JARs are, whether processing is switched on, and what options it receives. The <javac> task has no attribute for them; all three go through <compilerarg>.

The tidepool CLI uses picocli-codegen, a processor that writes GraalVM native-image configuration for the @Command class. The build file above passes it a processor path and the project option the processor requires:

<compilerarg value="-processorpath"/>
<compilerarg pathref="processor.path"/>
<compilerarg value="-Aproject=io.tidepool/tidepool-cli"/>

Two nested elements are needed for a path, because -processorpath and its value are separate arguments; pathref converts a <path> to the platform’s path string. After the build the processor’s output is next to the class files:

$ find build/cli -type f | sort
build/cli/classes/META-INF/native-image/picocli-generated/io.tidepool/tidepool-cli/proxy-config.json
build/cli/classes/META-INF/native-image/picocli-generated/io.tidepool/tidepool-cli/reflect-config.json
build/cli/classes/META-INF/native-image/picocli-generated/io.tidepool/tidepool-cli/resource-config.json
build/cli/classes/io/tidepool/cli/Main.class

A processor that is only on the compile class path is a trap on a recent JDK. Since JDK 23 javac no longer runs processors that it discovers on the class path unless processing is requested explicitly: the JDK 26 javac manual says that without -proc annotation processing happens only if another option configures it (such as -processorpath or -processor). The experiment compiles all seven tidepool sources with picocli-codegen on the class path and counts the files below META-INF:

Extra compilerarg Files below META-INF in the output

-Xlint:none (a harmless switch, so the processor is only on the class path)

0

-proc:full

3

-proc:none

0

Compilation succeeded in all three runs and printed nothing about the skipped processor, so the failure shows up later, as a missing generated class or a missing configuration file. The fix is either a processor path (preferred: it keeps processors off the application class path) or -proc:full. -proc:only runs processors without compiling, which is useful to generate sources into a directory with -s before a second compile. For the processor itself, picocli documents the project option and how to enable the processor.

The equivalents are annotationProcessorPaths in the Maven compiler plugin (Compiling & Toolchains) and the annotationProcessor configuration in Gradle (Java Projects); both keep processors off the compile class path by construction.

Modules: modulepath and modulesourcepath

Ant supports the Java module system from 1.9.7. There are two shapes.

A single module. A module-info.java at the root of the source directory is compiled like any other file; add modulepath when it requires modules that are not in the JDK. This build compiles tidepool-model as the module io.tidepool.model:

module-info.java
module io.tidepool.model {
    exports io.tidepool.model;
}
<javac srcdir="src" destdir="build/classes" release="21" includeantruntime="false"/>
$ ant compile
    [javac] Compiling 5 source files to /work/ex6b/build/classes
$ ls build/classes
io
module-info.class

Several modules in one go. With modulesourcepath, one <javac> compiles a set of modules from a layout of one directory per module, and writes one output directory per module under destdir. The * token in the manual’s example stands for the module name. Here io.tidepool.engine requires io.tidepool.model (transitively) and org.apache.commons.csv, and the CSV JAR goes on the modulepath:

src/io.tidepool.engine/module-info.java
module io.tidepool.engine {
    requires transitive io.tidepool.model;
    requires org.apache.commons.csv;
    exports io.tidepool.engine;
}
build.xml
<project name="tidepool-modules" default="compile" basedir=".">
  <target name="compile">
    <mkdir dir="build/mods"/>
    <javac modulesourcepath="src"
           destdir="build/mods"
           release="21"
           includeantruntime="false">
      <modulepath>
        <fileset dir="lib" includes="*.jar"/>
      </modulepath>
    </javac>
  </target>
</project>
$ ant compile
compile:
    [mkdir] Created dir: /work/ex6/build/mods
    [javac] Compiling 8 source files to /work/ex6/build/mods

BUILD SUCCESSFUL
$ find build -type f | sort
build/mods/io.tidepool.engine/io/tidepool/engine/StationReader.class
build/mods/io.tidepool.engine/io/tidepool/engine/TideEngine.class
build/mods/io.tidepool.engine/module-info.class
build/mods/io.tidepool.model/io/tidepool/model/Constituent.class
build/mods/io.tidepool.model/io/tidepool/model/Height.class
build/mods/io.tidepool.model/io/tidepool/model/Station.class
build/mods/io.tidepool.model/io/tidepool/model/TideEvent.class
build/mods/io.tidepool.model/module-info.class
$ java --module-path build/mods:lib --describe-module io.tidepool.engine
exports io.tidepool.engine
requires io.tidepool.model transitive
requires java.base mandated
requires org.apache.commons.csv

Without the modulepath, the compile fails at the requires line:

    [javac] /work/ex6/src/io.tidepool.engine/module-info.java:3: error: module not found: org.apache.commons.csv
    [javac]     requires org.apache.commons.csv;
    [javac]                                ^
    [javac] 1 error

BUILD FAILED
/work/ex6/build.xml:4: Compile failed; see the compiler error output for details.

Two further points. A JAR on the module path becomes a named module (an automatic module if it has no module-info), while the same JAR on the classpath is in the unnamed module and invisible to requires; an Ant build that mixes both must keep the paths apart. And upgrademodulepath is for the rare case of replacing an upgradeable JDK module. Packaging the compiled modules into a modular JAR or a runtime image is on Packaging Archives.

Native headers: nativeheaderdir replaces javah

A class with a native method needs a C header with the JNI function prototype. javac -h generates it while compiling, and the Ant attribute nativeheaderdir (Ant 1.9.8) passes the directory:

src/io/tidepool/jni/Harmonics.java
package io.tidepool.jni;

public class Harmonics {
    public static final int MAX_CONSTITUENTS = 37;

    public static native double predict(double[] amplitudes, double[] phases, long epochSecond);
}
<javac srcdir="src" destdir="build/classes" nativeheaderdir="build/headers"
       release="21" includeantruntime="false"/>
$ ls build/headers
io_tidepool_jni_Harmonics.h
$ cat build/headers/io_tidepool_jni_Harmonics.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class io_tidepool_jni_Harmonics */

#ifndef _Included_io_tidepool_jni_Harmonics
#define _Included_io_tidepool_jni_Harmonics
#ifdef __cplusplus
extern "C" {
#endif
#undef io_tidepool_jni_Harmonics_MAX_CONSTITUENTS
#define io_tidepool_jni_Harmonics_MAX_CONSTITUENTS 37L
/*
 * Class:     io_tidepool_jni_Harmonics
 * Method:    predict
 * Signature: ([D[DJ)D
 */
JNIEXPORT jdouble JNICALL Java_io_tidepool_jni_Harmonics_predict
  (JNIEnv *, jclass, jdoubleArray, jdoubleArray, jlong);

#ifdef __cplusplus
}
#endif
#endif

The header is written only when the class is compiled. Ant decides what to compile from class file timestamps and does not look at native headers, so a deleted header is not regenerated while the class file is newer than the source; delete the class (or the whole output directory) to force it. The header above also contains the MAX_CONSTITUENTS constant: javac -h writes the constants of the class as #define lines.

The older <javah> task generated the same headers from compiled classes. It was deprecated with Java 9 and removed from the JDK in Java 10, and Ant 1.10.2 made it fail with an explicit message rather than a missing executable:

<javah class="io.tidepool.jni.Harmonics" destdir="build/headers2">
  <classpath path="build/classes"/>
</javah>
BUILD FAILED
/work/ex7/build.javah.xml:3: javah does not exist under Java 10 and higher, use the javac task with nativeHeaderDir instead

<rmic> has the same history: rmic was deprecated in Java 13 and removed in Java 15, and the Ant task fails on Java 15 and later unless you give it an explicit executable of an older JDK:

<rmic base="build/classes" classname="io.tidepool.jni.Harmonics"/>
     [rmic] RMI Compiling 1 class to /work/ex7/build/classes

BUILD FAILED
/work/ex7/build.rmic.xml:3: rmic does not exist under Java 15 and higher, use rmic of an older JDK and explicitly set the executable attribute

Modern RMI needs no stubs (dynamic proxies replace them since Java 5), so the answer for a build is to delete the <rmic> call. The -iiop and -idl options (CORBA) fail from Java 11, because that release removed the CORBA packages; the manual states that Ant 1.10.3 and later fail on them.

Checking dependencies with the depend task

Timestamps catch a changed source but not what it means for its callers. Suppose Client calls Api.twice(int), and Api is changed to return a String. Only Api.java is newer, so a plain <javac> recompiles only that file, Client.class stays, and the build succeeds. The failure comes at run time:

src/demo/Api.java (after the change)
package demo;

public class Api {
    public static String twice(int x) {
        return "" + 2 * x;
    }
}
src/demo/Client.java
package demo;

public class Client {
    public static void main(String[] args) {
        int result = Api.twice(21);
        System.out.println("twice(21) = " + result);
    }
}
$ ant -f build.nodepend.xml compile
compile:
    [javac] Compiling 1 source file to /work/ex8/build/classes
BUILD SUCCESSFUL
$ java -cp build/classes demo.Client
Exception in thread "main" java.lang.NoSuchMethodError: 'int demo.Api.twice(int)'
	at demo.Client.main(Unknown Source)

The <depend> task reads the class references encoded in the existing class files (it does not parse the sources), builds the inverse "affected by" relation, and deletes the class files of every class that depends on a class that is out of date. The following <javac> then recompiles them, and the compiler reports the real problem. cache stores the dependency information between runs, and closure="true" follows indirect dependencies too (A depends on B depends on C):

build.xml
<project name="depend-demo" default="compile" basedir=".">
  <target name="compile">
    <mkdir dir="build/classes"/>
    <depend srcdir="src" destdir="build/classes" cache="build/depcache" closure="true"/>
    <javac srcdir="src" destdir="build/classes" release="21" includeantruntime="false"/>
  </target>
</project>
$ ant compile
compile:
   [depend] Deleted 2 out of date files in 0 seconds
    [javac] Compiling 2 source files to /work/ex8/build/classes
    [javac] /work/ex8/src/demo/Client.java:5: error: incompatible types: String cannot be converted to int
    [javac]         int result = Api.twice(21);
    [javac]                               ^
    [javac] 1 error
BUILD FAILED
/work/ex8/build.xml:5: Compile failed; see the compiler error output for details.

The attributes of <depend> are srcdir (required, a path of source directories), destdir (the class files to analyse; defaults to srcdir), cache, closure, dump (write the dependencies to the debug log), classpath (jars and classes to also check against, which should not contain JARs that never change, such as the JDK or third-party libraries) and warnOnRmiStubs. It forms an implicit fileset, so <include> and <exclude> work as in <javac>. The task is part of ant.jar.

Its limits matter. It cannot see a class relationship that the compiler removed, such as the inlined value of a public static final constant (changing DEBUG in a Constants class is not picked up by the classes that read it), it cannot relate a non-public class to a source file, and it costs time on large trees. A clean build on CI, or the incremental compilation of Maven and Gradle, is the more robust answer; Gradle tracks class-level dependencies by itself, and the Maven compiler plugin recompiles the module when it detects a change. See Build Automation Fundamentals for the general picture. The depend attribute of <javac> is not a substitute: the manual says it only works for the jikes and classic compilers.

native2ascii

Property resource bundles were ISO-8859-1 only until Java 9 (which made them UTF-8, JEP 226), so files kept in another encoding had to be converted with the JDK tool native2ascii. That tool is gone from the JDK: JDK 26 has no native2ascii executable. Ant’s task works anyway because, since Ant 1.9.8, the builtin converter is Ant’s own implementation. It converts a directory of files from a native encoding to ASCII with \uXXXX escapes (the reverse attribute converts back, supported by the sun and builtin converters):

station.name=Île de Bréhat
station.country=França
<native2ascii src="i18n" dest="build/i18n" encoding="UTF-8" includes="**/*.properties"/>
convert:
[native2ascii] Converting 1 file from /work/ex9/i18n to /work/ex9/build/i18n

BUILD SUCCESSFUL
$ cat build/i18n/messages.properties
station.name=Île de Bréhat
station.country=França

The reverse="true" run on build/i18n with the same encoding gave back the original text. The attributes are src (default basedir), dest (required), encoding, ext (rename the output files), reverse, implementation (default, sun, kaffe or builtin), the usual includes/excludes set and an optional nested <mapper>; if src and dest are the same directory, ext or a mapper is required. Use it when a legacy tool chain needs pure-ASCII properties files; for new code keep the files in UTF-8.

JSP precompilation

Compiling JSP pages is a different compile with different tools. The core Ant <jspc> task is deprecated: the manual says it was tested only against Tomcat 4.x, has known problems with Tomcat 5.x that will not be fixed, and recommends the task that Tomcat ships instead. The Jasper JspC task, its Ant integration and the production setting are in Jasper Configuration & Precompilation.

The equivalents in Maven and Gradle

Ant <javac> Maven (compiler plugin 3.16.0) Gradle 9.8.1

release="21"

maven.compiler.release or <release>

options.release = 21, or a Java toolchain

srcdir, destdir

src/main/java, target/classes (conventions)

src/main/java, build/classes/java/main (conventions)

classpathref

dependencies and their scopes

dependency configurations (implementation, api, compileOnly)

<compilerarg value="-Xlint:all"/>

<compilerArgs>

options.compilerArgs

-processorpath and -proc:full

annotationProcessorPaths

annotationProcessor configuration

modulepath, modulesourcepath

the plugin builds the module path from module-info.java and the dependencies

module path inference for a module-info.java in the source set

nativeheaderdir

-h through compilerArgs

options.headerOutputDirectory

fork, executable

fork and executable parameters, or toolchains

options.isFork and a toolchain

<depend> and timestamps

incremental detection by the plugin

incremental compilation with class-level tracking

More on the other two tools is on Compiling & Toolchains and Java Projects; the side-by-side verdict is on Comparing Ant, Maven & Gradle.

Where the book is outdated

Java Development with Ant (Ant 1.5, 2002) compiles with <javac srcdir destdir classpath> and does not mention includeantruntime, release, modulepath or nativeheaderdir, which came with Ant 1.8, 1.9.8 and 1.9.7. Its JNI chapter relies on <javah>, which fails on Java 10 and later (use nativeheaderdir); its RMI material relies on <rmic>, which fails on Java 15 and later; its source="1.4" examples fail on a current compiler, which rejects releases older than 8 (and flags 8 as obsolete); its JSP precompilation with <jspc> is Tomcat-4-only and deprecated; and the jikes compiler it recommends for speed is no longer maintained. The section’s bibliography (Bibliography) gives the publisher page of the book.

Rewriting such a build is covered on Deprecations & Migration.