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);