A tool to help
make play nicely together
jdep version 1.4, dated 19-July-2016.
Jdep is a tool for analyzing Java
.class file dependencies, so that the
peculiar compilation behavior of many Java compilers can be tamed to be
compatible with conventional Unix style Makefiles.
The official source and documentation for jdep is maintained at:
https://github.com/FUDCo/jdep (i.e., here)
At the moment, I'm only distributing this tool in source form, open sourced under the MIT license.
Bug reports, flames, questions and other feedback should be directed to firstname.lastname@example.org
jdeprepo from https://github.com/FUDCo/jdep
maketo compile the package. It will produce two executable files in the
touchp(the latter is actually just a simple shell script).
Copy the executables to wherever you put your installed executables.
This tool has been tested on and is known to work on (and indeed is used regularly on) every version of SPARC Solaris, Linux, FreeBSD, and Mac OSX I've encountered as of this writing. It will work with all Sun Java JDKs up through JDK 8 (which is the most current version as of this writing).
More generally, I see no reason why
jdep shouldn't be able to build and run
without modification on any sane Unix platform or system that supports the Unix
file API, as it doesn't do anything exotic at the OS level. But I don't make
any promises, and the usual disclaimers (which are more verbosely and
legalistically explicated in the license terms included in the source
You are welcome to make whatever use of this code you like, but if you fix any bugs or tweak it to work on a wider range of platforms, please do let me know and I'll be happy to roll your changes back into the main distribution.
What The Heck Is This?
(You don't need to read this section if all you want to know is how to use the tool. You can skip ahead to the next section. On the other hand, it's not much use knowing how to use the tool unless you understand the problem it's trying to solve, so you might want to read this section anyway, even though it's a bit long and rambling.)
This is my answer to the problem that Sun's Java compiler,
javac does not
play nicely with
make. This is also a problem with any other Java compiler
which attempts to emulate the behavior of
javac, such as that provided with
OpenJDK. This is unspeakably irritating to those of us who would like to use
the Unix command line tools as our principal development environment to develop
Java code the same way we have always developed C or C++ or FORTRAN code or
indeed code in just about any compiled language except Java.
Unlike, for example, C, Java does not have header files. This is good, in that it defines out of existence an entire class of C development bugs, wherein a header and the source file(s) it corresponds to get out of sync with each other. Instead, Java classes simply import the other classes they depend on. However, this means that to compile a Java class you need to have compiled the classes that it imports. But this introduces a couple of problems.
The first problem is that classes may be mutually dependent. That is, class X
may import class Y while at the same time class Y imports class X. The
compiler copes with this by requiring you to compile such classes together,
i.e., in a single invocation of the compiler. This effectively precludes
separate compilation in the sense that we have traditionally come to know it,
where we compile each source file individually and then link the resulting
files when done. This joint compilation requirement is the first step to making
javac incompatible with
The second problem is more subtle and happens when class A imports class B but
class B does not import class A. The authors of
javac apparently recognized
that Java's importation rules hindered the operation of
make to a degree, so
they incorporated a bunch of make-like behavior into
javac itself. Thus,
if you attempt to compile class A and it requires the importation of class B,
javac will go looking for
B.class. If it fails to find it, it will then
B.java and implicitly (and silently) include it in the compilation.
This helpful behavior means that classes can get mysteriously recompiled when
you are not expecting it, resulting in all manner of surprising mayhem. The
mayhem ensues because although it understands to recompile B if necessary when
compiling A, it doesn't know to recompile A when B changes (though knowing when
to do the latter is the fundamental mission of
make in the first place).
Attempting to reconcile this implicit compilation behavior with a set of file
dependencies that one can put into a makefile eventually reduces one to hair
pulling and ultimately to gibbering idiocy.
The most straightforward way out of this situation, the one which I took (until
jdep), and the approach taken by essentially everyone I know doing
Java development work using the conventional Unix toolchain, is to
simply always recompile everything whenever recompiling anything. This has the
virtue of being extremely easy to do in a makefile, as well as being absolutely
foolproof from a dependency analysis standpoint. Unfortunately, it is rather
awkward to manage in a project environment where different people are
responsible for different pieces of the source tree. Also, even with fast
computers it is painfully time consuming once a project develops into having a
large number of source files (which, in a non-trivial project, it eventually
will because Java wants you to put each class into its own source file).
After suffering with this situation for about 5 years, cursing at
the while, I finally had a classic "Aha!" experience: from the perspective of
javac does not behave like a compiler but more like a linker. It's
just that it does its job using the pre-compilation (
.java) files rather than
the post-compilation (
.class) ones. This perspective leads to the following
make strategy: "compile" a
.java file by
touching the corresponding
.class file (this updates the last-modified timestamp on the
just as actually compiling it would), then "link" by running
javac on all the
.java files whose
.class files now need to be "linked" according to
make's dependency analysis. This strategy relies on the assumption that it is
possible to have your makefile synthesize a command line by mapping a list of
.class files into a corresponding list of
.java files. Fortunately, GNU
make readily does this sort of thing (I don't know about Sun's
others, but given that we do have GNU
make there's no real reason to use
anything else anyway).
The final piece of the puzzle concerns file dependencies. When making a C
.c files will depend on the
.h files, which you can either
keep track of by hand (which is tedious and error prone), or you can have an
automated tool keep track of the dependencies for you. In the case of C this is
very easy, since all you need to do is have the C compiler produce a list of
.h files it included in the process of compiling a given
.c file. The
GNU C compiler,
gcc, does this in a very convenient way with the
command line option (or, in practice, the
-MMD option), which not only
produces a list of file dependencies but outputs it in the form of an actual
make dependency line (in a
.d file) that can be included in your makefile
The case of Java is a bit more complex. Remember, as we said, that Java
doesn't use header files. Instead, a given
.java file depends, in effect, on
.java files. There is no obvious inclusion hierarchy to follow as there
is in C. Ideally,
javac would spit out the same kind of dependency
gcc does, but it doesn't (and is unlikely ever to, in my
estimation, since this mode of use is really not what its creators intended).
Due to the several ways in which one can implicitly import a class without ever
naming it explicitly in an
import declaration, writing a tool to extract the
dependency information from the Java source files yourself is not really
practical (or rather, if you succeed in doing it you will have done a large
fraction of the work towards writing your own Java compiler). However, once
javac has run, the dependency information we need is present in the resulting
.class files, which are very easy to read (indeed, were designed to be in a
machine-friendly format). This was even easier for me, as I already had a
program to read and dump
.class files laying around that I had written years
ago when I was messing with Java compilers; it was a simple matter to modify it
into the program now called
Here is what
jdep does: it reads a batch of
.class files and produces a
corresponding set of
.d files suitable for inclusion into a GNU makefile.
.d files define the dependencies used to drive the "compilation" phase
of the make process described above, in which compilation is simulated using
touch (actually, using
touchp, but that's a minor detail we'll get to in a
jdep to enable using
By convention, the source for a Java class
Foo is placed in a file named
Foo.java. Moreover, this file is generally located according the class's
package in a file directory tree whose hierarchical structure matches that of
the overall package hierarchy. These conventions are not so much enforced by
the compiler as they are expected by it, in the sense that if you deviate from
them it will get confused and not always do the right thing. Consequently, we
assume you will continue to follow these conventions when using
make. (None of this applies, of course, to IDEs, which keep track of all
this stuff in a database or project file of some kind. But if I wanted to use
an IDE I wouldn't be here. The problem with Integrated Development Environments
is they're so darned Integrated. But I digress.)
The discussion that follows describes setting up a makefile. For expository
JAVA_DIR is assumed to be set to the pathname
of the directory root of the file tree where Java source files live, while the
CLASS_DIR fulfills the same role with respect to compiled class
DEP_DIR similarly with respect to
.d files. Thus for example,
bar.baz.Foo would have its source file be taken from
$(JAVA_DIR)/bar/baz/Foo.java, while its compiled class file would end up in
$(CLASS_DIR)/bar/baz/Foo.class and its dependencies would be described in
As described in more detail in the preceding section, the key trick to making
this work is to think of compilation with
javac as the "link" phase of the
build process, and to "compile" by using
An example of how you set all this up is included in the subdirectory
In the example, we define the list of source files:
EXAMPLE_SRC = $(shell cd $(JAVA_DIR); find com -name '*.java')
find to enumerate the source files; however, you can get your list of
source files however you like, including by just manually entering them
We then define the list of class files by doing a pattern transformation on the list of source files:
EXAMPLE_CLA = $(EXAMPLE_SRC:%.java=$(CLASS_DIR)/%.class)
The list of make dependency files is defined similarly:
EXAMPLE_DEP = $(EXAMPLE_SRC:%.java=$(DEP_DIR)/%.d)
touchp. I use the following implicit
$(CLASS_DIR)/%.class: $(JAVA_DIR)/%.java touchp $@
Note that I use
touchp rather than
touch. This is a simple shell script
that is included as part of the
jdep package. The analogy is to
touchp is to
mkdir -p is to
create a zero-length file if the file being touched does not yet exist.
However, if the directory the resulting file would placed in does not already
touch will fail. In contrast,
touchp will create the directory path
down to the point needed for the
touch to succeed, just as
mkdir -p will
create an entire directory path.
javac, using a
make rule where the ultimate target depends on
the list of class files:
in this rule, execute a
javac command line like:
javac -d $(CLASS_DIR) -classpath $(CLASS_DIR) $(?:$(CLASS_DIR)/%.class=$(JAVA_DIR)/%.java)
make will bind the variable
$? to the list of class files that were newer
than the target file, i.e., the ones that got touched in the "compilation"
phase. This list of class files is converted back into a list of source files
using another pattern transformation.
Next, as a further part of the "link" rule, run
jdep to update the
for any classes that got compiled:
jdep -c $(CLASS_DIR) -j $(JAVA_DIR) -d $(DEP_DIR) $?
Finally, produce an updated version of the ultimate target file. I create a
.jar file of all the
cd $(CLASS_DIR); jar cf ../$@ `find com -name '*.class'`
but if you prefer to work with a loose collection of .
class files you could
instead just have your ultimate target be a marker file that you
Finally, be sure to include the dependency files that
jdep generated in
earlier runs of
One limitation of this approach
The scheme described here absolutely depends on the 1-to-1 correspondence
between source files and class files. However, the compilation rules for Java
permit you in some cases to violate this principle: you are allowed to place
more than one non-public (i.e., package scoped) class in a source file. Though
it's arguably a bad practice anyway, if you use
jdep you simply can't do
that at all. You can use inner classes, however, since there is enough
information in a class file to track an inner class back to its outer class,
and inner classes are nearly always preferrable to embedded package classes.
jdep Command Reference
jdep option... file...
Each file should be a Java
.class file, which may be specified either with
or without the trailing
.class portion of the name.
The program accepts the following options:
Include all packages in the dependency information generated. By
jdep will omit packages whose package names begin
com.sun. since these
contain library classes that your makefile won't know how to build anyway.
Exclude the package package from the dependency information
generated. This option may be specified as many times as you wish to exclude as
many packages as you wish. Use this to exclude library packages in addition to
the defaults mentioned in the description of the
Include the package package in the dependency information generated.
This option may be specified as many times as you wish to include as many
packages as you wish. If this option is not used,
jdep will include
all packages not explicitly excluded with the
-e option. However, if
at least one
-i option is specified, then
jdep will only
include those packages it was specifically told to include.
Print a summary of the command options and then exit.
Use cpath as the base directory for
.class files. This path
will be used to locate class files for inner classes.
Use jpath as the base directory for
.java files. This path
will be prepended to source file paths in the resultant dependency
Use dpath as the base directory for
.d files. This path will
be used to generate the output file pathnames for the various
dependency files which
Added the `-e' command line option to explicitly specify which packages will be excluded from analysis.
Added the `-i' command line option to explicitly specify which packages will be included in the analysis.
Cleaned up the text of the usage information output by the `-h' option.
Changed the license from the GPL to the MIT license.
Migrate onto GitHub.
There should be a proper man page for
touchp is kind of a hack. It would be great if it could be dispensed with,
perhaps if somebody were to add the
-p flag to
touch (hint, hint).