Ant sucks.

I've recently had the opportunity to use Ant quite heavily in a project at work, and have thus come to realize it sucks. Quite hard.

Contents

  • Contents
  • The structure of an Ant "project"
  • What's a build script anyway?
  • How Ant does it
  • Modelling a build process
  • Refactoring: Worsening the Design of Existing Code
  • Bonus chapter
  • Conclusion

The structure of an Ant "project"

The top-level element of an Ant project is the Project, represented by an XML element called project. This entity does not actually do anything useful.

A project consists of a list of definitions of properties, macros and targets, mixed with a number of miscellaneous directives (like import). That's the actual stuff - the need for the top-level node seems to be purely a side-effect of XML requiring, for no good reason, a single specific top-level node.

Contrast the Makefile - realizing that there is no real need for a "project" thingy, the Makefile puts properties (variables) and targets directly on the top-level.

I shouldn't bring up syntax this early in the flame, but I really must.

Compare this, the simplest Ant file I can come up with:

<project name="useless" default="all">
    <property name="foo" value="bar" />

    <target name="all">
        ... <!-- see below -->
    </target>
</project>

And the corresponding Makefile:

foo=bar

all:

See the difference? How much of the Ant file conveys actual contents? How much is just crud to explicitly write out the entire Ant AST in the rawest, most explicit form possible? At this point, I'm thinking that the writers of Ant have entirely missed ... everything that the field of CS has come up with when it comes to programming (and other) languages? But they certainly noticed the invention of XML.

What's a build script anyway?

The thing in common between most build systems is what I'll call the dependencies->target structure. That is, when the build tool is run, you give it a set of targets (or use the default as defined by the ant/make file), and the tool recursively builds all targets' dependencies before building the target, building each target according to some description of what it's supposed to do to build it and when it's supposed to be built.

Ant is no different there; and make is the canonical example of such a system.

The difference lies in how the systems determine what to build. Make has an intrinsic notion of targets and dependencies being files (rather, it has a notion about file targets and "phony" targets, defined by the Makefile) - if any of the dependencies are newer than the target, the target is rebuilt. If any of the dependencies were out-of-date and rebuilt, their dependents are rebuilt automatically by make.

This is probably old news to you, but I want to emphasize the effect of this property of make: provided with a correct set of dependencies, make does the right thing. And with make's ability to automatically check files, it is very easy to provide that correct complete set of dependencies.

(Granted, it does get harder in make with things like automatically discovering C/C++ header file dependencies, so many makefile writers google for recipes for automatically maintaining and updating dependency files and copy-paste this into their Makefile. But once this code is in the Makefile, make can take care of keeping the dependency files updated just as any other targets are kept updated.)

How Ant does it

As opposed to make, ant doesn't encourage this kind of specification of recursive dependencies. The easiest way to write an Ant file is to take the shell script you wrote (or memorized!) for building everything and translate this line-by-line into the corresponding Ant syntax for running things in a series. Let's say that the series of shell commands includes the generation of a generated whizbang file, like this:

javac Generate.java
java Generate Whizbang.whizbang Whizbang.java
javac Whizbang.java Foo.java Bar.java

It is relatively straight-forward to translate this into Ant syntax. But do take note of how irksome it is to do even simple stuff in Ant!

<project name="useless" default="all">
    <property name="foo" value="bar" />

    <target name="all">
        <javac>
            <!-- god forbit you write a path without putting it in an attribute of an XML node -->
            <src path="Generate.java" />
        </javac>
        <java>
            <arg text="Generate" />
            <!-- yes. please. put. every. string. in. a. separate. XML. node. thank. you. -->
        </java>
        <javac>
            <!-- repeat spanking -->
        </javac>
    </target>
</project>

See, Ant is bash-in-XML, with all the pain and none of the benefit. Every word becomes an attribute of an XML node inside the XML node specifying the "verb" of the action. Being able to put the word javac or java directly in the node name is just an effect of ant being so geared towards Java. For any other program, you'll end up with something like <exec><command name="java" /></exec>. Ugly!

Actually, previous versions of Ant supported putting commands as text inside XML nodes, like <exec>gcc -o target</exec>. This has since been deprecated. The producers of angle bracket keys probably bribed the Ant maintainer at the time.

Modelling a build process

This brings me to possibly the most important part of this whole comparison slash rant. The model of a build process enforced by Ant.

Ant: A project consists of a list of "targets" to run, each containing a list of commands and a list of other targets to run before those commands

Make: A Makefile consists of a list of targets (files or phony), each listing its dependencies and a list of commands to run to update the target from those dependencies

Notice the difference between updating a target from its updated constituent parts and building a target by executing dependency targets and commands. In Ant, all you're really doing is calling functions or post-order traversing a tree of targets.

Refactoring: Worsening the Design of Existing Code

The way an ant file is refactored into independent parts is to extract a bunch of commands doing one thing (for example mkdir cpp-objects followed by one build command for each object), into its own target (for example <target name=cpp-objects>) and then either inline calling that target with <ant ...> (this is just like a function call), or by adding that target as a dependency. The end result is a large set of phony targets that aren't linked to their products or sources. What have been gained? The resulting Ant file does exactly the same thing, just as bad as the original file did it.

Contrast this with how you'd do it with make. You take the set of commands in the rule you want to split and identify the products and sources produced by that set. Let's say it's every .o file generated from cpp-sources/*.c.

The canonical way to do that would be something like this:

CPP_OBJECTS=interesting-.o-files
cpp-objects/%.o: cpp-sources/%.c
    $(CC) -c -o $@ $<

other_target: $(CPP_OBJECTS)

Notice how features of make conspire to reduce duplication in typing and modelling, and how easy it is to produce something that properly models the building of the required set of .o files from the appropriate .c files and how the actual commands are derived from a generic pattern rule. This is what a DSL (Domain Specific Language) for building looks like. Not like this:

<exec><command name="build-many-cfiles.sh ${cpp_objects}"/></exec>

I suppose there could well be some kind of "for-each" XML thing in Ant, but it's still just so far from what we're modelling. Building is not running a series of commands, it is updating an end product from a set of sources. There is just no way for ant to actually do this with a model like that!

(Bonus: Conditional execution in Ant)

Then, we have the Ant way to build things conditionally (hang on, this'll get hairy! to spare the sensitive reader, this is only pseudo-ant code).

Let's say I want to do the Thing only when a certain file is newer than the target or the target file doesn't exist.

<doThing>
    <condition>
        <or>
            <and>
                <exists path="target" />
                <exists path="source" />
                <newer left="source" right="target" />
            </and>
            <not><exists path="target" /></not>
        </or>
    </condition>
</doThing>

Wow! That's lean.

Well, the end result is that some Ant hackers get so fed up with making ant do the right thing that they produce atrocities like

<target name="build" dependencies="clean"> ... </target>

or

<target name="build"><delete><dir path="output"></delete> ... </target>

Just to get ant to rebuild stuff that's out of date, accidentally rebuilding everything else in the process. But workstations are fast, right?

Yes. As I might write about some other day, waiting for the compile is not an unimportant factor of the subjective irksomeness of any build system. Going from "Yes, there's the typo!" to waiting for a complete rebuild of your project is enough to make any sane coder go bat-shit insane. If the delay is due to stupid build systems, even more so!

Conclusion

Ant is a poor way to model builds. Ant has syntax so brain-dead, it goes way beyond any bastard child of Java and Cobol in needless verbosity. In some places every, fucking, word, needs, its, own, XML element. And see above for Ant's take on the ubiquitous if statement. In short: Ant sucks.

If Ant was developed internally by any code shop, it would surely already have appeared on The Daily WTF. And we would all have laughed at the round-about ways to write shell scripts, and how many tokens you need in order even to do nothing (not to mention what you need to actually do anything at all).

The Real WTF, someone would say, is that these people produced a make replacement that does half as much as make and does it worse!

Then someone would counter with a joke post about "hey, if this language had conditionals, it'd probably look like this" (see above), at which point in time the original reporter realizes with a shiver that that was the exact way they did implement it.

Oh, and just for a final rubbing-in, did I mention that Ant sucks?

4 responses so far

  1. 1

    Mikael Jansson said,

    2008-05-27 23:52:37

    Regarding the suckyness of ANT, see "The Nature of Lisp" http://www.defmacro.org/ramblings/lisp.html by Coffeemug. Very insightful!

  2. 2

    tom said,

    2009-01-27 15:21:23

    Yes, it really sucks! Other way to implement if/else is to use <target name="antStyleIfElse"> <available file="some/file" property="file.exists" /> <antcall target="thenElse" /> </target>

    <target name="thenElse" if="file.exists"> ... then </target>

    <target name="thenElse" unless="file.exists"> ... else </target>

    Sucks even more, as it fills the ant script with 1000s of targets, and no possibility to mark them as "private" or so.

    Whow - this webside sucks too! Cannot handle < in entries :-(

    ... finally ...

  3. 3

    Bill Cernansky said,

    2009-02-24 23:11:59

    I could not have said it better. As a CM and build/release engineer, I actually avoid jobs that "require" ANT usage. Horrible, horrible, horrible.

  4. 4

    João Jerónimo said,

    2010-06-05 04:37:51

    I could not agree more with this article. What is unbelievable is that this shit is actually being used by many projects. Surely, the people who designed Ant don't really understand the dependency-solving paradigm behind build systems. They are like C programmers who decided to implement a compiler for a functional language.

    I agree that "make" has quite a lot of problems, but it does it's job correctly if it is provided with a correct specification.