'Gradle: Building a modularized library that is compatible with Java 8

So Java 9 is there, soon to be followed by Java 10. Time we should make our libraries ready for use in Java 9 projects. I did it in the following way:

  1. provide a module-info.java
  2. added the (experimental) jigsaw plugin in build.gradle
  3. Manually made changes according to the guide on the gradle site instead of using the jigsaw plugin.

So far, both approaches work fine, and I can use the generated Jar in Java 9 projects.

The problem is, the resulting Jar is not compatible with Java 8 although I used no Java 9 features except the module-info.java. When I set targetCompatibility = 8, an error message tells me to also set sourceCompatibility = 8 accordingly. Which then rejects the module-info.java for which I should set sourceCompatibility = 9.

How can this be solved?

I removed the jigsaw plugin again, and tried this, but am stuck:

  1. set sourceCompatibility = 8 and targetCompatibility = 8
  2. create a new source set moduleInfo that contains the single file module-info.java
  3. set sourceCompatibility = 9 and targetCompatibility = 9 for the new sourceset

Now compilation works, and Gradle uses Java 9 when it tries to compile the module-info.java. However, modules (in this case log4j) are missing, and I get this error:

:compileJava UP-TO-DATE
:processResources NO-SOURCE
:classes UP-TO-DATE
:jar UP-TO-DATE
:sourcesJar UP-TO-DATE
:assemble UP-TO-DATE
:spotbugsMain UP-TO-DATE
:compileModuleInfoJava
classpath:
compilerArgs: [--module-path, , --add-modules, ALL-SYSTEM]
D:\git\utility\src\module-info\java\module-info.java:14: error: module not found: org.apache.logging.log4j
    requires org.apache.logging.log4j;
                               ^
warning: using incubating module(s): jdk.incubator.httpclient
1 error
1 warning
:compileModuleInfoJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileModuleInfoJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s
5 actionable tasks: 1 executed, 4 up-to-date

This is the build.gradle used (Gradle version is 4.5.1):

plugins {
  id "com.github.spotbugs" version "1.6.0"
}

apply plugin: 'maven'
apply plugin: 'maven-publish'
apply plugin: 'java-library'
apply plugin: 'com.github.spotbugs'

sourceCompatibility = 8
targetCompatibility = 8

group = 'com.dua3.utility'

repositories {
    mavenLocal()
    jcenter()
}

dependencies {
  compile     group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.10.0'
  testRuntime group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.10.0'

  // Use JUnit test framework
  testImplementation 'junit:junit:4.12'
}

ext.moduleName = 'com.dua3.utility' 

sourceSets {
    moduleInfo {
        java {
            srcDir 'src/module-info/java'            
        }
    }
}

compileModuleInfoJava {
    sourceCompatibility = 9
    targetCompatibility = 9

    inputs.property("moduleName", moduleName)

    doFirst {
        options.compilerArgs = [
            '--module-path', classpath.asPath,
            '--add-modules', 'ALL-SYSTEM'
        ]
        classpath = files()  
        System.out.println("classpath: "+classpath.asPath)
        System.out.println("compilerArgs: "+options.compilerArgs)
    }
}

tasks.withType(com.github.spotbugs.SpotBugsTask) {
    reports {
        xml.enabled false
        html.enabled true
    }
}

task sourcesJar(type: Jar, dependsOn: classes) {
    classifier = 'sources'
    from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}

artifacts {
    archives sourcesJar
// fails with jigsaw:    archives javadocJar
}

defaultTasks 'build', 'publishToMavenLocal', 'install'

And this is module-info.java:

module com.dua3.utility {
    exports com.dua3.utility;
    exports com.dua3.utility.io;
    exports com.dua3.utility.jfx;
    exports com.dua3.utility.swing;
    exports com.dua3.utility.lang;
    exports com.dua3.utility.math;
    exports com.dua3.utility.text;

    requires javafx.controls;
    requires javafx.web;
    requires java.xml;
    requires java.desktop;
    requires org.apache.logging.log4j;
}


Solution 1:[1]

OK, I finally got it working. In case anyone else wants to know how to do it, this is what I have done:

  • set the Java version to 8, so that the library will be usable by Java 8 applications:

    sourceCompatibility = 8
    targetCompatibility = 8

  • configure the module name

    ext.moduleName = com.dua3.utility

  • add a new sourceset consisting only of module-info.java:

     sourceSets {
            moduleInfo {
                java {
                    srcDir 'src/module-info/java'            
                }
            }
        }
    
  • set compatibility to Java 9 for the moduleInfo, sourceSet, configure modules, and set the output directory:

     compileModuleInfoJava {
        sourceCompatibility = 9    
        targetCompatibility = 9
    
    inputs.property("moduleName", moduleName)
    
    doFirst {
        classpath += sourceSets.main.compileClasspath
    
        options.compilerArgs = [
            '--module-path', classpath.asPath,
            '--add-modules', 'ALL-SYSTEM,org.apache.logging.log4j',
            '-d', sourceSets.main.output.classesDirs.asPath
        ]
    }
    }
    
  • configure the jar task to include moduleInfo:

    jar 
    {
      from sourceSets.main.output
      from sourceSets.moduleInfo.output
    }
    

In case you are using the SpotBugs plugin, you also have to configure the sourceSet explicitly because it will otherwise fail when it tries to process the ModuleInfo sourceSet.

I finally ended up with this version of build.gradle:

plugins {
  id "com.github.spotbugs" version "1.6.0"
}

apply plugin: 'maven'
apply plugin: 'maven-publish'
apply plugin: 'java-library'
apply plugin: 'com.github.spotbugs'

sourceCompatibility = 8
targetCompatibility = 8

group = 'com.dua3.utility'

repositories {
    mavenLocal()
    jcenter()
}

dependencies {
  compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.10.0'
  testRuntime group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.10.0'

  // Use JUnit test framework
  testImplementation 'junit:junit:4.12'
}

ext.moduleName = 'com.dua3.utility' 

sourceSets {
    moduleInfo {
        java {
            srcDir 'src/module-info/java'            
        }
    }
}

compileModuleInfoJava {
    sourceCompatibility = 9
    targetCompatibility = 9

    inputs.property("moduleName", moduleName)

    doFirst {
        classpath += sourceSets.main.compileClasspath

        options.compilerArgs = [
            '--module-path', classpath.asPath,
            '--add-modules', 'ALL-SYSTEM',
            '-d', sourceSets.main.output.classesDirs.asPath
        ]
    }
}

jar 
{
    from sourceSets.main.output
    from sourceSets.moduleInfo.output
}

spotbugs {
    sourceSets = [sourceSets.main]
}

tasks.withType(com.github.spotbugs.SpotBugsTask) {
    reports {
        xml.enabled false
        html.enabled true
    }
}

task sourcesJar(type: Jar, dependsOn: classes) {
    classifier = 'sources'
    from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}

artifacts {
    archives sourcesJar
    archives javadocJar
}

defaultTasks 'build', 'publishToMavenLocal', 'install'

Solution 2:[2]

The question is over a year old, but in case anyone stumbles here, this functionality is now supported by Gradle Modules Plugin since version 1.5.0.

With this plugin, you don't have to create a custom source set, and you only need to call modularity.mixedJavaRelease method.

Here's a sample of how to apply the plugin to one's main build.gradle:

plugins {
  // your remaining plugins here

  id 'org.javamodularity.moduleplugin' version '1.5.0' apply false
}

subprojects {
  // your remaining subproject configuration here

  apply plugin: 'org.javamodularity.moduleplugin'
  modularity.mixedJavaRelease 8 // sets "--release 8" for main code, and "--release 9" for "module-info.java"

  // test.moduleOptions.runOnClasspath = true // optional (if you want your tests to still run on classpath)
}

Solution 3:[3]

I have developed a Gradle plugin for this: https://github.com/Glavo/module-info-compiler

I have tried Gradle Modules Plugin, but there are still some troublesome problems, so I developed this plugin, a compiler specifically used to compile module-info.java.

It is not implemented by calling javac. It is a complete compiler that can run above Java 8. It recognizes the syntax of module-info.java and generates the corresponding module-info.class file according to it.

It only checks the grammar, and does not actually check those packages, classes or modules, so it can work without configuration of any module path.

This Gradle plugin has processed everything for you. For a Java 8 project containing module-info.java, you only need to do this:

plugins {
    id("java")
    id("org.glavo.compile-module-info-plugin") version "2.0"
}

tasks.compileJava {
    options.release.set(8)
}

This answer copy the answer written by myself under another question (https://stackoverflow.com/a/72074642/7659948).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Prags
Solution 2 Tomasz Linkowski
Solution 3 Glavo