Using IntelliJ IDEA's javac2 in Gradle
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.)