Writing a Java Build File
Java and the java compilers pose an interesting problem for any build
system. When compiling other languages you compile a single
source file into a single object file in separate calls to the
compiler. Then you link them all together into the final
product. With java the compiling takes place with one
command. You pass all the files you want it to compile and it
compiles them and then some. Then there is the dependency
problem, how do you know what files need to be rebuilt and how do you
get just that list of files to the compiler? CPMake has the
answer as I will show you here.
In this tutorial I will use BeanShell to write a build file for CPMake
that builds a simple Java application using the Jikes compiler.
Now lets get started.
In order to keep my build files generic I put all the project specific
stuff up at the top as variables.
blddir = "build";
srcdir = "src";
jardir = "jar";
jarfile =
jardir+"/javatutorial.jar";
This sets up where to put the class files, the jar file and where the
source is located.
The next step is to create a list of the source files and potential
class files like so:
sourceFiles =
make.createFileList(srcdir, ".*\\.java", (make.INCLUDE_PATH |
make.RECURSE | make.RELATIVE_PATH));
classFiles =
make.substitute("(.*)\\.java", blddir+"/$1.class", sourceFiles);
Here I make use of some CPMake convenience functions. The first
creates a string array of file names in the source directory that match
the given regular expression (in this case all java files). The
second call creates a list of the potential class files by substituting
the names in the list of source files.
It is also important to set a search path for your java files.
This will help CPMake when it tries to find dependencies.
make.addSearchPath(".*\\.java",
srcdir);
In this project I have two directories "build" and "jar" that need to
be created. CPMake has built in rules for creating directories I
just have to tell it to do so like this:
make.createDirectoryRule(blddir,
null, true);
make.createDirectoryRule(jardir,
null, true);
The second parameter is if there are any prerequisites that need to be
done before creating this directory. The last parameter tells
CPMake to verify that the directory was created.
Now things get interesting. With java source files you don't want
to call the compiler separately for each file. This is because
the compiler will go and compile other class files, as the need arises,
when compiling the one you told it to. It has to do this to
verify syntax. So by calling the compiler once for each java file
you could potentially compile every file in the project one time for
each file. So if you had 50 files you could compile them all 50
times.
With CPMake this is not a problem. The trick here is to only give
the compiler a list of files that are out of date. Also make sure
that the old class file is removed so the compiler cannot use it when
compiling other files. This is all done with a rule that looks
similar to what I did in the C++ tutorial
make.createPatternRule(blddir+"/(.*).class",
"$1.java", "removeClass", false);
This rule sets up a pattern dependency between a class file and its
corresponding java file. The method to call is "removeClass" and
the last parameter tells CPMake to not verify that the target was
built. This is needed because in fact the target will not be
built until later. So what does removeClass do? Here it is:
removeClass(String target,
String[] prereqs)
{
print(prereqs[0]);
rm(target);
compileList +=
prereqs[0]+" ";
}
This prints the java file that will be compiled and then calls a
BeanShell routine that deletes the target which in this case is the
class file. Last the java file is appended to a list that will be
handed to the compiler later.
Now CPMake has already determined that this class file needs to be
rebuilt or else it would not have called this rule. By removing
the class file we ensure that 1. It will get built and 2. it will not
be incorrectly used when compiling other files.
Now for the rule that is going to do the work.
make.createPhonyRule("compile",
blddir+" "+make.arrayToString(classFiles), "compile");
This has to be a phony rule because the output is actually what the
output of the last rule was supposed to be and that is the class
files. Because you cannot have two rules for a single target this
one has to be phony which is ok. The prerequisites of this rule
are the build directory and the class files. That way the above
pattern rule will be triggered before this one is.
Here is the compile method:
compile(String target, String[]
prereqs)
{
cmd = "javac
-classpath "+blddir+" -d "+blddir+" -sourcepath "+srcdir+"
"+compileList;
make.exec(cmd,
true);
}
The magic here is that we are using the compileList variable that was
put together in the above pattern rule. This list will only
contain those files that need to be built. You may find that it
build more file then just the one you updated. That is because
CPMake does automatic dependency checking in the background. It
parses the class files to see what other class files are used. So
if you update a file it will rebuild all files that use that one.
This lets you find errors because of an interface change at compile
time instead of at run time.
Next is a rule for creating the jar file.
make.createExplicitRule(jarfile,
"compile "+jardir, "createJar", true);
I made sure to add the phony "compile" rule to the prerequisites
list. The createJar method looks like this:
createJar(String target, String[]
prereqs)
{
print("Creating "+target);
cmd = "jar
-cfm "+target+" manifest.txt -C "+blddir+" .";
make.exec(cmd);
}
The command here is simple jar up everything in the build directory
along with the manifest file and jar it into target which happens to be
our jar file.
The last rule is to test the program.
make.createPhonyRule("test",
jarfile, "test");
test(String target, String[]
prereqs)
{
print("Running
test");
cmd = "java
-jar "+jarfile;
make.exec(cmd);
}
And finally we set the default target for this build file:
make.setDefaultTarget(jarfile);
And there you have it. A very simple build file that will build
almost any java application. The only thing you have to do is
change the variables at the top of the file.
As always for your viewing pleasure here is the build file in its
entirety
1:blddir = "build";
2:srcdir = "src";
3:jardir = "jar";
4:jarfile = jardir+"/javatutorial.jar";
5:
6:sourceFiles = make.createFileList(srcdir, ".*\\.java",
7: (make.INCLUDE_PATH | make.RECURSE | make.RELATIVE_PATH));
8:
9:classFiles = make.substitute("(.*)\\.java", blddir+"/$1.class", sourceFiles);
10:
11:compileList = "";
12:
13://Set source search path
14:make.addSearchPath(".*\\.java", srcdir);
15:
16://Rule for directories
17:make.createDirectoryRule(blddir, null, true);
18:make.createDirectoryRule(jardir, null, true);
19:
20://Rule that removes out of date class files
21:make.createPatternRule(blddir+"/(.*).class", "$1.java", "removeClass", false);
22:removeClass(String target, String[] prereqs)
23: {
24: print(prereqs[0]);
25: rm(target);
26: compileList += prereqs[0]+" ";
27: }
28:
29://Compile class files
30:make.createPhonyRule("compile", blddir+" "+make.arrayToString(classFiles), "compile");
31:compile(String target, String[] prereqs)
32: {
33: cmd = "javac -classpath "+blddir+" -d "+blddir+
34: " -sourcepath "+srcdir+" "+compileList;
35: make.exec(cmd, true);
36: }
37:
38://Rule to create jar file
39:make.createExplicitRule(jarfile, "compile "+jardir, "createJar", true);
40:createJar(String target, String[] prereqs)
41: {
42: print("Creating "+target);
43: cmd = "jar -cfm "+target+" manifest.txt -C "+blddir+" .";
44: make.exec(cmd);
45: }
46:
47://Rule to test program
48:make.createPhonyRule("test", jarfile, "test");
49:test(String target, String[] prereqs)
50: {
51: print("Running test");
52: cmd = "java -jar "+jarfile;
53: make.exec(cmd);
54: }
55:
56:make.setDefaultTarget(jarfile);