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