Thomas Broyer
Posted on July 28, 2020
In the previous post, I explored how J2CL is used in Bazel. Starting with this post, with this knowledge, I'll try to design a Gradle plugin for J2CL.
Building blocks
Let's start with the low-level building blocks.
All the J2CL (GwtIncompatibleStrip
and J2clTranspile
)
and Closure compiler (CommandLineRunner
) tools
can easily be called from Java processes
(the Bazel workers actually call different, sometimes internal, APIs),
so they could be called from Gradle workers.
It also shouldn't be a problem to call the J2CL tools incrementally,
only processing changed files;
well, actually, it won't be a problem for the GwtIncompatibleStrip
which processes files one by one in isolation;
the J2clTranspile
however, just like javac
,
would also need to reprocess other Java files referencing the changed files,
so making it incremental would mean knowing about those dependencies;
let's leave this optimization work for later
(FWIW, the way Bazel deals with it is to not do any incremental/partial processing of any kind,
but instead using small sets of files, generally at the Java package level).
That means we'd have to declare 3 configuration
s (for the 3 tools' dependencies),
and we could create corresponding tasks with proper inputs and outputs.
That would work for project sources though, but not external dependencies.
For those, we could probably use artifact transforms,
but that excludes compiling the source files with javac
,
at least if we want to leverage the Gradle standard JavaCompile
task
(I've been floating the idea of an ASM-based GwtIncompatibleStrip
,
which would solve the problem then,
as an artifact transform of the binary JAR).
Let's keep external dependencies for later though.
Speaking of the JavaCompile
tasks,
for J2CL we'd want to configure their bootstrapClasspath
to the Java Runtime Emulation JAR.
Unfortunately, -bootclasspath
can only be used when compiling for Java 8,
so that rules out using any Java 9+ syntax: private methods in interfaces, var
for type inference, etc.
For those more recent Java versions, we'd want to use --system
,
but that's an entirely different format,
and one that's not even supported by Bazel yet, let alone produced by J2CL
(though maybe it's not that hard to create from the Java Runtime Emulation JAR).
Fwiw, Google also faces the same issue for Android for adding Java 9+ syntax support,
as well as J2ObjC,
so we can be sure they'll find a solution
(they're actually already working on it).
Tests
For tests, the annotation processor and its @J2clTestInput
have been designed as implementation details,
hidden behind a j2cl_test
rule in Bazel.
Contrary to Bazel where one j2cl_test
rule only runs a single test class (which could actually be a test suite),
with a gen_j2cl_tests
macro to generate them automatically from source files using a naming convention,
in Gradle we'll have a single task for the whole src/test
.
We could however have a task that generates a JUnit suite, annotated with @J2clTestInput
,
and referencing the test classes, using a naming convention,
and then processes it with the annotation processor.
This will generate some Java code to be transpiled with J2CL.
This phase can reuse the J2clTranspile
task from above,
using the src/test/java
and the generate Java code all at once.
The test_summary.json
file then needs to be processed
and fan out one Closure compilation per JS entrypoint
(one per non-suite test class, with .testsuite
file extension).
This cannot reuse the ClosureCompile
task though:
we want a single task driving multiple Closure compilations.
The result will thus be several JS applications;
we'll put them into separate directories (named after the original Java test),
and generate an additional HTML page to run them.
The task could be made incremental, only recompiling tests that have changed,
but that would need dependency information between files
(that can be extracted using Closure, with some additional work;
or possibly even just analyzing the goog.provide
/goog.require
).
BTW, that knowledge could possibly also be used by the ClosureCompile
task
to skip compilation if a file has changed that's not needed by anything.
Finally, we'll need to run those generated tests in browsers. The best way to do that is through Selenium WebDriver. We'll thus want a task taking those directories of compiled tests, that starts an HTTP server to serve them, and loads each of them in a web browser through WebDriver, generating reports (that helps skipping the task if no input has changed).
Putting it all to work
For an application like the HelloWorld sample from the Bazel repository, wiring all those tasks together would lead to a graph like the following:
The code for these tasks and sample project is available on Github.
tbroyer / gradle-j2cl-plugin
Gradle J2CL Plugin
Next steps
Now that we have the building blocks and are able to wire them together for a simple example, the next steps will be:
- handling external dependencies (stripping and transpiling them on-the-fly)
- handling project dependencies (a library subproject would expose its transpiled sources to an application subproject)
- defining conventions to make things as simple as applying a Gradle plugin (a J2CL application would be the easiest, as libraries could target J2CL, GWT, the JVM, J2ObjC, etc.)
Posted on July 28, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.