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.