Apache Ant and NAnt are XML-driven build tools that automate compiling, testing, and packaging — Ant for Java, NAnt for .NET. Both predate Maven, Gradle, and MSBuild, but they remain in active use across legacy enterprise systems and any project where a small, deterministic, dependency-free build runner is preferable to a heavy framework. This guide walks through how Ant and NAnt actually work, where they fit in a modern build pipeline, and when you should pick them over (or alongside) the newer tools that replaced them.
What is build automation, really?
Build automation is the practice of turning a checkout of source code into a deployable artefact — a JAR, DLL, container image, or static site bundle — without anyone running commands by hand. A build automation tool reads a declarative file (an XML script for Ant/NAnt, a Groovy or Kotlin DSL for Gradle, an MSBuild project for .NET) and executes a graph of tasks: clean, compile, test, package, sign, publish.
A useful build runner has to do four things well:
- Resolve dependencies between tasks. If
packagedepends ontestandtestdepends oncompile, the runner must execute them in the right order and skip any task whose inputs haven't changed. - Be reproducible. The same source tree on the same JDK or .NET SDK should produce a byte-identical artefact (or close to it). This is what enables zero-downtime deployments and one-click rollbacks — you can recreate any build from its commit hash.
- Be platform-agnostic. A build that only works on the lead engineer's laptop is broken. Ant and NAnt were both designed around this — XML build scripts run identically on Linux, macOS, and Windows.
- Be scriptable from CI. Whatever runs locally has to run unmodified inside Jenkins, GitHub Actions, GitLab CI, or DeployHQ's build pipeline.
If you've never formalised your build, see What is a build pipeline? for the broader concept and stages — this guide focuses specifically on Ant and NAnt as the build runners that sit inside a pipeline.
Apache Ant: the Java build pioneer
Ant — short for Another Neat Tool — was written by James Duncan Davidson in 2000 to build Apache Tomcat. Make existed, but its tab-sensitive syntax and shell-fragility were painful on Windows, and Java needed something portable. Ant's answer: describe your build as XML, ship it as a JAR, run it on any JVM.
A minimal build.xml:
<project name="MyProject" default="package" basedir=".">
<property name="src.dir" value="src"/>
<property name="build.dir" value="build"/>
<property name="classes.dir" value="${build.dir}/classes"/>
<property name="jar.dir" value="${build.dir}/jar"/>
<property name="main-class" value="com.example.Main"/>
<target name="clean">
<delete dir="${build.dir}"/>
</target>
<target name="compile" depends="clean">
<mkdir dir="${classes.dir}"/>
<javac srcdir="${src.dir}" destdir="${classes.dir}" includeantruntime="false"/>
</target>
<target name="package" depends="compile">
<mkdir dir="${jar.dir}"/>
<jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}">
<manifest>
<attribute name="Main-Class" value="${main-class}"/>
</manifest>
</jar>
</target>
</project>
Run it with ant package and Ant walks the dependency graph: clean → compile → package. Each <target> is a node, depends= is the edge.
What Ant does well
- Imperative control. Every target is a procedure you wrote. There is no convention to memorise — if a step is in the build, it's in the script. For unusual builds (legacy code generators, custom packaging steps, platform-specific signing) this is liberating.
- Massive task library. Out of the box Ant ships
<javac>,<jar>,<war>,<exec>,<copy>,<zip>,<ftp>,<sshexec>,<junit>,<scp>, plus hundreds of Ant-Contrib and third-party tasks. includeantruntime="false"matters. Always set this on<javac>. Without it Ant adds its own runtime classpath to your compile, which can mask missing dependency declarations and produce builds that work locally but fail in CI.
Where Ant breaks down
- Dependency management is bolted on. Ant has no native concept of
fetch this library from a registry
. You either commit JARs to the repo (the 2003 approach) or bolt on Apache Ivy for dependency resolution. Maven and Gradle solved this natively. - XML grows fast. A 200-line
build.xmlis fine. A 2,000-linebuild.xmlwith conditional targets, dynamic property loading, and macrodefs becomes write-only code. Most teams that hit this point migrate to Gradle. - No incremental compile by default. Ant's
<javac>recompiles whatever you point it at. Gradle's incremental compiler and build cache are dramatic wins on large codebases.
When to keep Ant
Don't migrate Ant to Gradle for the sake of it. Ant remains the right tool when:
- You're maintaining a legacy product with a working
build.xml. Migration risk usually outweighs the benefit. - You need a small, dependency-free build runner — Ant is a single JAR plus the JDK, no daemon, no wrapper script.
- Your build is genuinely procedural (custom code generation, multi-stage signing, hardware-in-the-loop tests). Ant's imperative model is honest about that complexity.
NAnt: build automation for .NET
NAnt is Ant ported to .NET — same XML semantics, same target/depends model, retargeted at the C# compiler (<csc>) and the .NET BCL. It was the dominant .NET build tool from roughly 2003 until Microsoft shipped MSBuild with .NET 2.0 in 2005.
A minimal NAnt build:
<?xml version="1.0"?>
<project name="MyNetProject" default="build" basedir=".">
<property name="build.dir" value="build"/>
<property name="src.dir" value="src"/>
<target name="clean">
<delete dir="${build.dir}" failonerror="false"/>
</target>
<target name="compile" depends="clean">
<mkdir dir="${build.dir}"/>
<csc target="library" output="${build.dir}/MyProject.dll" debug="true">
<sources basedir="${src.dir}">
<include name="**/*.cs"/>
</sources>
<references>
<include name="System.dll"/>
<include name="System.Core.dll"/>
</references>
</csc>
</target>
<target name="build" depends="compile"/>
</project>
The **/*.cs glob pattern is the most common NAnt question on Stack Overflow — it's a recursive include matching every .cs file under src.dir.
Why NAnt still exists
In 2026, NAnt is essentially in maintenance mode — the last stable release was 0.92 in 2012 — but it is still in production at a surprising number of organisations because:
- Cross-platform .NET predates .NET Core. Before 2016, NAnt + Mono was the only realistic way to build .NET code on Linux. Plenty of build servers were standardised on this stack and never migrated.
- It runs anywhere with Mono or .NET Framework. No SDK-style project files, no
dotnetCLI, no NuGet restore step. For old codebases, that's a feature. - Migration cost is real. Moving a NAnt build to MSBuild or
dotnet buildmeans adopting SDK-style.csprojfiles, restructuring solution layouts, and re-validating the artefact bit-for-bit.
When to migrate off NAnt
- You're targeting .NET 6 or later. Use
dotnet buildand SDK-style projects — there is no reason to introduce NAnt to a modern .NET project. - You need a maintained tool with security patches. NAnt has not had a release since 2012; if a CVE drops in a transitive dependency, you're on your own.
- You want first-class NuGet support. NAnt predates NuGet and has no native integration.
How Ant and NAnt fit into a modern build pipeline
Ant and NAnt are build runners, not pipelines. A pipeline is the orchestration layer above them — the thing that detects a Git push, runs the build, captures the artefact, and ships it.
flowchart TD
A[Git push] --> B[CI trigger]
B --> C[Restore dependencies]
C --> D["Run build runner<br/>(Ant / NAnt / Gradle / MSBuild)"]
D --> E[Run tests]
E --> F[Package artefact]
F --> G[Store as versioned artefact]
G --> H[Deploy to environment]
H --> I[Smoke test + monitor]
I -->|pass| K[Done]
I -->|fail| J[Rollback to<br/>previous artefact]
A few things in that diagram are worth calling out because Ant/NAnt projects often skip them:
- Capture the artefact. The output of
ant packageornant buildis the thing you deploy. It should be stored, versioned, and addressable by commit SHA. DeployHQ's deployment artefacts feature does this automatically; if you're rolling your own, push the JAR/DLL to S3 or a registry tagged with the build's commit hash. - Separate build and deploy. A common Ant antipattern is bundling deploy steps (
<scp>,<sshexec>) into the samebuild.xmlthat compiles the code. Don't. Build produces an artefact; a separate deployment tool ships it. This separation is what enables one-click rollback — you redeploy a previous artefact rather than rolling back the source. - Use the 3-2-1 rule for artefact storage. Three copies, two media, one off-site. For builds, that means: the artefact registry, the CI cache, and an off-site archive (S3 with versioning + replication). Builds you can't reproduce and can't retrieve are a recovery-time-objective (RTO) hazard.
If the project is already on DeployHQ, Ant or NAnt slots into the build pipeline step — call ant package (or nant build) as a build command, then ship the resulting artefact through the deploy step. For Node-heavy pipelines see using Node.js and NPM with the DeployHQ build pipeline, which follows the same pattern.
Ant and NAnt vs the modern alternatives
| Tool | Language | Config | Dependency mgmt | Incremental | Active development |
|---|---|---|---|---|---|
| Ant | Java | XML | Via Ivy | No (manual) | Maintenance |
| NAnt | .NET | XML | None native | No (manual) | Effectively dead (last release 2012) |
| Maven | Java | XML | Native (Central) | Partial | Active |
| Gradle | JVM | Groovy/Kotlin DSL | Native | Yes (build cache + incremental compile) | Active |
| MSBuild | .NET | XML (.csproj) |
Native (NuGet) | Yes | Active (shipped with .NET SDK) |
| Bazel | Polyglot | Starlark | Native | Yes (remote cache) | Active |
| Make | Any | Makefile | None | Yes (timestamp-based) | Stable |
A few honest takes:
- Maven is the right Ant successor for most Java projects you're starting today, unless you have strong reasons to want Gradle's flexibility.
- Gradle is the right Ant successor for projects where Maven's convention-over-configuration is too rigid — Android, polyglot JVM, anything with a custom packaging story.
- MSBuild is the only .NET build tool you should reach for in 2026. NAnt to MSBuild is the migration to plan, not the destination to build out.
- Bazel is overkill unless you're a monorepo with hundreds of contributors. The cost is real (BUILD files everywhere, learning Starlark) and only pays off at scale.
- Make is genuinely useful as a thin orchestration layer over any of the above — see Makefiles for web developers for that pattern.
Practical gotchas with Ant and NAnt
A few things that will bite you that no tutorial mentions:
- Property immutability. In Ant,
<property>is set-once. Reassigning it in the same build silently does nothing. Use<var>(from Ant-Contrib) or load properties from a file with<property file="build.properties"/>. failonerror. Most Ant tasks have afailonerrorattribute. Default is usuallytrue, but<delete>defaults tofalsein older versions. If yourcleantarget appears to succeed when the build directory doesn't exist but later steps fail mysteriously, addfailonerror="true"everywhere.- NAnt and modern .NET assemblies. NAnt's
<csc>task targets old C# compiler flags. Anything past C# 6 features (string interpolation, expression-bodied members) may need explicitlangversionor won't compile at all oncsc.exefrom .NET Framework. This is a common reason teams finally migrate. - Path handling on Windows. Ant treats paths as POSIX strings; use forward slashes (
src/main/java) insidebuild.xmleven on Windows. Backslashes will work in some contexts and silently break in others. - Locale-dependent failures. Both Ant's
<javac>and NAnt's<csc>will pick up the system locale for compiler messages, which makes CI logs hard to grep. SetJAVA_TOOL_OPTIONS=-Duser.language=enin your CI environment.
Putting it together
Ant and NAnt aren't dead — they're stable, predictable, and still running production builds across the industry. They're the right answer when:
- You're maintaining an older codebase that already builds reliably with them
- You want a transparent, scriptable build runner with no convention magic
- Your build is genuinely procedural and a Maven-style lifecycle would be a fight
They're the wrong answer when:
- You're starting a new Java project (use Maven or Gradle)
- You're starting a new .NET project (use MSBuild via
dotnet build) - You need first-class dependency management, build caching, or incremental compilation
Whichever build runner you choose, the pipeline around it matters more than the runner itself. Capture every artefact, version it by commit, separate build from deploy, and make rollback a single click — that's what turns a build script into a build system.
If you're ready to wire your Ant or NAnt build into a pipeline that handles artefacts, environments, and rollbacks for you, DeployHQ connects to GitHub, GitLab, Bitbucket, and self-hosted Git, runs your build commands in an isolated environment, and ships the result to your servers — with zero-downtime deployments and one-click rollback built in.
Questions about this guide, or stuck on a specific Ant/NAnt migration? Reach out at support@deployhq.com or find us on X at @deployhq.