Supporting Java Modules in Guix

by Julien Lepiller — Mon 02 October 2023

I recently wanted to update our JOSM package, the OSM editor written in Java. The newer version had slightly different dependencies than the previous one, and required me to update the build system. I wanted to write a little something to explain what I did and maybe give you a bit more understanding of some of the details.

The Problem

Currently, the default Java compiler is Java 8 (icedtea version 3 as it's called in Guix). Since we try to bootstrap our packages, meaning we do not want to rely on any binary version outside what we can build ourselves, Java is a bit of a challenge.

Most packages build with Maven, but Maven requires quite a lot of dependencies. Although we have a maven-build-system, which can use Maven to build packages, it's still quite hard to use as it misses many commonly-used plugins. It can currently only use the essential plugins (maven-compiler-plugin and friends).

Maven itself has a lot of dependencies that would normally be built with Maven, but as long as we don't reach Maven in the dependency graph, it's not possible to build use it. So, how do we build Maven, if we can't use it to build its dependencies?

One of the older build systems it Ant, which is easy to build and doesn't have dependencies. So, the first build-system we created for the Java ecosystem is the ant-build-system which can build any package using Ant. It was extended to generate a simple build.xml for any project, with just a few arguments. Then it runs the standard Ant phases to build the package.

Of course this is a very rudimentary build script which doesn't work for complex packages, but it works sufficiently well for simple packages, which represents the majority of packages we currently have.

Supporting Java Modules

As I tried to update JOSM, I found that it required two new packages that I needed to add to Guix: java-jakarta-json and java-parsson. These two packages require some maven plugins we do not have yet and I didn't want to add them too, as that would be too much work just to get a simple update.

So, I used the ant-build-system which generated a build.xml for me. Unfortunately, the build failed very quickly for the first package, as it threw an error when encountering the unknown module keyword:

    [javac] /tmp/guix-build-java-jakarta-json-2.1.3.drv-0/source/api/src/main/java/module-info.java:20: error: class, interface, or enum expected
    [javac] module jakarta.json {
    [javac] ^
    [javac] /tmp/guix-build-java-jakarta-json-2.1.3.drv-0/source/api/src/main/java/module-info.java:22: error: class, interface, or enum expected
    [javac]     exports jakarta.json;
    [javac]     ^
    [javac] /tmp/guix-build-java-jakarta-json-2.1.3.drv-0/source/api/src/main/java/module-info.java:23: error: class, interface, or enum expected
    [javac]     exports jakarta.json.spi;
    [javac]     ^
    [javac] /tmp/guix-build-java-jakarta-json-2.1.3.drv-0/source/api/src/main/java/module-info.java:24: error: class, interface, or enum expected
    [javac]     exports jakarta.json.stream;
    [javac]     ^
    [javac] /tmp/guix-build-java-jakarta-json-2.1.3.drv-0/source/api/src/main/java/module-info.java:25: error: class, interface, or enum expected
    [javac]     uses jakarta.json.spi.JsonProvider;
    [javac]     ^
    [javac] /tmp/guix-build-java-jakarta-json-2.1.3.drv-0/source/api/src/main/java/module-info.java:26: error: class, interface, or enum expected
    [javac] }
    [javac] ^
    [javac] 6 errors

Scary errors!

Since Java 9, modules were introduced as a way to encapsulate multiple classes and resources. It is possible to declare a module, make it depend on other modules. It also allows for hiding internal classes and exporting only classes that should be accessible outside the module.

As I am not a Java developer, and I only care about building packages, the only thing I need to know is that, whenever I see a module-info.java in the source code, it requires Java 9 or later because it's using modules.

To support this, I changed the JDK used for building the package, which is easy to do in the arguments field of the package declaration:

(arguments
 `(#:jdk ,openjdk9; or later versions
   ...)))

Although it was possible to build the first one like that, the second package's module declared a dependency on the first package, and the build failed with the following error:

    [javac] /tmp/guix-build-java-parsson-1.1.5.drv-0/source/impl/src/main/java/module-info.java:21: error: module not found: jakarta.json
    [javac]     requires transitive jakarta.json;
    [javac]                                ^

Turns out, Java doesn't look for modules in the same path as the CLASSPATH. Instead, one needs to explicitly give it a modules path. One can do that with ant:

<javac ...>
  <modulepath refid="some.path.variable" />
</javac>

After modifying the ant-build-system's generate-build.xml phase to add the modulepath in addition to the classpath, I had a few issues.

First, icedtea@3 relies on one of the phases to remove jar non-determinism, meaning that changing the ant-build-system requires rebuilding the java compiler. This significantly slowed down my ability to work on the build system.

Second, I found that the classpath was set incorrectly. The intent was to have it reflect the CLASSPATH environment variable, but it did not work correctly. I replaced:

<path id="classpath">
  <pathelement location="${env.CLASSPATH}" />
</path>

with:

<path id="classpath">
  <pathelement path="${env.CLASSPATH}" />
</path>

Then, I could use classpath as the modulepath.

Third, since we still use Java 8 by default, all builds that use Java 8 (that is, the vast majority of Java packages) initially failed since Java 8 doesn't support the modulepath option. Making it optional via a build-system argument solved the issue. To build with modules and the ant-build-system, you need to do:

(arguments
 `(#:jdk ,openjdk9; or later
   #:use-java-modules? #t ; to use modules
   #:jar-name "..." ; to generate a build.xml
   ...))

Future Work

This small work was quite hard because I had to rebuild the JDK each time I tried something new. It might sound simple now that I figured what needed to be done, but it still took me a few days to figure out.

Anyway, I think this is a great first step towards upgrading our default Java compiler to something that is still supported. I suspect our bootstrap packages being very old will probably not all build with a newer JDK, and since Java 9 introduced new features (such as modules), it's probably the most reasonable next step: Java 9 by default.

Of course, there's still more work to be done to reach the long-term goals:

  • Java 11 by default
  • Keeping Java packages up-to-date
  • Using the Maven build system in more than one package
  • Bootstrapping more of the Java/Android ecosystem
  • Add your own goals here :)

And that's it for today!