-
Notifications
You must be signed in to change notification settings - Fork 1
Design
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.
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.
Compiling all the source files in a single run requires to split the process into three steps:
- build a list of the source files to compile
- compile the source files in the list
- 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:
- the automatic variables for prerequisites -- i.e.,
$^,$?, etc. -- would became useless, forcing us to resort to some nasty trick - being
run-javaca phony target, it would force unconditional execution ofsome.jar's recipe - even if we fix the previous issue -- e.g., making "run-javac" an
actual file and updating it every time
javacis 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
makeinvocations to fix those issues. It made theMakefilesomewhat difficult to read and understand, not to mention slower -- it requiredmaketo be run three times, one for each step.

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.