Gradle Ant Integration
|
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 Gradle 9.8.1 and was run on 2026-10-16 on JDK 26 (the daemon and the compiler both ran on JDK 26; no toolchain was involved). Every example was run through the wrapper, once with Kotlin DSL build files and once with Groovy DSL build files, and the console output is the same in both apart from the timings, the order of unrelated tasks and the messages that are stated below. The scratch copies pointed their wrapper at a local copy of the same distribution archive; the properties file that a real project commits is
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
The commands use --console=plain. In the listings the "Consider enabling configuration cache" hint, the link to the problems
report and the daemon start-up line are left out, and the scratch directory is shown as /work/ant-integration. Where a
listing is shortened the shortening is marked with a line of three dots on its own. The Ant comparison runs used Apache Ant
1.10.18 and Ivy 2.6.0. Gradle 8.14.x was not installed here, so statements about it come from the official documentation and
are labelled "not run here".
Gradle can use Ant in two ways, and they answer two different questions. The first is using Ant’s tasks (javac, zip,
checksum, a company’s own deploy task) from a Gradle build script, where Gradle is the build and Ant is a library of
steps. The second is importing an Ant build, where every target of a build.xml becomes a Gradle task, so that an Ant
build can be adopted by Gradle first and rewritten afterwards. Both are old features and both are still supported in 9.8.1,
but they have a cost that did not exist when they were written: neither of them works with the configuration cache that is
the preferred way to run Gradle since 9.0, with one exception that is shown below. The page ends with the migration of the
tidepool Ant and Ivy build, one verified stage at a time. The shorter version of that migration, next to the Ant to Maven
and Ivy to Resolver Ant Tasks routes, is in
Migrating Between Tools; the Maven direction is
Migrating from Maven.
Two layers of Ant
The manual splits Ant into two layers and Gradle treats them differently.
| Layer | What it contains | How Gradle reaches it | Configuration cache |
|---|---|---|---|
The Ant language |
The |
|
Not supported. Gradle turns the cache off for the whole build and says so. |
Ant tasks and types |
|
The |
Works when the task action uses the |
Ant can also be started as an external process ("ant clean compile".execute() in a Groovy script), which is the third and
worst way: Gradle cannot see the targets, so nothing is shared, up to date or parallel. The equivalent bridges in the other
two tools are maven-antrun-plugin (see Migrating Between Tools) for
Maven, and for Ant itself the Java API described in Embedding Ant,
which is what Gradle uses inside.
The Ant that is inside Gradle
Gradle does not call the ant command and does not use an installed Ant. It carries its own Ant, as libraries, and the
ant object runs the tasks in that one. The lib directory of the 9.8.1 distribution contains ant-1.10.17.jar,
ant-launcher-1.10.17.jar and ant-antlr-1.10.15.jar and no other Ant JAR, and a task prints the version it runs:
tasks.register("antVersion") {
doLast {
println(ant.properties["ant.version"])
}
}
tasks.register('antVersion') {
doLast {
println ant.properties['ant.version']
}
}
./gradlew antVersion
> Task :antVersion
Apache Ant(TM) version 1.10.17 compiled on April 6 2026
BUILD SUCCESSFUL in 565ms
1 actionable task: 1 executed
The standalone Ant used for the comparison runs on this page is 1.10.18, so the two differ by one patch release, and the Ant
inside Gradle is not the one you installed. The upgrade guides record when it moved: 1.10.13 in Gradle 8.2, 1.10.14 in 8.10 and
1.10.15 in 8.12; the 1.10.17 above was measured on 9.8.1. (A Gradle 8.14 distribution was not available here, so its Ant
version is not shown, and the 8.x entries come from the guide.) Only the core tasks are in that ant.jar. An optional task
needs its own JAR, and without it the failure is the ordinary Ant message about an optional component:
tasks.register("optionalTask") {
doLast {
ant.withGroovyBuilder {
"junit"("printsummary" to "yes")
}
}
}
tasks.register('optionalTask') {
doLast {
ant.junit(printsummary: 'yes')
}
}
./gradlew optionalTask
> Task :optionalTask FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':optionalTask' (registered in build file 'build.gradle.kts').
> Problem: failed to create task or type junit
Cause: the class org.apache.tools.ant.taskdefs.optional.junit.JUnitTask was not found.
This looks like one of Ant's optional components.
Action: Check that the appropriate optional JAR exists in
-ANT_HOME/lib
-the IDE Ant configuration dialogs
The same message appears in Groovy, with a Where: line for the build file added in front of What went wrong. How to put the
JAR on a classpath is in the section on custom tasks and Ant libraries below.
Using Ant tasks and types
The ant property of a build script is an AntBuilder. You run an Ant task by calling a method with the task’s name, pass
the XML attributes as named arguments, pass nested text as the first plain argument, and pass nested elements in a closure.
The translation from the XML that an Ant user knows is mechanical, and Kotlin needs one extra wrapper because the Kotlin
compiler cannot call methods that exist only at run time.
| Ant XML | Groovy DSL | Kotlin DSL |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
A task name with a hyphen is a string in both languages (ant.'check-stations'(...) in Groovy, "check-stations"(...) in
Kotlin), which the custom task example below uses. The example project has a data directory with the tidepool
stations.csv and a libs directory with commons-csv-1.14.1.jar; the tasks echo a message, copy, zip and checksum the data,
and list a path:
import org.apache.tools.ant.types.Path
tasks.register("hello") {
doLast {
val greeting = "hello from Ant"
ant.withGroovyBuilder {
"echo"("message" to greeting)
}
}
}
tasks.register("echoText") {
doLast {
ant.withGroovyBuilder {
"echo"("hello with nested text")
}
}
}
tasks.register("pack") {
val out = layout.buildDirectory.dir("pack")
doLast {
val dir = out.get().asFile
ant.withGroovyBuilder {
"mkdir"("dir" to dir)
"copy"("todir" to dir.resolve("data")) {
"fileset"("dir" to "data") {
"include"("name" to "**/*.csv")
}
}
"zip"("destfile" to dir.resolve("data.zip")) {
"fileset"("dir" to "data") {
"include"("name" to "**.csv")
"exclude"("name" to "**.tmp")
}
}
"checksum"("file" to dir.resolve("data.zip"), "algorithm" to "SHA-256", "fileext" to ".sha256")
}
println(dir.listFiles()!!.map { it.name }.sorted())
}
}
tasks.register("list") {
doLast {
val path = ant.withGroovyBuilder {
"path" {
"fileset"("dir" to "libs", "includes" to "*.jar")
}
} as Path
path.list().forEach {
println(it.substringAfterLast('/'))
}
}
}
tasks.register('hello') {
doLast {
String greeting = 'hello from Ant'
ant.echo(message: greeting)
}
}
tasks.register('echoText') {
doLast {
ant.echo('hello with nested text')
}
}
tasks.register('pack') {
def out = layout.buildDirectory.dir('pack')
doLast {
def dir = out.get().asFile
ant.mkdir(dir: dir)
ant.copy(todir: new File(dir, 'data')) {
fileset(dir: 'data') {
include(name: '**/*.csv')
}
}
ant.zip(destfile: new File(dir, 'data.zip')) {
fileset(dir: 'data') {
include(name: '**.csv')
exclude(name: '**.tmp')
}
}
ant.checksum(file: new File(dir, 'data.zip'), algorithm: 'SHA-256', fileext: '.sha256')
println dir.listFiles()*.name.sort()
}
}
tasks.register('list') {
doLast {
def path = ant.path {
fileset(dir: 'libs', includes: '*.jar')
}
path.list().each {
println it.substring(it.lastIndexOf('/') + 1)
}
}
}
./gradlew hello echoText pack list
> Task :hello
[ant:echo] hello from Ant
> Task :echoText
[ant:echo] hello with nested text
> Task :pack
[data, data.zip, data.zip.sha256]
> Task :list
commons-csv-1.14.1.jar
BUILD SUCCESSFUL in 551ms
4 actionable tasks: 4 executed
Everything the Ant task did is visible in the file system, not on the console: pack copied the CSV into build/pack/data,
zipped it, and checksum wrote data.zip.sha256 next to the archive. The Ant output prefix [ant:echo] is Gradle’s: it
puts the name of the Ant task in brackets after ant: so that you can tell Ant output from Gradle output. Ant types work the
same way. path returned an org.apache.tools.ant.types.Path, an ordinary Java object that the task iterated with list().
The same two builds print the same lines in Groovy.
Gradle itself has better tools for these jobs: a Copy, Zip or Sync task has declared inputs and outputs, is up to date
when nothing changed, and can be cached, which the ant.copy call above cannot (see
Working with Files and
Incremental Build). Use the ant object for a step
that has no Gradle equivalent (checksum with a particular file extension, a task from a company’s Ant library), not for a
step that has one. The manual makes the same point for the file tasks (copy, zip, unzip) when it discusses migration.
The ant object of the project and of the task
There are two ant objects. Every Project has one, and every Task has one too (Task.ant, an AntBuilder of its own).
Inside tasks.register("x") { doLast { ant... } } the name ant resolves to the task’s, because the task is the closest
receiver. That is the form shown above and it works with the configuration cache: the first run stores the cache and the
second reuses it.
./gradlew --configuration-cache hello pack
./gradlew --configuration-cache hello pack
Calculating task graph as no cached configuration is available for tasks: hello pack
> Task :hello
[ant:echo] hello from Ant
> Task :pack
[data, data.zip, data.zip.sha256]
BUILD SUCCESSFUL in 618ms
2 actionable tasks: 2 executed
Configuration cache entry stored.
Reusing configuration cache.
> Task :hello
[ant:echo] hello from Ant
> Task :pack
[data, data.zip, data.zip.sha256]
BUILD SUCCESSFUL in 600ms
2 actionable tasks: 2 executed
Configuration cache entry reused.
The cached run executed the same two tasks (a task that declares no outputs is never up to date) and
printed the same lines. A build script that grabs the project’s ant into a variable and uses it inside the action is
different, because the cache has to serialize the variable:
val projectAnt = ant
tasks.register("viaProject") {
doLast {
projectAnt.withGroovyBuilder { "echo"("message" to "project-level ant object") }
}
}
def projectAnt = ant
tasks.register('viaProject') {
doLast {
projectAnt.echo(message: 'project-level ant object')
}
}
./gradlew --configuration-cache viaProject
Calculating task graph as no cached configuration is available for tasks: viaProject
> Task :viaProject
[ant:echo] project-level ant object
FAILURE: Build failed with an exception.
* What went wrong:
Configuration cache problems found in this build.
1 problem was found storing the configuration cache.
- Task `:viaProject` of type `org.gradle.api.DefaultTask`: cannot serialize Gradle script object references as these are not supported with the configuration cache.
See https://docs.gradle.org/9.8.1/userguide/configuration_cache_requirements.html#config_cache:requirements:disallowed_types
...
BUILD FAILED in 663ms
1 actionable task: 1 executed
Configuration cache entry discarded with 1 problem.
The Kotlin build fails because it cannot serialize a reference to the build script. The Groovy build fails with a longer
message ("2 failures"), which says that the project’s AntBuilder holds Ant’s own Project and Target objects and one of
them is a java.util.Vector that uses ObjectOutputStream.putFields, and that a DefaultProject cannot be serialized
either. In both languages the repair is to let the closure resolve ant to the task’s.
Custom Ant tasks and Ant libraries
A custom Ant task is a Java class that extends org.apache.tools.ant.Task (see
Writing Custom Tasks for the Ant side). The
tidepool build has one, CheckStations, which checks the header and the length of the stations file. It was compiled
against the Ant 1.10.18 ant.jar with --release 17 and packaged with the jar tool, so that the same JAR runs under the
standalone Ant and under the Ant inside Gradle:
package io.tidepool.ant;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
/** Ant task: fails the build when a stations CSV is malformed or too short. */
public class CheckStations extends Task {
private java.io.File file;
private int min = 1;
public void setFile(java.io.File file) { this.file = file; }
public void setMin(int min) { this.min = min; }
@Override
public void execute() {
if (file == null) {
throw new BuildException("The file attribute is required");
}
try {
List<String> lines = Files.readAllLines(file.toPath());
if (lines.isEmpty() || !lines.get(0).equals("id,name,latitude,longitude,constituents")) {
throw new BuildException(file + " must start with the header id,name,latitude,longitude,constituents");
}
int stations = lines.size() - 1;
if (stations < min) {
throw new BuildException(file + " has " + stations + " stations, at least " + min + " required");
}
log("Checked " + stations + " stations in " + file.getName());
} catch (IOException e) {
throw new BuildException("Cannot read " + file, e, getLocation());
}
}
}
javac --release 17 -cp apache-ant-1.10.18/lib/ant.jar -d classes src/io/tidepool/ant/CheckStations.java
jar --create --file tools/tidepool-ant-tasks.jar --date 2026-10-16T00:00:00Z -C classes .
Neither command prints anything. Gradle’s taskdef is Ant’s: it takes either a name and a classname, or a resource
that is a properties file or an antlib, and a classpath. The manual says taskdef is usually the easier of the two for tasks, and
builds the classpath from Gradle’s dependency management by putting the JAR in a configuration and passing asPath as the
classpath. A configuration of its own keeps the Ant task JAR off the compile classpath of the code:
// The custom Ant task, taken from a jar on a Gradle configuration instead of a taskdef in build.xml.
val antTasks = configurations.create("antTasks")
dependencies {
antTasks(files("tools/tidepool-ant-tasks.jar"))
}
tasks.register("checkStations") {
group = "verification"
description = "Runs the custom Ant task check-stations"
doLast {
ant.withGroovyBuilder {
"taskdef"(
"name" to "check-stations",
"classname" to "io.tidepool.ant.CheckStations",
"classpath" to antTasks.asPath
)
"check-stations"("file" to "data/stations.csv", "min" to 3)
}
}
}
// The custom Ant task, taken from a jar on a Gradle configuration instead of a taskdef in build.xml.
configurations {
antTasks
}
dependencies {
antTasks files('tools/tidepool-ant-tasks.jar')
}
tasks.register('checkStations') {
group = 'verification'
description = 'Runs the custom Ant task check-stations'
doLast {
ant.taskdef(
name: 'check-stations',
classname: 'io.tidepool.ant.CheckStations',
classpath: configurations.antTasks.asPath
)
ant.'check-stations'(file: 'data/stations.csv', min: 3)
}
}
./gradlew checkStations
> Task :checkStations
[ant:check-stations] Checked 3 stations in stations.csv
BUILD SUCCESSFUL in 608ms
1 actionable task: 1 executed
The task name has a hyphen, so Kotlin passes it as a string and Groovy quotes the method name. Note the prefix:
[ant:check-stations] is the log message that the task wrote with log(...), and that is at Ant’s INFO priority, which Gradle
hides by default. The output above is shown with a build file that raises the Ant INFO messages to Gradle’s lifecycle level;
the next section explains the one line that does it (ant.lifecycleLogLevel). Without that line the task runs and
prints nothing. The taskdef does the job of the taskdef element of a build.xml; nothing has to be copied into the Ant installation.
An Ant library from Maven Central
A task library is a JAR, and Gradle can resolve it. The Apache Ant Compress Antlib (org.apache.ant:ant-compress:1.5) is an
Ant library, not part of the Ant inside Gradle (1.10.17), whose tasks are in the antlib namespace antlib:org.apache.ant.compress.
It is also a good example of why the configuration is separate: the library declares a dependency on org.apache.ant:ant
with the version range [1.8.0,), so the resolution adds a second Ant (1.10.18, as the dependency report below shows) to that configuration, which is one more reason to keep Ant task libraries out of the configurations that the code uses.
repositories {
mavenCentral()
}
// Gradle ships Ant 1.10.17 and the core tasks only; optional Ant libraries need their own classpath.
val antLibs = configurations.create("antLibs")
dependencies {
antLibs("org.apache.ant:ant-compress:1.5")
}
tasks.register("gzipStations") {
val classpath = antLibs.incoming.files
val out = layout.buildDirectory.dir("gz")
doLast {
ant.withGroovyBuilder {
"taskdef"(
"resource" to "org/apache/ant/compress/antlib.xml",
"uri" to "antlib:org.apache.ant.compress",
"classpath" to classpath.asPath
)
"mkdir"("dir" to out.get().asFile)
"antlib:org.apache.ant.compress:gzip"("src" to "data/stations.csv", "destfile" to out.get().file("stations.csv.gz").asFile)
}
println(out.get().asFile.list()!!.sorted())
}
}
repositories {
mavenCentral()
}
// Gradle ships Ant 1.10.17 and the core tasks only; optional Ant libraries need their own classpath.
configurations {
antLibs
}
dependencies {
antLibs 'org.apache.ant:ant-compress:1.5'
}
tasks.register('gzipStations') {
def classpath = configurations.antLibs.incoming.files
def out = layout.buildDirectory.dir('gz')
doLast {
ant.taskdef(resource: 'org/apache/ant/compress/antlib.xml',
uri: 'antlib:org.apache.ant.compress',
classpath: classpath.asPath)
ant.mkdir(dir: out.get().asFile)
ant.'antlib:org.apache.ant.compress:gzip'(src: 'data/stations.csv', destfile: out.get().file('stations.csv.gz').asFile)
println out.get().asFile.list().sort()
}
}
./gradlew dependencies --configuration antLibs
./gradlew gzipStations
------------------------------------------------------------
Root project 'tidepool-ant'
------------------------------------------------------------
antLibs
\--- org.apache.ant:ant-compress:1.5
+--- org.apache.ant:ant:[1.8.0,) -> 1.10.18
| \--- org.apache.ant:ant-launcher:1.10.18
+--- org.apache.commons:commons-compress:1.14
+--- org.tukaani:xz:1.6
\--- org.brotli:dec:0.1.2
> Task :gzipStations
[stations.csv.gz]
BUILD SUCCESSFUL in 638ms
1 actionable task: 1 executed
taskdef with a resource and a uri registers the whole antlib under the namespace and the gzip task is then called by
its qualified name (antlib:org.apache.ant.compress:gzip).
This build is compatible with the configuration cache, with one change that the listing already contains: the task copies
antLibs.incoming.files into a local variable instead of using the Configuration object itself, because a Configuration
cannot be stored. With the object captured, the first cached run fails with cannot serialize object of type
'org.gradle.api.internal.artifacts.configurations.DefaultLegacyConfiguration'. With the file collection, the cache is
stored and then reused:
./gradlew --configuration-cache gzipStations
Calculating task graph as no cached configuration is available for tasks: gzipStations
> Task :gzipStations
[stations.csv.gz]
BUILD SUCCESSFUL in 657ms
1 actionable task: 1 executed
Configuration cache entry stored.
The second run of the same command reused the entry:
Reusing configuration cache.
> Task :gzipStations
[stations.csv.gz]
BUILD SUCCESSFUL in 253ms
1 actionable task: 1 executed
Configuration cache entry reused.
(Both runs are the Kotlin DSL build; the Groovy DSL build printed the same lines apart from the timings.) The same applies to the Task.ant rule above: the
object has to be the task’s own and everything else must be a plain value or a file collection.
Ant properties and references
Ant keeps two global tables for a build: properties (strings that cannot be changed once they are set) and references
(objects with an id, such as a path). The AntBuilder exposes both as maps, and also lets you use the Ant tasks that
fill them:
tasks.register("props") {
doLast {
ant.properties["buildName"] = "tidepool"
ant.withGroovyBuilder {
"property"("name" to "dataDir", "location" to "data")
}
ant.withGroovyBuilder { "echo"("message" to "name=\${buildName} dataDir=\${dataDir}") }
ant.withGroovyBuilder { "path"("id" to "libs.path", "location" to "libs") }
println(ant.references["libs.path"].toString().substringAfterLast('/'))
println(ant.properties["dataDir"].toString().substringAfterLast('/'))
}
}
tasks.register('props') {
doLast {
ant.properties['buildName'] = 'tidepool'
ant.property(name: 'dataDir', location: 'data')
ant.echo(message: 'name=${buildName} dataDir=${dataDir}')
ant.path(id: 'libs.path', location: 'libs')
println ant.references['libs.path'].toString().substring(ant.references['libs.path'].toString().lastIndexOf('/') + 1)
println ant.properties['dataDir'].toString().substring(ant.properties['dataDir'].toString().lastIndexOf('/') + 1)
}
}
./gradlew props
> Task :props
[ant:echo] name=tidepool dataDir=/work/ant-integration/data
libs
data
BUILD SUCCESSFUL in 574ms
1 actionable task: 1 executed
ant.properties is a mutable map, ant.references a map of the Ant objects, and the property task can set a
location property. The result was name=tidepool set through the map, the ${dataDir} that the property task made absolute
(location resolves the name against the project directory, so the value is an absolute path, shortened here), and the references and properties
read back. Gradle does not translate its own project properties into Ant properties: -Pname=value, gradle.properties
and the ext block are invisible to an Ant task, and any value that an Ant file needs must be put into ant.properties
before the Ant task runs (an imported build needs it before the import, as the next section shows). In the other direction,
ant.properties shows the properties that an imported build.xml defined, including the ones that an Ant task set when
it ran.
Ant state is shared, and the configuration cache runs tasks in parallel
There is one Ant project behind the ant objects of a Gradle project, and its properties are shared: a property set by one
task is visible to another task that runs later. Two tasks, setIt and readIt with mustRunAfter, were run for this
with the cache off and then on, and readIt printed shared=set by setIt every time. That sharing has a price once the
configuration cache is on, because the cache lets independent tasks of one project run at the same time. Three tasks that
do not depend on each other and each set their own Ant property (so no name is used twice) before echoing it:
listOf("a", "b", "c").forEach { n ->
tasks.register(n) {
doLast {
ant.properties["name.$n"] = "tidepool"
ant.withGroovyBuilder { "echo"("message" to "$n: name=\${name.$n}") }
}
}
}
['a', 'b', 'c'].each { n ->
tasks.register(n) {
doLast {
ant.properties["name.${n}"] = 'tidepool'
ant.echo(message: "${n}: name=\${name.${n}}")
}
}
}
./gradlew a b c
./gradlew --configuration-cache a b c
./gradlew --configuration-cache a b c
> Task :a
[ant:echo] a: name=tidepool
> Task :b
[ant:echo] b: name=tidepool
> Task :c
[ant:echo] c: name=tidepool
BUILD SUCCESSFUL in 469ms
3 actionable tasks: 3 executed
Calculating task graph as no cached configuration is available for tasks: a b c
> Task :b
[ant:echo] b: name=${name.b}
> Task :a
[ant:echo] a: name=${name.a}
> Task :c
[ant:echo] c: name=tidepool
BUILD SUCCESSFUL in 422ms
3 actionable tasks: 3 executed
Configuration cache entry stored.
Reusing configuration cache.
> Task :b FAILED
> Task :a
[ant:echo] a: name=${name.a}
> Task :c
[ant:echo] c: name=tidepool
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':b' (registered in build file 'build.gradle.kts').
> 1 >= 1
...
BUILD FAILED in 494ms
3 actionable tasks: 3 executed
Configuration cache entry reused.
Without the cache the three tasks ran one after another and printed their properties. With the cache, the first run
stored the entry but two of the three tasks did not see the property they had just set (${name.b} was printed as it
was written), and on the second run, which reused the entry, a task failed with the unhelpful message 1 >= 1. Which tasks
fail changes from run to run, and the Groovy build behaved the same way. This is nondeterministic by nature. In six
runs of the Kotlin build with three tasks that set the same property, every run failed at least one task. My reading of the results, which the
manual does not state, is that the shared Ant project is not safe for writes from two threads. Tasks that only echo, with no
properties involved, were not affected in these runs. What did work was to give the tasks an order, so that they do not overlap:
listOf("a", "b", "c").forEachIndexed { i, n ->
tasks.register(n) {
if (i > 0) mustRunAfter(listOf("a", "b", "c")[i - 1])
doLast {
ant.properties["name.$n"] = "tidepool"
ant.withGroovyBuilder { "echo"("message" to "$n: name=\${name.$n}") }
}
}
}
['a', 'b', 'c'].eachWithIndex { n, i ->
tasks.register(n) {
if (i > 0) mustRunAfter(['a', 'b', 'c'][i - 1])
doLast {
ant.properties["name.${n}"] = 'tidepool'
ant.echo(message: "${n}: name=\${name.${n}}")
}
}
}
Reusing configuration cache.
> Task :a
[ant:echo] a: name=tidepool
> Task :b
[ant:echo] b: name=tidepool
> Task :c
[ant:echo] c: name=tidepool
BUILD SUCCESSFUL in 225ms
3 actionable tasks: 3 executed
Configuration cache entry reused.
With the chain of mustRunAfter, eight runs of the Kotlin build and eight of the Groovy build with the cache on all succeeded.
So when a build keeps ant calls in several tasks and runs with the cache, either order the tasks that write Ant properties
or keep the ant calls in as few tasks as possible. Moving the work to Gradle’s own Copy and Zip tasks removes the
problem together with the Ant dependency.
Ant logging
Gradle maps the priority of every Ant message to a Gradle log level, so Ant’s output joins the build’s output and obeys
-q, --info and --debug (see Directories & Logging).
| Ant priority | Gradle log level |
|---|---|
|
|
|
|
|
|
|
|
|
|
No priority maps to LIFECYCLE, the level Gradle shows by default, and many Ant tasks log their useful messages at INFO.
So javac ("Compiling 6 source files"), jar, mkdir and the ivy:retrieve report are all hidden by default (stage 1 of
the migration below shows it) and appear with --info, which also shows everything else. AntBuilder.setLifecycleLogLevel fixes that: every Ant message at that priority
or above is shown at LIFECYCLE (at least), and any message below it at INFO (at most). The example logs one message at each of
three priorities:
tasks.register("hello") {
doLast {
ant.withGroovyBuilder {
"echo"("level" to "info", "message" to "hello from info priority!")
"echo"("level" to "verbose", "message" to "hello from verbose priority!")
"echo"("level" to "warning", "message" to "hello from warning priority!")
}
}
}
tasks.register('hello') {
doLast {
ant.echo(level: 'info', message: 'hello from info priority!')
ant.echo(level: 'verbose', message: 'hello from verbose priority!')
ant.echo(level: 'warning', message: 'hello from warning priority!')
}
}
./gradlew hello
> Task :hello
[ant:echo] hello from warning priority!
BUILD SUCCESSFUL in 648ms
1 actionable task: 1 executed
Only the warning message is shown. After adding one line at the top of the file, the Kotlin DSL uses the enum
AntBuilder.AntMessagePriority.INFO and the Groovy DSL the string 'INFO':
ant.lifecycleLogLevel = AntBuilder.AntMessagePriority.INFO
ant.lifecycleLogLevel = 'INFO'
> Task :hello
[ant:echo] hello from info priority!
[ant:echo] hello from warning priority!
BUILD SUCCESSFUL in 657ms
1 actionable task: 1 executed
The INFO message now appears at the default level, the WARN message still does, and the verbose message (mapped to DEBUG)
stays out. If the level were raised to ERROR, WARN messages would be pushed down to INFO and disappear from the default
output. Set it once, where the project is configured, and not inside a task. Both logging builds are compatible with the
configuration cache: they were run with --configuration-cache, which stored an entry and then reused it.
Importing an Ant build
ant.importBuild("build.xml") reads an Ant file and registers one Gradle task for every target.
Dependencies between targets (depends) are kept, a target’s description becomes the task’s description, and the properties
and references of the file become ant.properties and ant.references. The example Ant file has four targets and the Gradle
build wires it up in four ways: it passes a property in before the import, renames what it imports, adds a Gradle task that an
Ant target depends on, and attaches a Gradle action to an Ant target.
<project name="tidepool-ant" default="hello" basedir=".">
<property name="greeting" value="Hello"/>
<property name="out.dir" location="build/ant"/>
<target name="hello" description="Greets from Ant">
<echo>${greeting}, from Ant</echo>
</target>
<target name="pack" depends="hello,stamp" description="Zips data/ (needs the Gradle task stamp)">
<mkdir dir="${out.dir}"/>
<zip destfile="${out.dir}/data.zip" basedir="data"/>
</target>
<target name="again" description="Calls hello through antcall">
<antcall target="hello"/>
</target>
<target name="maybe" if="do.maybe" description="Runs only when do.maybe is set">
<echo>maybe ran</echo>
</target>
</project>
// Pass a Gradle project property on to Ant before the import, so Ant's <property> keeps it.
findProperty("greeting")?.let { ant.properties["greeting"] = it }
// "stamp" is a Gradle task named in depends, so it keeps its name.
ant.importBuild("build.xml") { antTargetName ->
if (antTargetName == "stamp") antTargetName else "ant-$antTargetName"
}
tasks.register("stamp") {
doLast {
println("Gradle task stamp, greeting is ${ant.properties["greeting"]}")
}
}
// The Ant target "pack" names "stamp" in depends; "hello" gets an extra Gradle action.
tasks.named("ant-hello") {
doLast {
println("Hello, from Gradle")
}
}
// Ant targets have no inputs or outputs; declare them to get UP-TO-DATE checking.
tasks.named("ant-pack") {
inputs.dir("data")
outputs.dir(layout.buildDirectory.dir("ant"))
}
// Pass a Gradle project property on to Ant before the import, so Ant's <property> keeps it.
if (findProperty('greeting')) {
ant.properties['greeting'] = findProperty('greeting')
}
// "stamp" is a Gradle task named in depends, so it keeps its name.
ant.importBuild('build.xml') { antTargetName ->
antTargetName == 'stamp' ? antTargetName : "ant-${antTargetName}".toString()
}
tasks.register('stamp') {
doLast {
println "Gradle task stamp, greeting is ${ant.properties['greeting']}"
}
}
// The Ant target "pack" names "stamp" in depends; "hello" gets an extra Gradle action.
tasks.named('ant-hello') {
doLast {
println 'Hello, from Gradle'
}
}
// Ant targets have no inputs or outputs; declare them to get UP-TO-DATE checking.
tasks.named('ant-pack') {
inputs.dir('data')
outputs.dir(layout.buildDirectory.dir('ant'))
}
./gradlew ant-pack
./gradlew ant-pack
> Task :ant-hello
[ant:echo] Hello, from Ant
Hello, from Gradle
> Task :stamp
Gradle task stamp, greeting is Hello
> Task :ant-pack
BUILD SUCCESSFUL in 669ms
3 actionable tasks: 3 executed
> Task :ant-hello
[ant:echo] Hello, from Ant
Hello, from Gradle
> Task :stamp
Gradle task stamp, greeting is Hello
> Task :ant-pack UP-TO-DATE
BUILD SUCCESSFUL in 669ms
3 actionable tasks: 2 executed, 1 up-to-date
The first run executed the three tasks in the order that the Ant depends="hello,stamp" states: the Ant target hello (with
the extra Gradle action that printed Hello, from Gradle after the Ant echo), then the Gradle task stamp, then the Ant
target pack. The second run executed the first two again and found ant-pack UP-TO-DATE.
What an imported target is
The imported tasks are real Gradle tasks of the type org.gradle.api.tasks.ant.AntTarget; they have a path, a description, and
dependencies, and you can configure them as you would configure any other task.
./gradlew help --task ant-pack
> Task :help
Detailed task information for ant-pack
Path
:ant-pack (registered in build file 'build.gradle.kts')
Type
AntTarget (org.gradle.api.tasks.ant.AntTarget)
Options
--rerun Causes the task to be re-run even if up-to-date.
Description
Zips data/ (needs the Gradle task stamp)
Group
-
BUILD SUCCESSFUL in 818ms
(The Groovy build prints build.gradle in the "registered in build file" line, and nothing else differs.) The task has no
group, so the plain tasks listing does not show the imported targets at all and ends with the hint "To see all tasks and more
detail, run gradlew tasks --all"; with --all they are in the "Other tasks" section with their Ant descriptions. Because an AntTarget has no declared inputs or outputs, Gradle cannot tell whether
it is up to date and runs it every time. That is the reason for the last block of the build file above:
-
inputs.dir("data")andoutputs.dir(...)onant-packgive Gradle what it needs to decide that nothing changed, and the second run reportedUP-TO-DATE. Gradle does not check that the declaration is true: if the Ant target also reads another file, a change to that file is not noticed. -
ant-hellohas no outputs, so it ran again, and so didstamp.
The manual says the same: incremental build information can still be added to an imported Ant build, but it is more work than in a native Gradle build.
Renaming targets, and an Ant target that depends on a Gradle task
The one-argument importBuild keeps the Ant names, and the two-argument form takes a transformer that maps each name. Why
rename? When a plugin that is applied to the project adds a task with the same name as an Ant target, the build stops; the
case for jar is in Migrating Between Tools and the
case for build and clean is in the migration below. A prefix is the simplest rule, but there is a trap in it. Gradle applies
the transformer to every name in a depends attribute, not only to the targets that exist in the file. In the example,
pack names stamp, which is a Gradle task. With the plain rule "ant-$antTargetName" the dependency becomes ant-stamp:
ant.importBuild("build.xml") { antTargetName -> "ant-$antTargetName" }
./gradlew ant-pack
FAILURE: Build failed with an exception.
* What went wrong:
Could not determine the dependencies of task ':ant-pack'.
> Imported Ant target 'ant-pack' depends on target or task 'ant-stamp' which does not exist
The Groovy build fails in the same way, and the rule that fixes it is the one in the build file above: the transformer returns
the name unchanged for the names that are not Ant targets (stamp). Gradle looks the transformed name up among
the Gradle tasks, so a Gradle task (stamp here) can be named in an Ant depends, as long as the transformer lets the name
through.
Passing properties to Ant
Properties flow from Gradle to Ant through ant.properties, and they have to be set before the import, because an Ant
property that is already defined is not changed by a later property element. The first lines of the build file above
do this for a Gradle project property:
./gradlew ant-hello -Pgreeting=Ahoy
> Task :ant-hello
[ant:echo] Ahoy, from Ant
Hello, from Gradle
BUILD SUCCESSFUL in 601ms
1 actionable task: 1 executed
The Ant file says <property name="greeting" value="Hello"/> and the run printed Ahoy, because Gradle set the Ant property
first. Without the copy, -Pgreeting=Ahoy would be invisible to Ant, which is also what the manual says about imported
Ant tasks and Gradle project properties. In the other direction, the Ant properties are available as
ant.properties["name"] once the import has run, which is how the migration below reads the directories of the Ant build.
antcall and ant bypass the Gradle tasks
Ant’s antcall and ant tasks start another target, or another build file, by name inside Ant. Gradle handles them, but
what they run is the Ant target, not the Gradle task that was imported from it. In a copy of the example whose pack does
not depend on stamp, the Ant file has a target
<target name="again" description="Calls hello through antcall">
<antcall target="hello"/>
</target>
and the Gradle build still has the extra action on ant-hello:
./gradlew ant-again
> Task :ant-again
[ant:echo] Hello, from Ant
BUILD SUCCESSFUL in 695ms
1 actionable task: 1 executed
The Hello, from Gradle line of the ant-hello action did not run, because antcall called the Ant target hello and not
the Gradle task ant-hello. In the build above, where pack depends on the Gradle task stamp, the same antcall fails, because Ant cannot see the
Gradle task that its own depends names:
./gradlew ant-again
> Task :ant-again FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':ant-again' (registered in build file 'build.gradle.kts').
> Target "stamp" does not exist in the project "tidepool-ant". It is used from target "pack".
The rule that follows is to avoid antcall and ant (and subant) in the targets you keep, and to replace
them by task dependencies as soon as you can.
Multi-project builds
In a multi-project build every project can import its own Ant file, which is the first step of the manual’s recipe: a Gradle
build file with a single ant.importBuild line for each project that has a build.xml, and an empty one where there is none.
A typical Ant multi-project build calls the build of the project that it needs with <ant dir=... target=...>. The example
has two projects, util and web, and web builds util this way:
<project name="web" default="compile">
<property name="root.dir" location=".."/>
<target name="buildRequiredProjects">
<ant dir="${root.dir}/util" target="build"/>
</target>
<target name="compile" depends="buildRequiredProjects" description="Compiles the web module">
<echo>compiling web</echo>
</target>
</project>
With only the import in web/build.gradle(.kts), ./gradlew :web:compile runs the util Ant target inside the web task:
> Task :web:buildRequiredProjects
[ant:echo] building util
> Task :web:compile
[ant:echo] compiling web
BUILD SUCCESSFUL in 648ms
2 actionable tasks: 2 executed
util was built by the buildRequiredProjects task of web, and the Gradle project util never took part: its tasks,
caching and up-to-date checks were bypassed. The manual replaces the Ant detour with a task dependency between the Gradle
projects and warns to remove the old one.
ant.importBuild("build.xml")
// Replace the <ant dir="../util" target="build"/> detour with a Gradle task dependency.
tasks.named("compile") {
setDependsOn(listOf(":util:build"))
}
ant.importBuild 'build.xml'
// Replace the <ant dir="../util" target="build"/> detour with a Gradle task dependency.
tasks.named('compile') {
dependsOn = [':util:build']
}
> Task :util:build
[ant:echo] building util
> Task :web:compile
[ant:echo] compiling web
BUILD SUCCESSFUL in 675ms
2 actionable tasks: 2 executed
setDependsOn (Groovy: assigning dependsOn =) replaces the dependencies of compile, which is what removes
buildRequiredProjects; dependsOn(...) would have added to them. The manual calls this less robust than Gradle’s project
dependencies, but enough for the immediate problem; the real replacement is a project(":util") dependency
once the two projects are native. The order the manual suggests is: leaf projects first (those with no dependencies on other
projects of the build), then the projects that depend only on migrated ones, and finally remove the copy tasks that the
unmigrated Ant builds needed. See Multi Project Builds.
The configuration cache
The manual states that Ant integration is not fully compatible with the configuration cache: using Task.ant in an action
may work, as the first sections showed, but importing an Ant build is not supported, and Gradle turns the cache off when it finds
an imported target. A short run of the import example shows the message to look for in a CI log:
./gradlew --configuration-cache ant-hello
Calculating task graph as no cached configuration is available for tasks: ant-hello
> Task :ant-hello
[ant:echo] Hello, from Ant
Hello, from Gradle
Some tasks or features in this build are not compatible with the configuration cache.
See the complete report at file:///work/ant-integration/build/reports/configuration-cache/dxrxbv6qyar068fbr9z1rnx3v/32b53nhzx0w2qyviawmxozs2l/configuration-cache-report.html
BUILD SUCCESSFUL in 555ms
1 actionable task: 1 executed
Configuration cache disabled because incompatible task was found.
The build succeeds and runs without the cache, so a build with an import still works in a CI that enables the cache, but it
does not get any of its speed. The cache returns when the last ant.importBuild line is gone, which is the goal of the
migration below.
Migrating the tidepool build step by step
The manual’s advice for any Ant migration is to keep the Ant build next to the Gradle one until the Gradle build is proven to
produce the same artifacts, to leave the directory layout alone until the end, and to move one piece at a time while the
build stays working. This section follows it with the tidepool Ant build, whose dependencies are managed by Ivy and whose
build.xml has a custom Ant task. Every stage is a copy of the same project, the build works at the end of every stage, and
each stage was run in both DSLs.
Stage 0: the Ant and Ivy build
The sources are directly in src, not src/main/java, the JARs come from Ivy into lib, and the targets use the names that
clash with Gradle’s lifecycle (clean, build).
tidepool/
build.xml
ivy.xml
ivysettings.xml
data/stations.csv
src/io/tidepool/{engine,model}/*.java
tools/ivy-2.6.0.jar
tools/tidepool-ant-tasks.jar
<?xml version="1.0" encoding="UTF-8"?>
<project name="tidepool" default="package" basedir="." xmlns:ivy="antlib:org.apache.ivy.ant">
<property name="version" value="1.0.0"/>
<property name="src.dir" location="src"/>
<property name="lib.dir" location="lib"/>
<property name="build.dir" location="build"/>
<property name="classes.dir" location="${build.dir}/classes"/>
<property name="dist.dir" location="${build.dir}/dist"/>
<taskdef uri="antlib:org.apache.ivy.ant" resource="org/apache/ivy/ant/antlib.xml"
classpath="${basedir}/tools/ivy-2.6.0.jar"/>
<taskdef name="check-stations" classname="io.tidepool.ant.CheckStations"
classpath="${basedir}/tools/tidepool-ant-tasks.jar"/>
<path id="compile.classpath">
<fileset dir="${lib.dir}" erroronmissingdir="false" includes="*.jar"/>
</path>
<target name="clean" description="Delete build output">
<delete dir="${build.dir}"/>
</target>
<target name="resolve" description="Retrieve the compile configuration with Ivy">
<ivy:settings file="${basedir}/ivysettings.xml"/>
<ivy:retrieve conf="compile" pattern="${lib.dir}/[artifact]-[revision].[ext]"/>
</target>
<target name="prepare" description="Create the output directories">
<mkdir dir="${classes.dir}"/>
<mkdir dir="${dist.dir}"/>
</target>
<target name="build" depends="resolve,prepare" description="Compile the main sources">
<javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="compile.classpath"
release="17" includeantruntime="false"/>
</target>
<target name="package" depends="build" description="Package the library jar">
<jar destfile="${dist.dir}/tidepool-${version}.jar" basedir="${classes.dir}"/>
</target>
<target name="check-data" description="Check data/stations.csv with the custom Ant task">
<check-stations file="data/stations.csv" min="3"/>
</target>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
<info organisation="io.tidepool" module="tidepool" revision="1.0.0"/>
<configurations>
<conf name="compile" description="Needed to compile the main sources"/>
<conf name="runtime" extends="compile" description="Needed at run time"/>
</configurations>
<dependencies>
<dependency org="org.apache.commons" name="commons-csv" rev="1.14.1" conf="compile->default"/>
</dependencies>
</ivy-module>
<ivysettings>
<settings defaultResolver="central"/>
<caches defaultCacheDir="${basedir}/.ivy-cache"/>
<resolvers>
<ibiblio name="central" m2compatible="true"/>
</resolvers>
</ivysettings>
ant package check-data
Buildfile: /work/ant-integration/build.xml
resolve:
[ivy:retrieve] :: Apache Ivy 2.6.0 - 20260712075753 :: https://ant.apache.org/ivy/ ::
[ivy:retrieve] :: loading settings :: file = /work/ant-integration/ivysettings.xml
[ivy:retrieve] :: resolving dependencies :: io.tidepool#tidepool;1.0.0
[ivy:retrieve] confs: [compile]
...
[ivy:retrieve] :: retrieving :: io.tidepool#tidepool
[ivy:retrieve] confs: [compile]
[ivy:retrieve] 3 artifacts copied, 0 already retrieved (976kB/5ms)
prepare:
[mkdir] Created dir: /work/ant-integration/build/classes
[mkdir] Created dir: /work/ant-integration/build/dist
build:
[javac] Compiling 6 source files to /work/ant-integration/build/classes
package:
[jar] Building jar: /work/ant-integration/build/dist/tidepool-1.0.0.jar
check-data:
[check-stations] Checked 3 stations in stations.csv
BUILD SUCCESSFUL
Total time: 3 seconds
Ivy retrieved three JARs into lib (commons-csv, and the two that its POM depends on, commons-io and commons-codec), the
compile used release="17", and the jar and the custom task ran. This is the reference build. For the Ivy side see
Ivy Ant Tasks and
Ivy: Module Descriptors & Configurations.
Stage 1: import the build
The first commit only adds a Gradle build file and a settings file with the project name. Nothing about the Ant files changes.
ant.importBuild("build.xml")
ant.importBuild 'build.xml'
./gradlew clean package check-data
> Task :clean
> Task :resolve
> Task :prepare
> Task :build
> Task :package
> Task :check-data
BUILD SUCCESSFUL in 749ms
6 actionable tasks: 6 executed
Every Ant target, including the Ivy resolve, ran as a Gradle task, in the order of the depends attributes. No Ant output is
shown, because javac, jar and the ivy:retrieve messages are INFO messages (compare the logging section above). The custom
task is the same: it ran, and its report was hidden. Raising the Ant INFO level to the lifecycle level, as in the logging
section, makes the Ant output visible:
./gradlew check-data
> Task :check-data
[ant:check-stations] Checked 3 stations in stations.csv
BUILD SUCCESSFUL in 596ms
1 actionable task: 1 executed
The imported targets with their descriptions, as ./gradlew tasks --all lists them in the "Other tasks" section:
-----------
build - Compile the main sources
check-data - Check data/stations.csv with the custom Ant task
clean - Delete build output
package - Package the library jar
prepare - Create the output directories
prepareKotlinBuildScriptModel
resolve - Retrieve the compile configuration with Ivy
(prepareKotlinBuildScriptModel is a Gradle task that IDEs use, and it appears in the Groovy listing as well.) This stage
gives you a build that Gradle can run, with the same behavior and without the speed. The next stages replace what Gradle does
better.
Stage 2: compile with Gradle
Applying the Java Library plugin brings compileJava, assemble, build, clean and check. The manual’s recipe has four
steps: apply the plugin before the import, rename the Ant targets that clash, point the source set at the existing layout, and
rewire the dependencies. The plugin’s clean and build collide with the Ant targets of the same names, so the transformer
renames those two. The sources are read from the Ant property src.dir and the output goes to classes.dir, so Gradle
compiles exactly where the Ant build did. The dependencies are still the JARs that Ivy retrieved.
plugins {
`java-library`
}
group = "io.tidepool"
version = "1.0.0"
// Targets "build" and "clean" collide with the lifecycle tasks of the Java plugin; the others keep their names.
ant.importBuild("build.xml") { antTargetName ->
if (antTargetName == "build" || antTargetName == "clean") "ant_$antTargetName" else antTargetName
}
// Reuse the layout of the Ant build through its properties.
sourceSets {
main {
java.setSrcDirs(listOf(ant.properties["src.dir"]))
java.destinationDirectory.set(file(ant.properties["classes.dir"] as String))
}
}
dependencies {
// Still the jars that Ivy retrieved into lib/ by the Ant "resolve" target.
implementation(fileTree("lib") { include("*.jar") })
}
tasks.compileJava {
options.release = 17
dependsOn("prepare", "resolve")
}
// "package" no longer waits for the Ant compile; "assemble" builds the Ant jar instead of Gradle's.
tasks.named("package") {
setDependsOn(listOf(tasks.compileJava))
}
tasks.assemble {
setDependsOn(listOf("package"))
}
plugins {
id 'java-library'
}
group = 'io.tidepool'
version = '1.0.0'
// Targets "build" and "clean" collide with the lifecycle tasks of the Java plugin; the others keep their names.
ant.importBuild('build.xml') { String antTargetName ->
antTargetName == 'build' || antTargetName == 'clean' ? "ant_${antTargetName}".toString() : antTargetName
}
// Reuse the layout of the Ant build through its properties.
sourceSets {
main {
java {
srcDirs = [ant.properties['src.dir']]
destinationDirectory = file(ant.properties['classes.dir'])
}
}
}
dependencies {
// Still the jars that Ivy retrieved into lib/ by the Ant "resolve" target.
implementation fileTree('lib') { include '*.jar' }
}
tasks.named('compileJava') {
options.release = 17
dependsOn 'prepare', 'resolve'
}
// "package" no longer waits for the Ant compile; "assemble" builds the Ant jar instead of Gradle's.
tasks.named('package') {
dependsOn = ['compileJava']
}
tasks.named('assemble') {
dependsOn = ['package']
}
./gradlew assemble check-data
./gradlew assemble
> Task :resolve
> Task :prepare
> Task :compileJava
> Task :package
> Task :assemble
> Task :check-data
BUILD SUCCESSFUL in 790ms
5 actionable tasks: 5 executed
> Task :resolve
> Task :prepare
> Task :compileJava UP-TO-DATE
> Task :package
> Task :assemble
BUILD SUCCESSFUL in 714ms
4 actionable tasks: 3 executed, 1 up-to-date
compileJava replaced the Ant build target. It is incremental: on the second run it was UP-TO-DATE, while the Ant resolve
and package targets ran again, because they have no declared inputs and outputs. The details of the Kotlin DSL fragment:
java.setSrcDirs(listOf(...)) and java.destinationDirectory.set(...) configure the source directory set; dependsOn("prepare", "resolve") adds the Ant targets that make the directories and fetch the JARs; and
setDependsOn(listOf(...)) replaces the dependencies, which is what detaches package from the Ant build target and
assemble from Gradle’s own jar. The Groovy form assigns dependsOn = [...]. The jar task of the plugin is not used yet;
the Ant package target still builds the jar.
The assignment options.release = 17 replaces the release="17" of the Ant javac. Gradle’s toolchains are the better way
to say which JDK compiles, see Toolchains and
Java Projects.
Stage 3: dependencies, from Ivy to Gradle
The manual’s three steps for Ivy are to move the <dependency> elements into the dependencies block, to move the
configurations that a standard one cannot replace, and to move the resolvers of the Ivy settings into repositories. In
tidepool the ivy.xml has one dependency, with conf="compile->default", and the settings file names Maven Central, so it
becomes a dependency on the api configuration and mavenCentral():
| Ivy | Gradle |
|---|---|
|
|
|
|
|
nothing; a |
|
the Gradle user home dependency cache |
More of this table is in Migrating Between Tools, and the Gradle side of the vocabulary is in Declaring Dependencies & Configurations and Repositories & Content Filtering; versions that the Ant build kept in properties move into Version Catalogs. The changed part of the build file:
repositories {
mavenCentral()
}
// The <dependency> of ivy.xml, with conf="compile->default" written as "api".
dependencies {
api("org.apache.commons:commons-csv:1.14.1")
}
// Ivy's <retrieve> copied the jars into lib/; Gradle needs no such step, but a Copy task can do it.
tasks.register<Copy>("retrieveRuntimeDependencies") {
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("libs"))
}
tasks.compileJava {
options.release = 17
dependsOn("prepare")
}
repositories {
mavenCentral()
}
// The <dependency> of ivy.xml, with conf="compile->default" written as "api".
dependencies {
api 'org.apache.commons:commons-csv:1.14.1'
}
// Ivy's <retrieve> copied the jars into lib/; Gradle needs no such step, but a Copy task can do it.
tasks.register('retrieveRuntimeDependencies', Copy) {
from configurations.runtimeClasspath
into layout.buildDirectory.dir('libs')
}
tasks.named('compileJava') {
options.release = 17
dependsOn 'prepare'
}
The resolve dependency is gone from compileJava, the lib directory is not created any more, and the Ivy cache is not used.
The retrieveRuntimeDependencies task is only there to prove the equivalence:
./gradlew assemble check-data retrieveRuntimeDependencies
./gradlew dependencies --configuration runtimeClasspath
> Task :prepare
> Task :compileJava
> Task :package
> Task :assemble
> Task :check-data
> Task :retrieveRuntimeDependencies
BUILD SUCCESSFUL in 819ms
5 actionable tasks: 5 executed
------------------------------------------------------------
runtimeClasspath - Runtime classpath of source set 'main'.
\--- org.apache.commons:commons-csv:1.14.1
+--- commons-io:commons-io:2.20.0
\--- commons-codec:commons-codec:1.19.0
and the three files that it copied into build/libs:
commons-codec-1.19.0.jar
commons-csv-1.14.1.jar
commons-io-2.20.0.jar
These are exactly the three JARs that the Ivy retrieve copied into lib in stage 0 (commons-codec-1.19.0.jar,
commons-csv-1.14.1.jar and commons-io-2.20.0.jar). Gradle resolved the same versions because both tools read the
POM of commons-csv from Maven Central. When the Ant build keeps its JARs in a lib directory without Ivy, the manual
gives two routes: a flat-directory repository for the files that are named name-version.jar, and file dependencies for the
rest; that case, with its run, is in Migrating Between Tools.
Two Ivy ideas have no counterpart in Gradle: lock strategies (the dependency cache is safe for concurrent builds) and
latest strategies (there is one conflict rule, the newest version wins, and resolutionStrategy overrides it). The replacement
of the Ivy conflict managers is in
Dependency Resolution, Conflicts & Capabilities.
Stage 4: the custom Ant task
The manual gives two options for custom Ant tasks: use the existing task from Gradle, which is quick but needs the incremental build API for it to take part in up-to-date checks and has awkward Ant paths and filesets, or rewrite it as a Gradle task type, which is the better long-term choice because task types are typed, simpler and incremental. Both are shown.
The first is the taskdef in a Gradle task from the custom tasks section above, where the JAR sits on a configuration
(checkStations there). It replaces the imported check-data, and check depends on it; for the log message to be visible the
build file raises the Ant INFO messages to the lifecycle level, as in the logging section. At this stage the check-data
Ant target and its taskdef can still stay in build.xml. They are unused.
The second is a task class with declared inputs and outputs. It does what the Ant task did, writes a report file so that
Gradle has an output to track, and fails the same way. The class is in the build script here, because it is a few lines; in
a real build it belongs to a build-logic included build or buildSrc (see
Convention Plugins & Sharing Build Logic and
Writing Custom Tasks).
The stage also removes the rest of the Ant build: Gradle’s jar task writes the jar where the Ant package target did, so
build.xml, ivy.xml, ivysettings.xml, tools and lib are all deleted and there is no ant.importBuild left.
plugins {
`java-library`
}
group = "io.tidepool"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
api("org.apache.commons:commons-csv:1.14.1")
}
// The sources are still directly in src/; moving them to src/main/java is a separate commit.
sourceSets {
main {
java.setSrcDirs(listOf("src"))
}
}
tasks.compileJava {
options.release = 17
}
// Where the Ant "package" target wrote the jar.
tasks.jar {
destinationDirectory = layout.buildDirectory.dir("dist")
}
// The custom Ant task, rewritten as a task type with declared inputs and outputs.
abstract class CheckStations : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val stations: RegularFileProperty
@get:Input
abstract val minStations: Property<Int>
@get:OutputFile
abstract val report: RegularFileProperty
@TaskAction
fun check() {
val file = stations.get().asFile
val lines = file.readLines()
if (lines.firstOrNull() != "id,name,latitude,longitude,constituents") {
throw GradleException("$file must start with the header id,name,latitude,longitude,constituents")
}
val count = lines.size - 1
if (count < minStations.get()) {
throw GradleException("${file.name} has $count stations, at least ${minStations.get()} required")
}
report.get().asFile.writeText("$count stations\n")
logger.lifecycle("Checked $count stations in ${file.name}")
}
}
val checkStations = tasks.register<CheckStations>("checkStations") {
group = "verification"
description = "Checks data/stations.csv"
stations = layout.projectDirectory.file("data/stations.csv")
minStations = 3
report = layout.buildDirectory.file("reports/stations.txt")
}
tasks.check {
dependsOn(checkStations)
}
plugins {
id 'java-library'
}
group = 'io.tidepool'
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
api 'org.apache.commons:commons-csv:1.14.1'
}
// The sources are still directly in src/; moving them to src/main/java is a separate commit.
sourceSets {
main {
java {
srcDirs = ['src']
}
}
}
tasks.named('compileJava') {
options.release = 17
}
// Where the Ant "package" target wrote the jar.
tasks.named('jar') {
destinationDirectory = layout.buildDirectory.dir('dist')
}
// The custom Ant task, rewritten as a task type with declared inputs and outputs.
abstract class CheckStations extends DefaultTask {
@InputFile
@PathSensitive(PathSensitivity.NONE)
abstract RegularFileProperty getStations()
@Input
abstract Property<Integer> getMinStations()
@OutputFile
abstract RegularFileProperty getReport()
@TaskAction
void check() {
def file = stations.get().asFile
def lines = file.readLines()
if (lines.isEmpty() || lines[0] != 'id,name,latitude,longitude,constituents') {
throw new GradleException("${file} must start with the header id,name,latitude,longitude,constituents")
}
int count = lines.size() - 1
if (count < minStations.get()) {
throw new GradleException("${file.name} has ${count} stations, at least ${minStations.get()} required")
}
report.get().asFile.text = "${count} stations\n"
logger.lifecycle("Checked ${count} stations in ${file.name}")
}
}
def checkStations = tasks.register('checkStations', CheckStations) {
group = 'verification'
description = 'Checks data/stations.csv'
stations = layout.projectDirectory.file('data/stations.csv')
minStations = 3
report = layout.buildDirectory.file('reports/stations.txt')
}
tasks.named('check') {
dependsOn checkStations
}
./gradlew --configuration-cache build
./gradlew --configuration-cache build
Calculating task graph as no cached configuration is available for tasks: build
> Task :processResources NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :checkStations
Checked 3 stations in stations.csv
> Task :compileJava
> Task :classes
> Task :compileTestJava NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check
> Task :jar
> Task :assemble
> Task :build
BUILD SUCCESSFUL in 600ms
3 actionable tasks: 3 executed
Configuration cache entry stored.
Reusing configuration cache.
> Task :processResources NO-SOURCE
> Task :checkStations UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :compileJava UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :compileTestJava NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :jar UP-TO-DATE
> Task :assemble UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build UP-TO-DATE
BUILD SUCCESSFUL in 513ms
3 actionable tasks: 3 up-to-date
Configuration cache entry reused.
There is no import left, so nothing prevents the configuration cache: the first run stored an entry and the second
reused it, with every task UP-TO-DATE, including checkStations, which has a declared input file, a declared
property and a declared output file. The Groovy build
prints the same lines in a different order for processResources and processTestResources, which Gradle does not fix. A
failing check is the same message the Ant task produced, now from Gradle:
./gradlew checkStations
(run with minStations raised to 5)
> Task :checkStations FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':checkStations' (registered in build file 'build.gradle.kts').
> stations.csv has 3 stations, at least 5 required
Stage 5: check that the two builds agree
The manual treats the comparison of artifacts as an essential step, because even a small change such as the contents of a manifest can break a deployment. The
jar from the Ant package target (stage 3, which still ran the Ant jar) and the jar from Gradle’s jar task (stage 4) have
the same entries:
no difference
(jar tf of each jar, sorted, then diff.) Their class files are byte-identical as well. These are the first 16 characters of
the SHA-256 of each class in the Ant jar and the Gradle jar:
io/tidepool/engine/StationReader.class c3c8dceda692ba27 c3c8dceda692ba27
io/tidepool/engine/TideEngine.class 8f983de0e7875875 8f983de0e7875875
io/tidepool/model/Constituent.class 3394980f18831975 3394980f18831975
io/tidepool/model/Height.class c462e026766a42e4 c462e026766a42e4
io/tidepool/model/Station.class 1e0065751b6e13be 1e0065751b6e13be
io/tidepool/model/TideEvent.class 09d849a667f023cb 09d849a667f023cb
The jars are not identical, and that is expected: the manifests differ. The Ant jar task wrote Ant-Version and
Created-By lines, and Gradle’s jar wrote only Manifest-Version:
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.10.17
Created-By: 26+35-2893 (Oracle Corporation)
---
Manifest-Version: 1.0
If a consumer depends on those attributes, add them to the jar task’s manifest before you declare victory. Gradle 9
writes archives reproducibly by default (see
Publishing). The Gradle build
also stopped writing to lib, so a checkout does not need ant resolve any more. When you are satisfied, the cleanup the
manual describes remains: move the sources to src/main/java, delete the remaining overrides of the layout, and delete the
files of the Ant build.
Which route to take
| Situation | Route | Why |
|---|---|---|
A small Ant build that mostly compiles, tests and packages |
Rewrite from scratch with the Java Library plugin, |
Nothing in the Ant file is worth importing, and the result is faster and cacheable at once |
A large or unusual Ant build, many targets, many people |
Import first, replace one target at a time (stages 1 to 5) |
The build never stops working and the team can review each step |
Ant tasks with no Gradle equivalent ( |
Keep using them through |
They need no migration; keep the call in a task action that uses |
A mixed estate where one Ant build must stay |
Keep the import for that project only |
The other projects migrate; the Ant project gives up the configuration cache until it is converted |
The same idea in the other two tools
| Need | Gradle | Maven | Ant |
|---|---|---|---|
Run Ant tasks inside the build |
|
|
(native) |
Use a build file written for the other tool |
|
|
|
Use custom Ant tasks |
|
|
|
Get the same artifacts from the new build |
compare jars (stage 5) |
compare jars |
(reference build) |
The cross-tool comparison is in Comparing Ant, Maven & Gradle; for the Ant that Gradle embeds see Embedding Ant.
Gradle 8.14 and the Ant integration
The Ant integration page of the 8.14.6 manual was compared with the 9.8.1 page, and the only change in the text is the
sentence that says the configuration cache is turned off when an Ant build is imported. The examples are the same apart from
the command (gradle and ./gradlew) and a shorter form of the pmd dependency. The code on this page should therefore run
on 8.14, with these differences, none of which was run here: the embedded Ant is older (the 8.12 upgrade guide names 1.10.15),
the Kotlin DSL form by configurations.creating, which 9.8.1 deprecates with a pointer to the Gradle 9.6 upgrading guide,
is not yet deprecated, and the configuration cache is off by default in both 8.14 and 9.8.1, so its behavior on this
page applies when it is requested with --configuration-cache. The removal of jcenter() in 9.0 matters to old Ant-era builds that
used it as a repository.
Where the books are outdated
The Gradle in Action chapter on migration (Gradle 1.7) predates several things on this page. The checks are against the 9.8.1 manual.
| Older practice | Current status |
|---|---|
|
Removed in Gradle 7.0; use |
The project and its objects used freely while tasks execute, including |
With the configuration cache (stable in 8.1, preferred since 9.0) the project’s |
|
Java toolchains are preferred, and |
|
Removed in 9.0; use |
|
Removed in 5.0; use |
The Ant side has moved on too. The Ant inside Gradle 9.8.1 is 1.10.17 and contains only the core tasks, so a task from an
optional library needs its JAR on a taskdef classpath, as in the section above.