Makefiles for Java

There are quite a few built tools to help you compile Java code. Some of the most commonly used include Apache Ant, Apache Maven and Gradle. It is however perfectly feasible to use GNU make to build Java projects.

Makefile setup

Create a file called Makefile in the root of the project directory, with the following contents:

.SUFFIXES:

SOURCE = src
OUTPUT = build
CLASS_PATH = $(SOURCE)

sources = $(shell find $(SOURCE) -type f -name '*.java')
classes = $(sources:$(SOURCE)/%.java=$(OUTPUT)/%.class)
build_dirs = $(sort $(dir $(classes)))

all: $(classes)

$(build_dirs):
        mkdir -p $@

$(OUTPUT)/%.class: $(SOURCE)/%.java | $(build_dirs)
        javac -cp $(CLASS_PATH) -d $(OUTPUT) $<

run: $(classes)
        cd $(OUTPUT); java com.example.Main

clean:
        rm -vrf $(OUTPUT)/

The makefile above assumes the following:

  • GNU make is installed
  • java and javac are installed and available
  • The find command is available
  • Java source code is being kept under a directory called src/

Using make

Before anything is built only the Java source code and makefile will exist:

$ tree
.
├── Makefile
└── src
    └── com
        └── example
            ├── Bar.java
            ├── Foo.java
            └── Main.java

3 directories, 4 files

Running make will call javac to compile the source code:

$ make
mkdir -p build/com/example/
javac -cp src -d build src/com/example/Foo.java
javac -cp src -d build src/com/example/Bar.java
javac -cp src -d build src/com/example/Main.java

The class files will be created under the build directory:

$ tree
.
├── build
│   └── com
│       └── example
│           ├── Bar.class
│           ├── Foo.class
│           └── Main.class
├── Makefile
└── src
    └── com
        └── example
            ├── Bar.java
            ├── Foo.java
            └── Main.java

6 directories, 7 files

Running make run will run the Java program:

$ make run
cd build; java com.example.Main
Hello World!

And finally running make clean will completely remove the build directory:

$ make clean
rm -vrf build/
removed ‘build/com/example/Foo.class’
removed ‘build/com/example/Bar.class’
removed ‘build/com/example/Main.class’
removed directory: ‘build/com/example’
removed directory: ‘build/com’
removed directory: ‘build/’

Makefile explanation

Makefiles can be a little cryptic. Below is a quick explanation for each section of the makefile:

Setting .SUFFIXES to nothing removes all known suffixes. This prevents make trying to run default suffix rules:

.SUFFIXES:

The source directory, build directory and class path variables are set next:

SOURCE = src
OUTPUT = build
CLASS_PATH = $(SOURCE)

Next a list of Java source files is created. This is done using the shell function to call find:

sources = $(shell find $(SOURCE) -type f -name '*.java')

The sources list is then used to create two additional lists; a list of class file paths and a list of directories. This is done using text and file name functions. Note that sort is used to remove any duplicate build directories.

classes = $(sources:$(SOURCE)/%.java=$(OUTPUT)/%.class)
build_dirs = $(sort $(dir $(classes)))

The first target not prefixed with a dot (.) will be the default goal. In this case a phony target is used to create all Java classes:

all: $(classes)

The next target creates the build directories. The recipe uses an automatic variable ($@) to avoid hard coding directory names.

$(build_dirs):
        mkdir -p $@

The next target uses a pattern rule to build class files using javac. The pipe character is used to make sure the directories listed in build_dirs are order only prerequisites; without this Java classes will be rebuilt unnecessary if build directories are modified.

$(OUTPUT)/%.class: $(SOURCE)/%.java | $(build_dirs)
        javac -cp $(CLASS_PATH) -d $(OUTPUT) $<

The run target switches into the build directory and runs the com.example.Main class. It's worth noting that setting the class path could be used as an alternative to changing directory.

run: $(classes)
        cd $(OUTPUT); java com.example.Main

Finally a clean phony target is defined to make it easy to remove any compiled classes:

clean:
        rm -vrf $(OUTPUT)/