It is said that Ant is the proper tool for building Java projects. It is certainly intended to be more portable, and maybe integrates well with IDEs. And it is said that Make works poorly with Java because Java dependencies are too complex for it to handle. One could infer from that that Ant is also somehow better than Make specifically regarding dependencies. If all this is true, why am I using Make?
Let me first deal with the reasons that are specific to my situation:
-
I rarely develop on (say) Windows. Occasionally, I have developed for Windows, but only by cross-compiling. This makes build portability less of an issue for me.
-
Many of my Java projects also have C components, such as JNI, Linux kernel modules and launchers, that allow me to program beneath the Java abstraction. Ant didn't seem to buy me anything in these situations that Make didn't already offer.
What I actually want to say about Ant, specifically its <javac> task, and without using <depend>, is that it doesn't really handle Java dependencies very well either.
Suppose you have a class Foo
, which references class Bar
:
public class Foo { public static void main(String[] args) throws Exception { Bar.test(); } }
public class Bar { public static String test() { System.out.println("Test"); return null; } }
These classes compile fine, until the return type of
Bar.test()
changes.
Foo.java does not need to be changed, but
Foo
must be recompiled in
order to discover the new signature of the method.
Foo.class originally contains a reference
to Bar.test()Ljava.lang.String;, but if
the return type changes to Object
, Foo.class must be
re-created so that it references
Bar.test()Ljava.lang.Object; instead.
What does Ant do in this situation? It
detects that the source file for Bar
has changed, and passes it, along
with any other changed source files it has detected —
but not Foo.java — to a
single invocation of javac.
This is certainly more efficient than trying to get
Make to pick out changed files
and compile them separately. However, both approaches
are incorrect anyway, as they miss the need to
re-compile Foo
too.
Even if Ant is integrated with an IDE that searches for compile-time errors as they arise, this oversight is not detected, because there is no compile-time error. Even in more likely cases, where there would be an error, I would expect the build system to do enough work to detect it, i.e., it should submit everything necessary to the compiler, even if it's just to find that there is a compile-time error, and Ant does not do that. This requirement is important if, for whatever reason, you intend to program without an IDE.
There is a <depend> task which handles this problem, but it fails to detect classes that depend on changes to inlined constants if you're using Java 7 or earlier. However, I've just discovered that Java 8 puts a runtime-redundant reference to the class defining the constant into the referencing class; <depend> picks this up and produces the correct behaviour. Nevertheless, <depend> feels like a hacky way to fix the oversimple <javac> task, when really there should be a single, integrated task whose aim is to do things correctly from the start.
<depend> has a closure flag that makes it detect more kinds of change, but it wouldn't need it if it didn't miss any. And the flag wouldn't be optional if it didn't sometimes trigger unneccessary compilations, nor incur a significant overhead. Under what circumstances would you not turn it on to get a more internally consistent build? At this point, is it not better to just compile the whole tree on the condition that any source file changed?
I find that it's simpler to have a clean build within a source tree, a single invocation of javac that compiles everything at once. It guarantees internal consistency among the class files of the tree. Make is perfectly capable of handling that, and I wrote Jardeps to deal with three specific problems that arise from it:
-
It's easy to run javac unconditionally on every invocation of Make, but the Java code might not be the only component of the project. Changes to the other parts should not incidentally trigger recompilation of the Java component, so something is needed to detect an internal change to a source tree.
-
For sufficiently large projects, a complete rebuild could be too time-consuming. It should be possible to break the Java code up into elements that are consciously free of dependency cycles between each other, and build only the elements that change.
-
The programmer will also be conscious of dependencies between those elements, especially between one element and the interface of another. It should be possible to express those dependencies to avoid manually working out when to force re-compilation of elements that have not changed internally.
I'll mention a couple of other points in Ant's favour:
-
Its approach does mean that all classes get compiled with full annotation processing.
Wait a minute. Is that actually true? Won't some classes get compiled implicitly when referenced by a class that has changed? You could turn implicit compilation off, but then even more work will be missed.
Perhaps it doesn't matter, as every class will be processed on the first build (if one knows the full set of source files beforehand), and it only needs to be reprocessed if the source has changed. However, maybe the generated source files will need recompiling even if they haven't changed, just as
Foo
does.Anyway, it's no longer an advantage over Jardeps, which now can ensure that every discovered source file is annotation-processed.
-
The <javac> task could be improved or replaced, to perform the same kind of dependency checks that Jardeps does. (But does Ant support any kind of order-only dependency?)