Using Makefiles for Java development

Introduction

make is one of the original Unix tools for Software Engineering, dating back to work at AT&T Bell Labs in the 70's in the same group that invented C and Unix. In general, the idea behind make is to automate many of the steps commonly required in the construction of programs -- ie. to make your development task easier.  Some of make's features make it particularly well suited to C and C++ application development.  However, make can be very useful for Java development as well.

Many tutorials exist for a quick introduction to make:

A GNU Make Tutorial
Make - a tutorial
An Introduction to the UNIX Make Utility
Creating Makefiles: A Mini Tutorial

If you have never used make before, have a look at at least one of those tutorials before proceeding, or at least check out the make manual page (run  man make in a terminal window).

Telling make about Java

make already understands basic things about application development in C: for example, that  object files have a .o suffix, sourcecode files have a .c suffix, and the way to get the first from the second is to run the C compiler, cc, with appropriate arguments.  make does not know the corresponding things about .java and .class files and the Java compiler javac.  But it can easily be told about them.

Put this line in your Makefile:

.SUFFIXES: .java .class

That tells make to add .java and .class to the list of filename suffixes that it knows about.  But you still need to tell it what to do with files with those suffixes.  So put these lines in your Makefile too:

.java.class:
    javac $<

(Note that in this rule pattern, like every make rule, the second line -- the command -- needs to start with a tab character, not spaces.)  That tells make that any file with a .class suffix depends on a file with the same name but a .java suffix, and the way to get the first from the second is to run javac with the depended-on file (the .java file) as argument.  If you want to always pass certain flags to javac, you can put them in here; it's just a command line that will be forked in a new shell. The $< is make's notation for the name of the depended-on file, whatever it happens to be.

Now suppose your Java application has the source code files A.java, B.java and C.java and so you want them to be (re)compiled to the corresponding .class file when you make changes to any of them.  For convenience, you can define the corresponding class file names as a macro:

CLASSES = A.class B.class C.class

And then set up a rule with a dummy target that depends on the value of that macro:

all: $(CLASSES)

With all that in a Makefile in your working directory, you just need to run make all on the command line, and if any of the .java files have been modified more recently than their corresponding .class files, make will automatically run javac on them for you.  Pretty simple, but pretty useful; it can save a lot of typing.

Useful Makefile targets for coursework development

Besides compiling classes, you often need to run tests, turn in assignments, generate documentation, and clean up your working directory.  Each of these actions can be easily scripted in a Makefile, by associating each with a rule that contains the appropriate commands.

Because some of these depend on source code files directly, it is convenient to define another macro whose value is the list of source file names:

SOURCES = A.java B.java C.java

Now you can define the rules you want.  They might look something like this:

turnin: $(SOURCES)
    bundleP3

clean:
    /bin/rm $(CLASSES)

doc: $(SOURCES)
    javadoc -author $(SOURCES)

test: $(CLASSES)
    java A testfile1; diff outfile1 reffile1
    java A testfile2; diff outfile2 reffile2
    java A testfile3; diff outfile3 reffile3

With all that in your Makefile, you are ready to easily execute most common, repetitive Java development tasks.  And once you understand the basics of make, you can usually pretty easily write any other command-line task you want to automate, such as checking files in and out of a CVS repository, etc.

More complicated Java dependencies

As shown above, it is easy to get make to understand that a .class file depends on a .java file with the same name.  And the Java compiler javac itself understands some dependencies:  When it is compiling a class X that references another class Y, and Y.java is newer than Y.class, or Y.class doesn't exist, it will automatically compile Y as well.   However, javac and the simple .class/.java dependency in make aren't smart enough to go the other way and figure out that if you make a change to Y.java, you might have to recompile X as well as Y

One crude way to address this is to run make clean before make all.  Since make clean (as defined above) removes all .class files, make all will then go ahead and rebuild all of them, and all dependencies should then be met.  However, this will probably involve more compiling than is really necessary.

A better approach is to explicitly express dependencies among .class files in your Makefile.  So for example if class A references classes B and C, and so depends on them, you can write the line

A.class: B.class C.class

Now if you make a change to B.java or C.java and run make all, A.java will be recompiled also.

Having to spell out all such dependencies is somewhat tedious, though it may be necessary especially if you are using inner classes or reflection.  On the other hand, if you are doing a really complex Java project, it may be better to use another framework entirely.  The Java-based build tool Ant, from the the Apache Ant project, is more flexible than make.  Or, consider an integrated development environment (IDE) that understands Java dependencies.  Two popular and free ones are Eclipse and NetBeans.



Author: Paul Kube © 2006