JetBrains’s IntelliJ IDEA uses a wrapper around the Java compiler, named javac2, to provide additional support for compiling .form files produced by the IDE, and for processing @Nullable and @NotNull annotations. It is naturally supported inside IDEA itself, and also by Apache Ant. However, other build systems like Gradle do not support it out of the box. Supporting it is useful - e.g. if you wanted to run a continuous integration server, which means you cannot use IDEA for building, and want it to compile your forms. Also, it could be useful if other developers on the same project used a different IDE.

Using Gradle’s integration with Apache Ant, it is possible to add javac2 support using the javac2 Ant task provided by JetBrains. This solution is by no means perfect - it doesn’t completely integrate with Gradle - but it did enough to meet my requirements.

I added a new configuration, antTask, so that javac2 does not pollute the classpath of the project itself:

configurations {
    antTask
}

Unfortunately the latest version of javac2 is not available in the Maven central repository, so as a workaround I added a flatDir repository to load the jars from a local directory (in this example, the directory lib/ below the root of the project:)

repositories {
    flatDir dirs: "${rootDir}/lib"
    mavenCentral()
    /* any other repositories... */
}

javac2 also depends on a customized version of ObjectWeb ASM, the IDEA forms runtime and JDOM 1.x, which can be expressed like so:

dependencies {
    antTask name: 'javac2', version: '12.1.0'
    antTask name: 'forms_rt', version: '12.1.0'
    antTask name: 'asm4-all', version: '12.1.0-idea'
    antTask group: 'org.jdom', name: 'jdom', version: '1.1'
}

There’s some flexibility in what you call the first three dependencies, as they are loaded locally. I chose to give them the same version numbers as the IDE, with a suffix of -idea on the ASM dependency to indicate this is a modified version of ASM.

These files must be copied over from IDEA’s directory like so (where $IDEA_ROOT is the path to your IDEA installation:)

mkdir lib
cp $IDEA_ROOT/redist/javac2.jar lib/javac2-12.1.0.jar
cp $IDEA_ROOT/redist/forms_rt.jar lib/forms_rt-12.1.0.jar
cp $IDEA_ROOT/lib/asm4-all.jar lib/asm4-all-12.1.0-idea.jar

The fourth and final dependency, JDOM, can be found in the Maven central repository and has not been modified by JetBrains, so it doesn’t need to be copied over from the IDEA directory.

Finally, the compileJava task needs to be overwritten to use javac2 via the Ant task:

task compileJava(overwrite: true, dependsOn: configurations.compile.getTaskDependencyFromProjectDependency(true, 'jar')) {
    doLast {
        project.sourceSets.main.output.classesDir.mkdirs()
        ant.taskdef name: 'javac2', classname: 'com.intellij.ant.Javac2', classpath: configurations.antTask.asPath
        ant.javac2 srcdir: project.sourceSets.main.java.srcDirs.join(':'),
            classpath: project.sourceSets.main.compileClasspath.asPath,
            destdir: project.sourceSets.main.output.classesDir,
            source: sourceCompatibility,
            target: targetCompatibility,
            includeAntRuntime: false
    }
}

The overwrite flag ensures the default task is replaced with the new one.

In a multi-module project, to ensure that a module is compiled after any modules it depends on, the getTaskDependencyFromProjectDependency method is used to populate the dependsOn property. It converts the list of module dependencies to a list of jar tasks for those modules. For example, if a module dependend on moduleA and moduleB, then the function would return moduleA:jar and moduleB:jar. This behaviour is required because the dependencies need to be added to the classpath of javac, and for that they need to have been compiled.

Ant will not create the output directory itself, so there’s a call to mkdirs() to do this before running the Ant task.

The ant.taskdef and ant.javac2 lines essentially map directly to <taskdef> and <javac2> in Ant’s XML syntax. The taskdef is used to load the javac2 task from the antTask configuration. The call to javac2 immediately following this is what actually does the compiling work. The arguments passed to it are exactly the same as the options the standard <javac> task accepts, so it shouldn’t need much explaining. Their values are taken from Gradle’s project object, so it’ll work even if you’ve changed your paths around.

There is some scope for future work, for example extending it to also compile test sources with javac2, or converting it to a plugin. However, I didn’t need these features, so I chose to go for a simple implementation instead (and I don’t know enough about the Gradle internals yet either!)

Credit should go to Douglas Bullard, who posted the original code for integrating javac2 with Gradle on the JetBrains forum, which this blog post improves upon (by removing the hard-coded paths, adding multi-module support and simplifying it.)