A Make Tool for the Java™ Language (Javamake)

HOW TO USE JAVAMAKE

The tool's usage is very similar to that of javac. To run it, simply invoke

java -jar javamake.jar [ options ] [ @files ]

Arguments may be in any order.

DESCRIPTION

Javamake first looks for a project database file in the current directory (the default name is javamake.pdb). If this file is not found, all of the source (.java) files are passed to the underlying Java compiler. After compilation, the tool looks for .class files corresponding to the source files. This lookup is based on source file names, so to make the tool work correctly, the name of each top-level class should be the same as its .java file name. Depending on whether or not the -d directory option was specified, a .class file is looked up in either an appropriate subdirectory of the destination directory, or in the same directory where its source file resides. In the former case, Javamake determines the subdir of the destination directory based on the source file directory name. Thus, to avoid "class file not found" errors, you should always arrange that the source file directory reflects the package name of the respective class, You should also place no more than one top-level class in a single .java file (doing the contrary is considered bad practice anyway), since Javamake currently makes no attempt to find a binary class whose name does not match the name of its source file. However, the tool is able to find all of the nested classes for each top-level or nested class, and keeps track of dependencies between such classes as well.

The essential information about classes and their sources is recorded in the project database. It includes the class name, the source file full path and current timestamp, and the information about dependencies, i.e. various references to other classes that each class contains. This information is withdrawn from the .class file and includes the superclass reference, the implemented interface list, the constant pool, the declarations of fields and methods, and some class file attributes. In fact, the dependencies are stored in the database in the form that largely reflects the class file structure, i.e. they sound like "class A extends class B, implements interface C, calls method m(D,E) of class F", and so on.

Javamake keeps track only of the classes explicitly specified to it at every invocation, either via their .java files or via the parameters of the -projclasspath option (see below). If any previously specified source file or JAR is omitted, the records for the respective top-level class(es) and all of its nested classes will be removed from the project database.

If Javamake finds an existing project database in the current directory, it first synchronizes the project database with the list of source files it is given. Records for missing sources are removed, and new sources are scheduled for recompilation. Then the timestamps of the remaining sources are compared to the timestamps of the respective classes, that are preserved in the database. Sources that are newer than their classes are recompiled, along with newly-added sources.

Next, those binary classes for which timestamps have changed are compared to their old versions preserved in the database. If any change in the new version of class C is source incompatible, for example the signature of a public method m(boolean b) is changed to m(int x), Javamake looks for old project classes that call C.m(boolean). You should have updated the sources for all such classes so that they now call C.m(int). However if you haven't done that for some classes, Javamake will force recompilation of all of them. If there are any errors, the compiler will report them.

If there are no compilation errors, the project is considered to be in the constistent state, and the project database is updated with the information on the new class versions.

Starting from release 1.3 of Javamake, it is also possible to include in the project classes that have no .java sources and are contained in JAR files. This is done with the primary goal of supporting development of hierarchically structured projects, where a top-level project consumes the output of its subprojects in the form of binary classes packed in JARs. .java sources are not provided with jarred classes, but these classes may occasionally be changed in incompatible ways. Therefore it is desirable to include such classes in the project, be able to detect incompatible changes to them and treat such changes as usual.

Javamake reads all .class files from all JARs specified via the -projclasspath command line option, adds these classes to the project and saves their dependency information in the project database as usual. A timestamp of each JAR is also saved in the project database, and whenever it changes, each class in this JAR is individually checked for updates. If an incompatible change is detected, all dependent classes are recompiled (note that it is always assumed that there are no incompatibilities between jarred classes).

Since jarred classes don't have sources that can be recompiled, none of these classes should depend on any "normal" classes that have .java sources. By default, a warning is issued if such a dependency is detected. You may use several special options to specify different behaviour.

Also starting from release 1.3, Javamake keeps track of classes that do not themselves belong to the project, but on which the project depends. These are the supertypes of project classes, that themselves are Java core classes or, say, third-party classes that you put on the class path during compilation. To enable Javamake to locate these classes, -classpath has been made an explicit Javamake option. For non-project classes no information is recorded in the project database. The only time when they are consulted is when some project class C is changed. In that case, Javamake verifies if the new version of C has the same or different set of direct and indirect superclasses and superinterfaces as the old version. Supertypes that are not a part of the project are included in this check procedure to increase its robustness. If this was not done, your code could become inconsistent if, for example, the new version of C extended a non-serializable variant of some Java core class, whereas in some other project class there is still an assignment like x = (Serializable) C;

Note on the JDK versions and Compiler Class Lookup

By default, our make tool assumes that it is invoked using the Java launcher of Sun's JDK 1.3 or higher. The javac compiler in these JDK versions is a Java application with the main class and methods called, respectively, com.sun.tools.javac.Main and static int compile(String args[]). The classes for the compiler are physically located in the javahome/lib/tools.jar archive.

If you have a version of JDK older than 1.3, or a Java Runtime Environment (JRE) installation which does not include a Java compiler, or a Java platform implementation from some other vendor, you have to find out where a Java compiler is located or obtain one. We support both compilers in the form of native executables and compilers written in Java, e.g. javac. If you use a compiler which is a Java application itself, the method of this application called by our tool should have a single argument of java.lang.String[] type corresponding to command line arguments normally passed to the compiler, and should have int return type. Zero should be returned if compilation is successful and any non-zero value if it is not. The class path, and the names for the main class and method of the compiler can be arbitrary and should be specified to the tool using -jcpath, -jcmainclass and -jcmethod options. For example, the default invocation of our tool is equivalent to:

java -jar javamake.jar -jcpath $JAVA_HOME/lib/tools.jar -jcmainclass com.sun.tools.javac.Main -jcmethod compile

Note on Changing Compile-Time Constants

In Java, a field of a class is called a compile-time constant if it is final, static and initialized with a compile-time constant expression, for example:

public static final String LOG_FILE_NAME = "mylog.log";
Unlike other fields, compile-time constants are not symbolically referenced by classes that use them. Instead, the value of such a constant is embedded directly into the bytecode of a class which references it. Thus, if the above LOG_FILE_NAME constant is defined in A.java and used (referenced) in B.java, B.class will have only the "mylog.log" string in its constant pool, but not a reference to A.LOG_FILE_NAME! If you change LOG_FILE_NAME in A.java to, say, "logfile.log" and recompile A.java, class B will still use the old vlaue "mylog.log".

Due to the above optimization in Java we, in general, are unable to deduce which classes reference a given non-private compile-time constant by analyzing class files only. We could have done that by parsing source files, but this is a complex task, so instead we currently recompile all of the classes that may reference a given primitive constant, depending on its access modifiers. In the worst case, when such a field is public, the whole project will be recompiled. We recognize this problem, but hope that changes to primitive constants are unlikely to be frequent enough to make this a major inconvenience.

OPTIONS

-h
Print the help message.

-version
Print the version of this copy of the tool.

-pdb project database file name
Specify the file to be used to store project information (default is javamake.pdb).

-d directory
Specify the directory to place generated class files. This option is exactly equivalent to the -d option of javac and is passed to the underlying Java compiler unchanged.

-classpath path
Set the user class path (note that unlike javac Javamake does not recognize the CLASSPATH environment variable). Javamake uses the user class path (along with the project class path described below, and the bootstrap/extensions class paths) to look up non-project superclasses and superinterfaces of project classes. When Javamake invokes a Java compiler, it is passed the -classpath option with the argument that is the Javamake's user class path and project class path merged.

-projclasspath path
Set the project class path (currently only JARs are allowed on this class path). Classes that are contained in JARs on the project class path are considered as special "sourceless" project classes. Javamake keeps track of changes made to them and recompiles the "normal" project classes affected by such changes, but always assumes that all sourceless project classes are mutually compatible. When Javamake invokes a Java compiler, it passes it the -classpath option with the argument that is the Javamake's user class path and project class path merged.

-Coption
Pass the specified option to the Java compiler used by the make tool. Note that if this compiler option has arguments, each of them should also be prefixed with -C. For example, passing the class path to the compiler will look like:

java -jar javamake.jar -C-classpath -Cmydir/mypackage ...
-jcpath directory or .jar file
Specify the class path for the Java compiler (default is javahome/lib/tools.jar).

-jcmainclass class name
Specify the main class for the Java compiler (default is com.sun.tools.javac.Main).

-jcmetod method name
Specify the method to call in the main Java compiler class (default is compile). The method should accept a single argument of java.lang.String[] type corresponding to the command line arguments and return an int type value. This value should be 0 if compilation is successful, and non-zero if any compilation errors were detected.

-jcexec executable compiler application
Specify the Java compiler which is a native executable application. This option is incompatible with -jcpath, -jcmainclass or -jcmethod.

-failondependentjar
Specify that Javamake should produce an error message and stop if a class in a JAR depends on a class with .java source (by default, a warning is issued).

-nowarnondependentjar
Specify that Javamake should silently ignore a detected dependency of a jarred class on a class with a .java source. This option should be used with care, since it effectively allows Javamake to leave the project in an inconsistent state without even notifying you.

-warnlimit maximum number of warnings
Specify the maximum number of warnings that the tool can print. The default value is 20.

-bootclasspath path
Specify a non-standard location for bootstrap classes (by default, the value of the sun.boot.class.path Java property is used). Javamake looks up non-project supertypes of project classes on the boot class path, and also passes it to the Java compiler.

-extdirs directories
Specify a non-standard location for extensions (by default, the value of the java.ext.dirs Java property is used). Javamake looks up non-project supertypes of project classes in the JAR and ZIP archives in the specified directories, and also passes this option to the Java compiler.

To shorten or simplify the tool invocation command, you may specify one or more files that themselves contain file names and/or options separated by blanks and new lines. On the command line, use the '@' character with the filename to specify it as a file list. When the tool encounters an argument beginning with the character '@', it operates on the file names and options in that file as if they had been on the command line. For example, you can list all of the options and source file names in a file named jmakefile. This file might look like:

-d myclasses -classpath myclasses -projclasspath myjars/x.jar
A.java
B.java
C.java
You could then run Javamake as:

java -jar javamake.jar @jmakefile

Example

The rules of using various options when invoking Javamake may seem a little confusing, since when you run our tool, you effectively invoke three applications in turn - java, javamake and javac (or some other Java compiler). Both javamake and javac are applications written in Java and executed by the java launcher. Each of the three applications has its own options. However, at least one option name (-classpath) is the same for java and javac. Another option, -d is used in the same way by both the make tool and the compiler, and is passed unchanged from the former to the latter. @-files are understood by both javac and javamake.

Here is an example which may help to clarify the use of options when switching from javac to our tool. Suppose that you previously used javac as follows:

javac -J-Xmx128m -classpath myclasspath -sourcepath mysourcepath -d myoutpath @mysourcelist
The first option passed to javac is prefixed with the -J flag and is thus actually used by the java launcher. It specifies the maximum size of memory allocation pool. The rest of the options are used by javac. To change the above command so that javamake is used, you should pass the first option directly to java, and prefix the rest of them (except -d and its argument, -classpath and its argument, and @mysourcelist) with -C, so that they are passed correctly to javac. Make sure that there are no spaces between -C and the actual compiler option. The result will look like:
java -Xmx128m -jar javamake.jar -classpath myclasspath -C-sourcepath -Cmysourcepath -d myoutpath @mysourcelist

FEEDBACK

Your feedback, particularly if you spot a bug or have a suggestion for improvement, is welcome. Please send e-mail to Misha Dmitriev.