Skip to content
Francesco Lattanzio edited this page Sep 18, 2016 · 8 revisions

The solution

From the analysis of the problem we came to the conclusion that for Java and GNU Make to work together nicely we need:

  • a tool to extract inter-classes dependencies
  • a means to compile all the files that require compilation in a single invocation of javac

NOTE: What follows is the description of one possible solution to the problem of building Java projects with GNU Make: the best I could find.

Dependencies extraction

When a C/C++ source file is compiled, no other file is parsed nor compiled, except those explicitly included in the source file by means of the #include directive.

In this case the prerequisites of the object file are the source file and all the files (recursively) included. Whenever one of those files change, the object file must be rebuilt.

The simplest way to collect the prerequisites is to run the C/C++ compiler or preprocessor with special options specifically designed to write recipe-less rules to be added to the Makefile. For each file encountered during recursive parsing there will be a target with as many prerequisites as #include directives.

With Java, we don't have an #include directive, but the import directive, which is quite different -- it just tells the compiler what classes the current source code could need to access. Moreover, it can accept wildcards, which makes finding the complete list of referenced classes even more difficult.

Until JDK 1.8 there were no official tool to extract/analyse dependencies. But now, we have jdeps, which, although far from perfect, it's good enough for what we need.

I've tried other tools (I can't remember which), but I've dropped them, because:

  • most of them were meant to interact with human beings, so they only had a GUI where the result of their analysis was printed, or
  • in the rare cases of tools writing their analysis into standard output, such output was too difficult to parse -- it still was intended to be read by human beings

Given proper options, jdeps will output inter-class dependencies, one per line. Thus, piping that output through sed allows us to convert this information into a convenient format without too much ado.

Note however that jdeps outputs a list of classes, but what we really want is a list of source files. This issue can be addressed without too much effort, for class names are computed from their respective source files with an easily reversible algorithm.

Three-steps compilation

Compiling all the source files in a single run requires to split the process into three steps:

  1. build a list of the source files to compile
  2. compile the source files in the list
  3. continue processing the target(s) that triggered the source files to be compiled

The first step is implemented by rules like the followings:

.PHONY: java_list

java_list:
        @: >$@

classes:
        mkdir $@

xclasses: | classes
        ln -s $| $@

xclasses/%.class: | java_list xclasses
        echo $< >>java_list

xclasses/dummy/A.class: dummy/A.java dummy/Dep.java

Where the last rule is repeated for each Java source files and it will specify, as prerequisites, the source file itself and all the source files it depends on (which we extracted by means of jdeps). It must have no recipe, as it is specified in the previous rule: for it is the same for all of them, we specify it once in a pattern rule.

The first two rules' purpose is to empty the "java_list" file every time make is run -- this will prevent to append source files to a list containing source files from a previous run. The pattern rule's prerequisite -- | java_list -- will ensure that the list is emptied before we start appending source files.

The third and fourth rules are used to build the "classes" and "xclasses" directories -- they will hold the classes compiled from the source files. Note that "xclasses" is a symbolic link to "classes". I'll explain its purpose soon.

The second step is implemented by the following rules:

.PHONY: run-javac

run-javac:
        if test -s java_list; then \
          javac -d classes @java_list; \
        fi

run-javac: xclasses/dummy/A.class

Where the last rule is repeated for each Java source file to be compiled. run-javac's recipe will compile the files listed in "java_list" and put the resulting classes into the "classes" directory. Note that run-javac is a "phony" target, i.e., it won't build any file named "run-javac".

At last, the third step:

classes/dummy/A.class: run-javac;

some.jar: classes/dummy/A.class

some.jar:
        jar cvf $@ $(addprefix -C classes ,$(patsubst classes/%,'%',$^))

The first two rules must be repeated for each Java source file and must not include any dependencies from jdeps. The last rule will build a JAR archive with the classes specified as prerequisites.

Now, why do we need the "xclasses" symbolic link? If you remember from the problem analysis, make will process each rule only once. So we cannot process:

classes/dummy/A.class: dummy/A.java dummy/Dep.java

and:

classes/dummy/A.class: run-javac;

in the same make run. Yet, we need both: the first to fill the "java_list" file with source files to be compiled and the second to trigger the run-javac every time we need a class file.

Maybe we could get rid of the second rule, implementing the third step like the following:

some.jar: run-javac
        jar cvf $@ $(addprefix -C classes ,$(patsubst classes/%,'%',$^))

This may seem a better solution, but there are several drawbacks:

  1. the automatic variables for prerequisites -- i.e., $^, $?, etc. -- would became useless, forcing us to resort to some nasty trick
  2. being run-javac a phony target, it would force unconditional execution of some.jar's recipe
  3. even if we fix the previous issue -- e.g., making "run-javac" an actual file and updating it every time javac is run -- in a big Java project with several JAR archives, every time a single Java source file is compiled all the JAR archives will be rebuilt, even if they don't depend on the compiled class file

Giving two names to each class file -- one under the "xclasses" directory and one under the "classes" directory -- allow us to write two distinct rules that work on the same class file.

NOTE: A previous implementation of make4java used recursive make invocations to fix those issues. It made the Makefile somewhat difficult to read and understand, not to mention slower -- it required make to be run three times, one for each step.

CC0
To the extent possible under law, Francesco Lattanzio has waived all copyright and related or neighboring rights to A Makefile for Java projects. This work is published from: Italy.

Clone this wiki locally