Ant vs. Make: Why Make Is a Superior Java Build System
Posted by Luke Maurer on 06 / 04 / 2008
Despite its widespread use, Ant is ultimately inadequate as a build system. In some ways, it is well-suited for building simple Java projects: It is integrated with the JVM and extensible in Java, and it has an established library of code for a variety of tasks. Unfortunately, there are core responsibilities of a build system — keeping track of dependencies among files and rebuilding only as necessary — that Ant simply does not perform, delegating instead to every individual task to do what should be core functionality. Furthermore, its XML format rapidly becomes unwieldy as a project expands, as every little task mushrooms into a messy blob of tags and attributes. In the end, Ant is little more than glorified XML batch files.
For decades, the de facto standard program for driving the build process for a C code base has been Make. Like so much else in the world of C and Unix, the Make language is terse, quirky, rather cryptic - and very effective. It is a simple, straightforward definition of what to build, how to build it, and what needs to be built first:
main.o: main.c options.h util.h
cc -o main.o -c main.c
options.o: options.c options.h util.h
cc -o options.o -c options.c
util.o: util.c util.h
cc -o util.o -c util.c
app: main.o options.o util.o
cc -o app main.o options.o util.o
The first entry says that the object file main.o depends on its source file, main.c, and the two header files options.h and util.h, and provides the command line for the C compiler to output main.o. Similarly for options.o and util.o. Finally, the executable file app, the ultimate product, depends on all three object files and has a somewhat different command line.
What's cool about Make is that it knows what's up to date and doesn't rebuild anything that is. For instance, if I've just run "make app", and I run it again, nothing happens — everything's already compiled. If I then change main.c and rerun "make app", it will recompile main.o since its dependency main.c is newer, and then it will relink app because its dependency main.o has changed. But it won't recompile either of the other object files. In contrast, if I change util.h, everything has to be rebuilt.
Ant cannot directly express the relationships between these files. Yes, you can say that app depends on main.o — but that only means that building app requires building main.o. Ant only uses dependencies to add targets to build, and figure out what order to build things in. But since the targets don't correspond to files, it can't be any more intelligent about them. Each task that should be skipped if its output files are up to date has to implement such functionality for itself. Most don't. (The only reason you don't see every Java class recompiled every time is that javac itself has a dependency-checking system of its own.)
This is by far not the only inadequacy of Ant, but to me it's the most egregious. Ant desperately needs an easy, flexible way to say what files depend on what other files, and how to decide whether a file needs to be updated. The <uptodate/> task is far too blunt an instrument to use for this — it's awkward to use, especially since Ant has no built-in if construct (?!!). Without this functionality at its core, there's little to recommend Ant over any other scripting language.
Fortunately, there is an alternative. Er, well, there will be one, when I'm done writing it :-) But that's a post for another day (soon).