Building jlox with Gradle
I just started following along with Bob Nystrom’s Crafting Interpreters book a few days ago, and one of the first things I did was set up a Gradle project for the jlox interpreter. It’s a simple Java application with no external dependencies, so the build script is hardly interesting, except for one thing: starting in Chapter 5, there is a GenerateAst “script” needs to be run before compiling, because it generates source files. Turns out, it’s pretty easy to add a code generation task to a Gradle build.
The first step is to create a
buildSrc
directory, and move GenerateAst.java into it, following the normal
conventions for Java projects – that is, put the source file in
buildSrc/src/main/java/com/craftinginterpreters/tool/. Now you can use
the GenerateAst
class from your build.gradle file, but its interface
isn’t really suited for being used programmatically. It has a standard
main
method that treats the first argument as a path to the output
directory, and it passes that path as a String
to the defineAst
method,
like so:
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("Usage: generate_ast <output directory>");
System.exit(1);
}
String outputDir = args[0];
defineAst(outputDir, "Expr", Arrays.asList(
// snip ...
}
private static void defineAst(
String outputDir, String baseName, List<String> types)
throws IOException {
String path = outputDir + "/" + baseName + ".java";
PrintWriter writer = new PrintWriter(path, "UTF-8");
// snip ...
}
Since we’ll be using GenerateAst
from a Gradle script instead of from the
command line, we can just pass in a
File
object
for the output directory, instead of using path strings. Then we can use
File
’s constructors and methods to create the subdirectories and files
that we need.
// this replaces main(String[] args)
public static void run(File outputDir) throws IOException {
File packageDir = new File(outputDir, "com/craftinginterpreters/lox");
packageDir.mkdirs();
defineAst(packageDir, "Expr", Arrays.asList(
// snip ...
}
private static void defineAst(File outputDir, String baseName,
List<String> types) throws IOException {
File outputFile = new File(outputDir, baseName + ".java");
PrintWriter writer = new PrintWriter(outputFile, "UTF-8");
// snip ...
}
Now we need to call GenerateAst.run
from our build.gradle, and pass it
an appropriate output directory. The project’s build directory is a good
place for generated files, so we’ll use a subdirectory under that.
def generatedSrcDir = new File(buildDir, "generated/src/main/java/");
task generateAst {
doLast {
GenerateAst.run(generatedSrcDir);
}
}
Running the generateAst
task will create Expr.java under the
build/generated/src/main/java/ directory, so now we just need to add
that directory to the main source set, and run the new task before
compiling.
compileJava.dependsOn(generateAst)
sourceSets {
main {
java {
srcDirs += generatedSrcDir
}
}
}
And that’s it. Now the code generator will run for every build. If you’re
using an IDE, then you’ll also want to run the generateAst
task manually
after making changes to GenerateAst.java, so the changes to the generated
classes will be visible.