Git Product home page Git Product logo

gradle-modules-plugin's Introduction

Build Status

Introduction

This Gradle plugin help with the creation of applications and libraries that make use of the Java Platform Module System. The plugin is published in the Gradle plugin repository. It makes building, testing and running modules seamless from the Gradle perspective. It sets up compiler and jvm settings with flags such as --module-path, so that you can build, test and run JPMS modules without manually setting up your build files.

๐Ÿ’ก When using this plugin, you should not set the --module-path compiler option explicitly. Also, you should disable the modularity.inferModulePath option introduced in Gradle 6.4: modularity.inferModulePath.set(false)

The plugin is designed to work in repositories that contain multiple modules. The plugin currently supports:

  • Compiling modules
  • Testing module code with whitebox tests (traditional unit tests)
  • Testing modules blackbox (testing module boundaries and services)
  • Running/packaging modular applications using the application plugin

The plugin supports the following test engines:

  • JUnit 5
  • JUnit 4
  • TestNG
  • Since 1.7.0 Spock 2 with Groovy 3
  • Since 1.7.0 AssertJ
  • Since 1.7.0 Mockito
  • Since 1.7.0 EasyMock

An example application using this plugin is available here.

Compatability

Plugin Version Gradle Versions Java Version Kotlin Version Notes
- -> 1.8.12 5.+ -> 7.5.+ 11+ 1.0.+ -> 1.6.+
1.8.12 -> 1.8.13 5.+ -> 7.5.+ 11+ 1.0.+ -> 1.9.+ Adds support for Kotlin 1.7 and above.
1.8.14 5.+ -> 7.6.+ 11+ 1.0.+ -> 1.9.+ Fixes compatibility issue with Gradle 7.6
1.8.15 -> + 5.+ -> 8.6.+ 11+ 1.6.+ -> 1.9.+ Fixes compatibility issues with Gradle 8.0.
Use JUnit v5.8.0 or above if using Gradle 8

Setup

For this guide we assume the following directory structure:

.
โ”œโ”€โ”€ build.gradle[.kts]
โ”œโ”€โ”€ gradle
โ”œโ”€โ”€ greeter.api
โ”œโ”€โ”€ greeter.javaexec
โ”œโ”€โ”€ greeter.provider
โ”œโ”€โ”€ greeter.provider.test
โ”œโ”€โ”€ greeter.provider.testfixture
โ”œโ”€โ”€ greeter.runner
โ”œโ”€โ”€ greeter.startscripts
โ””โ”€โ”€ settings.gradle
  • greeter.api: Exports an interface
  • greeter.javaexec: Applications that can be started with ModularJavaExec tasks
  • greeter.provider: Provides a service implementation for the interface provided by greeter.api
  • greeter.provider.test: Blackbox module test for greeter.provider
  • greeter.provider.testfixture: Blackbox module test with test fixtures for greeter.provider
  • greeter.runner: Main class that uses the Greeter service, that can be started/packaged with the application plugin
  • greeter.startscripts: Applications with start scripts generated by ModularCreateStartScripts tasks

The main build file should look as follows:

Groovy DSL
plugins {
    id 'org.javamodularity.moduleplugin' version '1.8.15' apply false
}

subprojects {
    apply plugin: 'java'
    apply plugin: "org.javamodularity.moduleplugin"

    version "1.0-SNAPSHOT"

    sourceCompatibility = 11
    targetCompatibility = 11

    repositories {
        mavenCentral()
    }

    test {
        useJUnitPlatform()

        testLogging {
            events 'PASSED', 'FAILED', 'SKIPPED'
        }
    }

    dependencies {
        testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
        testImplementation 'org.junit.jupiter:junit-jupiter-params:5.3.1'
        testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
    }
}
Kotlin DSL
plugins {
    id("org.javamodularity.moduleplugin") version "1.8.15" apply false
}

subprojects {
    apply(plugin = "java")
    apply(plugin = "org.javamodularity.moduleplugin")

    version = "1.0-SNAPSHOT"

    java {
        toolchain {
            languageVersion.set(JavaLanguageVersion.of(11))
        }
    }

    repositories {
        mavenCentral()
    }

    test {
        useJUnitPlatform()

        testLogging {
            events("PASSED", "FAILED", "SKIPPED")
        }
    }

    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.1")
        testImplementation("org.junit.jupiter:junit-jupiter-params:5.3.1")
        testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.3.1")
    }
}

The most important line in this build file is: apply plugin: "org.javamodularity.moduleplugin" which enables the module plugin for all subprojects. The subprojects typically don't need extra configuration in their build files related to modules.

To build the project simply run ./gradlew build like you're used to.

Creating a module

The only thing that makes a module a module is the existence of module-info.java. In a module-info.java you'll need to define the module name, and possibly declare your exports, dependencies on other modules, and service uses/provides. A simple example is the following. The module name is greeter.api and the module exports a package examples.greeter.api.

module greeter.api {
    exports examples.greeter.api;
}

For Gradle, just make sure the plugin is applied. This will make sure the correct compiler flags are used such as --module-path instead of -cp.

Setting the module version

Since 1.7.0 By default, the plugin uses the value of the project version to set the version of the module. You can configure a different module version using the modularity.moduleVersion method:

Groovy DSL
modularity.moduleVersion '1.2.3'
Kotlin DSL
modularity.moduleVersion("1.2.3")

If no project version is specified and you don't call the modularity.moduleVersion method, the module is created without a version.

Module dependencies

When a module depends on another module, the dependency needs to be declared in two different places. First it needs to be declared in the dependencies section of the gradle build file.

Groovy DSL
dependencies {
    implementation project(':greeter.api') //Example of dependency on another module in the project
    implementation "com.fasterxml.jackson.core:jackson-databind:2.9.5" //Example of an external dependency
}
Kotlin DSL
dependencies {
    implementation(project(":greeter.api")) //Example of dependency on another module in the project
    implementation("com.fasterxml.jackson.core:jackson-databind:2.9.5") //Example of an external dependency
}

Next, it needs to be defined in module-info.java.

module greeter.provider {
    requires greeter.api; //This is another module provided by our project
    requires java.net.http; //This is a module provided by the JDK
    requires com.fasterxml.jackson.databind; //This is an external module
}

Note that the coordinates for the Gradle dependency are not necessarily the same as the module name!

Why do we need to define dependencies in two places!?

We need the Gradle definition so that during build time, Gradle knows how to locate the modules. The plugin puts these modules on the --module-path. Next, when Gradle invokes the Java compiler, the compiler is set up with the correct --module-path so that the compiler has access to them. When using the module system the compiler checks dependencies and encapsulation based on the requires, exports and opens keywords in module-info-java. These are related, but clearly two different steps.

MonkeyPatching the module

There are times when explicit modular settings may be needed on compile, test, and run tasks. You have the option to specify these settings using a moduleOptions extension on the target task, for example:

Groovy DSL
compileJava {
    moduleOptions {
        addModules = ['com.acme.foo']
    }
}
Kotlin DSL
tasks.compileJava {
  extensions.configure<CompileModuleOptions> {
    addModules = listOf("com.acme.foo")
  }
}

The following options are supported by the moduleOptions extension:

  • addModules: Maps to --add-modules. Value is of type List<String>, e.g, ['com.acme.foo'].
  • addReads: Maps to --add-reads. Value is of type Map<String, String>, e.g, ['module1': 'module2'].
  • addExports: Maps to --add-exports. Value is of type Map<String, String>, e.g, ['module1/package': 'module2'].
  • addOpens: Maps to --add-opens. Value is of type Map<String, String>, e.g, ['module1/package': 'module2'] (available only for test and run tasks).

Note that multiple entries matching the same left hand side may be added to addReads, addOpens, and addExports but no value accumulation is performed, the last entry overrides the previous one. If you need to combine multiple values then you must do so explicitly. The following block resolves to --add-reads module1=module3

Groovy DSL
compileJava {
    moduleOptions {
        addReads = [
            'module1': 'module2',
            'module1': 'module3'
        ]
    }
}
Kotlin DSL
tasks.compileJava {
  extensions.configure<CompileModuleOptions> {
    addReads = mapOf(
      "module1", "module2",
      "module1", "module3"
    )
  }
}

Whereas the following block resolves to --add-reads module1=module2,module3

Groovy DSL
compileJava {
    moduleOptions {
        addReads = [
            'module1': 'module2,module3'
        ]
    }
}
Kotlin DSL
tasks.compileJava {
  extensions.configure<CompileModuleOptions> {
    addReads = mapOf(
      "module1", "module2,module3"
    )
  }
}

Whitebox testing

Whitebox testing is your traditional unit test, where an implementation class is tested in isolation.

Typically, we would have a structure as follows:

.
โ”œโ”€โ”€ build.gradle
โ””โ”€โ”€ src
    โ”œโ”€โ”€ main
    โ”‚ย ย  โ”œโ”€โ”€ java
    โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ examples
    โ”‚ย ย  โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ greeter
    โ”‚ย ย  โ”‚ย ย  โ”‚ย ย      โ””โ”€โ”€ Friendly.java
    โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ module-info.java
    โ”‚ย ย  โ””โ”€โ”€ resources
    โ””โ”€โ”€ test
        โ”œโ”€โ”€ java
        โ”‚ย ย  โ””โ”€โ”€ examples
        โ”‚ย ย      โ””โ”€โ”€ greeter
        โ”‚ย ย          โ””โ”€โ”€ FriendlyTest.java
        โ””โ”€โ”€ resources

This poses a challenge for the module system, because the whole point of encapsulation is to hide implementation classes! A class that is not exported can't be accessed from outside the module. In the example above we have another problem, the main and test code uses the same package structure (which is very common). However, The module system does not allow split packages.

We have two different options to work around this:

  • Run whitebox tests on the classpath (ignore the fact that we're in the module world)
  • Patch the module so that it contains the code from both the main and test sources.

Either option is fine. By default, the plugin will automatically set up the compiler and test runtime to run on the module path, and patch the module to avoid split packages.

How does it work?

Essentially, the plugin enables the following compiler flags:

  • --module-path containing all dependencies
  • --patch-module to merge the test classes into the modules
  • --add-modules to add the test runtime (JUnit 5, JUnit 4, TestNG, Spock, AssertJ, Mockito, and EasyMock are supported)
  • --add-reads for the test runtime. This way we don't have to require the test engine in our module.
  • --add-opens so that the test engine can access the tests without having to export/open them in --module-info.java.

The plugin also integrates additional compiler flags specified in a module-info.test file. For example, if your tests need to access types from a module shipping with the JDK (here: java.scripting). Note that each non-comment line represents a single argument that is passed to the compiler as an option.

module-info.test
// Make module visible.
--add-modules
  java.scripting

// Same "requires java.scripting" in a regular module descriptor.
--add-reads
  greeter.provider=java.scripting

See src/test/java/module-info.test and src/test/java/examples/greeter/ScriptingTest.java in test-project/greeter.provider for examples.

Fall-back to classpath mode

If for whatever reason this is unwanted or introduces problems, you can enable classpath mode, which essentially turns off the plugin while running tests.

Groovy DSL
test {
    moduleOptions {
        runOnClasspath = true
    }
}
Kotlin DSL
tasks {
    test {
        extensions.configure(TestModuleOptions::class) {
            runOnClasspath = true
        }
    }
}

You can also enable classpath mode, which essentially turns off the plugin while running tests.

Groovy DSL
compileTestJava {
    moduleOptions {
        compileOnClasspath = true
    }
}
Kotlin DSL
tasks {
  compileTestJava {
        extensions.configure(CompileTestModuleOptions::class) {
            compileOnClasspath = true
        }
    }
}

Blackbox testing

It can be very useful to test modules as a blackbox. Are packages exported correctly, and are services provided correctly? This allows you to test your module as if you were a user of the module. To do this, we create a separate module that contains the test. This module requires and/or uses the module under test, and tests it's externally visible behaviour. In the following example we test a module greeter.provider, which provides a service implementation of type Greeter. The Greeter type is provided by yet another module greeter.api.

The test module would typically be named something similar to the module it's testing, e.g. greeter.provider.test. In src/main/java it has some code that looks like code that you would normally write to use the module that's being tested. For example, we do a service lookup.

package tests;

import examples.greeter.api.Greeter;

import java.util.ServiceLoader;

public class GreeterLocator {
    public Greeter findGreeter() {
        return ServiceLoader.load(Greeter.class).findFirst().orElseThrow(() -> new RuntimeException("No Greeter found"));
    }
}

In src/test/java we have our actual tests.

package tests;

import examples.greeter.api.Greeter;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;

class GreeterTest {
    @Test
    void testLocate() {
        Greeter greeter = new GreeterLocator().findGreeter();
        assertFalse(greeter.hello().isBlank());
    }
}

Because we clearly want to run this code as a module, we also need to have a module-info.java.

import examples.greeter.api.Greeter;

module greeter.provider.test {
    requires greeter.api;

    uses Greeter;
}

As we've discussed before, we also need to configure the --module-path so that the compiler knows about the greeter.api module, and the JVM also starts with the greeter.provider module available. In the build.gradle we should add dependencies to do this.

Groovy DSL
dependencies {
    implementation project(':greeter.api')
    runtimeOnly project(':greeter.provider')
}
Kotlin DSL
dependencies {
    implementation(project(":greeter.api"))
    runtimeOnly(project(":greeter.provider"))
}

Using the Application plugin

Typically, you use the application plugin in Gradle to run the application from Gradle and, more importantly, package it in a distributable zip/tar. To work with modules correctly JVM needs to be configured with the correct arguments such as --module-path to use the module path instead of the classpath. The plugin takes care of all that automatically.

When starting a main class from a module, the module name needs to be provided. To make this easier, the plugin injects a variable $moduleName in the build script.

With Gradle 6.4 or newer, you should use the new syntax introduced by the Application plugin:

Groovy DSL
apply plugin: 'application'
application {
    mainClass = "examples.Runner"
    mainModule = moduleName
}
Kotlin DSL
apply(plugin = "application")
application {
    mainClass.set("examples.Runner")
    mainModule.set(moduleName)
}

When using older versions of Gradle (before 6.4) you can include the module name in the mainClassName property:

Groovy DSL
apply plugin: 'application'
mainClassName = "$moduleName/examples.Runner"
Kotlin DSL
apply(plugin = "application")
mainClassName.set("$moduleName/examples.Runner")

As usual, you can still set extra JVM arguments using the run configuration.

Groovy DSL
run {
    jvmArgs = [
            "-XX:+PrintGCDetails"
    ]

    applicationDefaultJvmArgs = [
            "-XX:+PrintGCDetails"
    ]
}
Kotlin DSL
(run) {
    jvmArgs = listOf("-XX:+PrintGCDetails")
    applicationDefaultJvmArgs = listOf("-XX:+PrintGCDetails")
}

Using the ModularJavaExec task

The application plugin can handle only one executable application. To start multiple applications, you typically need to create a JavaExec task for each executable application. The module plugin offers a similar task named ModularJavaExec, which helps to execute modular applications. This task automatically configures the JVM with the correct arguments such as --module-path. It exposes the same properties and methods as the JavaExec task, the only difference being that the module name should also be provided when setting the main property.

Groovy DSL
task runDemo1(type: ModularJavaExec) {
    group = "Demo"
    description = "Run the Demo1 program"
    main = "greeter.javaexec/demo.Demo1"
    jvmArgs = ["-Xmx128m"]
}
Kotlin DSL
tasks.create<ModularJavaExec>("runDemo1") {
    group = "Demo"
    description = "Run the Demo1 program"
    main = "greeter.javaexec/demo.Demo1"
    jvmArgs = listOf("-Xmx128m")
}

Using the ModularCreateStartScripts task

If you have several application classes in the same Gradle project, you may want to create a distribution that provides separate start scripts for each of them. To help you with this, the plugin offers a ModularCreateStartScripts task, which automatically configures the start scripts with the correct JVM arguments for modular applications.

Each ModularCreateStartScripts task needs an associated ModularJavaExec task that provides information about the application to be started. The typical way to create a distribution containing multiple start scripts is:

  • designate one of the application classes as primary and assign its name to the mainClassName property of the application plugin
  • for each of the remaining application classes:
    • configure a ModularJavaExec task for running the application class
    • configure a ModularCreateStartScripts task associated with the above ModularJavaExec task.

Suppose we have a project with two application classes: MainDemo and Demo1. If we designate MainDemo as primary, a possible build script will look like this:

Groovy DSL
import org.javamodularity.moduleplugin.tasks.ModularJavaExec
import org.javamodularity.moduleplugin.tasks.ModularCreateStartScripts

plugins {
    id 'application'
    id 'org.javamodularity.moduleplugin'
}

dependencies {
    implementation project(':greeter.api')
    runtimeOnly project(':greeter.provider')
}

application {
    mainClassName = "greeter.startscripts/startscripts.MainDemo"
    applicationName = "demo"
    applicationDefaultJvmArgs = ["-Xmx128m"]
}

task runDemo1(type: ModularJavaExec) {
    group = "Demo"
    description = "Run the Demo1 program"
    main = "greeter.startscripts/startscripts.Demo1"
    jvmArgs = ["-Xmx128m"]
}

task createStartScriptsDemo1(type: ModularCreateStartScripts) {
    runTask = tasks.runDemo1
    applicationName = 'demo1'
}

installDist.finalizedBy tasks.createStartScriptsDemo1
Kotlin DSL
import org.javamodularity.moduleplugin.tasks.ModularJavaExec
import org.javamodularity.moduleplugin.tasks.ModularCreateStartScripts

plugins {
    id("application")
    id("org.javamodularity.moduleplugin")
}

dependencies {
    implementation(project(":greeter.api"))
    runtimeOnly(project(":greeter.provider"))
}

application {
    mainClassName.set("greeter.startscripts/startscripts.MainDemo")
    applicationName.set("demo")
    applicationDefaultJvmArgs = listOf("-Xmx128m")
}

val runDemo1 = tasks.create<ModularJavaExec>("runDemo1") {
  group = "Demo"
  description = "Run the Demo1 program"
  main = "greeter.startscripts/startscripts.Demo1"
  jvmArgs = listOf("-Xmx128m")
}

val createScripts = tasks.create<ModularCreateStartScripts>("createStartScriptsDemo1") {
  runTask = runDemo1
  applicationName = "demo1"
}

installDist.finalizedBy(createScripts)

If you have more than two application classes, it's advisable to create programmatically the ModularJavaExec and ModularCreateStartScripts tasks, as shown in the greeter.startscripts project.

The ModularCreateStartScripts task introduces the mandatory property runTask, which indicates the associated ModularJavaExec task. Additionally, it exposes the same properties and methods as the CreateStartScripts task. However, you don't need to set the properties mainClassName, outputDir, classpath, or defaultJvmOpts, because they are automatically set by the plugin, based on the configuration of the associated runTask.

Patching modules to prevent split packages

The Java Platform Module System doesn't allow split packages. A split package means that the same package exists in multiple modules. While this is a good thing, it can be a roadblock to use the module system, because split packages are very common in (older) libraries, specially libraries related to Java EE. The module system has a solution for this problem by allowing to "patch" modules. The contents of a JAR file can be added to a module, by patching that module, so that it contains classes from both JARs. This way we can drop the second JAR file, which removes the split package.

Patching a module can be done with the --patch-module module=somelib.jar syntax for the different Java commands (javac, java, javadoc, ...). The plugin helps making patching easy by providing DSL syntax. Because patching typically needs to happen on all tasks the patch config is set in the build.gradle file directly.

In this example, the java.annotation module is patched with the jsr305-3.0.2.jar JAR file. The plugin takes care of the following:

  • Adding the --patch-module to all Java commands
  • Removing the JAR from the module path
  • Moving the JAR to a patchlibs folder for distribution tasks

Recommended approach

Since 1.7.0 The recommended way to patch modules is by means of the modularity.patchModule function:

Groovy DSL
modularity.patchModule("java.annotation", "jsr305-3.0.2.jar")
Kotlin DSL
modularity.patchModule("java.annotation", "jsr305-3.0.2.jar")

The patchModule method can be called more than once for the same module, if the module needs to be patched with the content of several JARs.

Legacy approach

Legacy 1.x Before 1.7.0, patching modules was possible only by setting patchModules.config:

Groovy DSL
patchModules.config = [
        "java.annotation=jsr305-3.0.2.jar"
]
Kotlin DSL
patchModules.config = listOf(
        "java.annotation=jsr305-3.0.2.jar"
)

For compatibility reasons, this way of patching modules is still supported in newer releases, but it is discouraged. Note that this method does not allow patching a module with more than one JAR.

Compilation

Compilation to a specific Java release

You might want to run your builds on a recent JDK (e.g. JDK 12), but target an older version of Java, e.g.:

You can do that by setting the Java compiler --release option (e.g. to 6 for Java 6, etc.). Note that when you build using:

  • JDK 11: you can only target Java 6-11 using its --release option,
  • JDK 12: you can only target Java 7-12 using its --release option,
  • etc.

Finally, note that JPMS was introduced in Java 9, so you can't compile module-info.java to Java release 6-8 (this plugin provides a workaround for that, though โ€” see below).

Concluding, to configure your project to support JPMS and target:

and the plugin will take care of setting the --release option(s) appropriately.

Separate compilation of module-info.java

If you need to compile the main module-info.java separately from the rest of src/main/java files, you can enable moduleOptions.compileModuleInfoSeparately option on compileJava task. It will exclude module-info.java from compileJava and introduce a dedicated compileModuleInfoJava task.

Typically, this feature would be used by libraries which target JDK 6-8 but want to make the most of JPMS by:

  • providing module-info.class for consumers who put the library on module path,
  • compiling module-info.java against the remaining classes of this module and against other modules (which provides better encapsulation and prevents introducing split packages).

This plugin provides an easy way to do just that by means of its modularity.mixedJavaRelease function, which implicitly sets compileJava.moduleOptions.compileModuleInfoSeparately = true and configures the --release compiler options.

For example, if your library targets JDK 8, and you want your module-info.class to target JDK 9 (default), put the following line in your build.gradle(.kts):

Groovy DSL
modularity.mixedJavaRelease 8
Kotlin DSL
modularity.mixedJavaRelease(8)

Note that modularity.mixedJavaRelease does not configure a multi-release JAR (in other words, module-info.class remains in the root directory of the JAR).

Improve Eclipse .classpath-file

When applying the eclipse-plugin (among others) a task called "eclipseClasspath" is added by that plugin creating a .classpath-file. The .classpath-file created by that task doesn't take into account modularity. As described by the documentation it is possible to configure the .classpath-file via configuration hooks.

Since 1.7.0 The gradle-modules-plugin provides the modularity.improveEclipseClasspathFile() method, which configures the .classpath-file via configuration hooks:

Groovy DSL
modularity.improveEclipseClasspathFile()
Kotlin DSL
modularity.improveEclipseClasspathFile()

Examples on how and where Gradle's eclipse-plugin could (and should) be improved and how a .classpath-file is affected if the feature is enabled are available on GitHub.

Limitations

Please file issues if you run into any problems or have additional requirements!

Requirements

This plugin requires JDK 11 or newer to be used when running Gradle.

The minimum Gradle version supported by this plugin is 5.1. However, we strongly recommend to use at least Gradle 6.0, because there are a few special cases that cannot be handled correctly when using older versions.

Contributing

Please tell us if you're using the plugin on @javamodularity! We would also like to hear about any issues or limitations you run into. Please file issues in the GitHub project. Bonus points for providing a test case that illustrates the issue.

Contributions are very much welcome. Please open a Pull Request with your changes. Make sure to rebase before creating the PR so that the PR only contains your changes, this makes the review process much easier. Again, bonus points for providing tests for your changes.

gradle-modules-plugin's People

Contributors

aalmiray avatar aaylward avatar adamcarroll avatar alfred-65 avatar big-andy-coates avatar bjorndarri avatar bredmold avatar carlosame avatar dependabot[bot] avatar djviking avatar ealrann avatar iherasymenko avatar kengotoda avatar mariusvolkhart avatar martilidad avatar netflixsiteops avatar paulbakker avatar siordache avatar sormuras avatar swpalmer avatar tiainen avatar tlinkowski avatar weiqigao avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gradle-modules-plugin's Issues

Support additional src & test tasks

Only standard src/main/java and src/test/java sources are currently supported.

Adding more source sets such as src/integration-test/java with a custom integrationTest task of type Test fails to be recognized for module updates.

Provide option to leave certain modules in the classpath

As of now, every dependency (module/jar file) is placed in the module path. However, some legacy libraries are not yet compatible with the module system, and for these cases it would be easiest to leave them on the classpath (for example, if a jar file does not provide an automatic module name but it's file name is invalid for the module system). Could you please provide an option to specify a list of dependencies that should stay on the classpath.

I think the code related is the following:

"--module-path", execTask.getClasspath()
.filter(patchModuleExtension::isUnpatched
).getAsPath(),

Modular JUnit (5.5.x) support

JUnit 5 uses explicit modules introduced by junit-team/junit5@0241727 since 5.5.x.

This modules seems to be incompatible with that since upgrading JUnit to 5.5.1 (5.4.2 works) in test-project makes ../gradlew build fail with:

failed to execute tests

org.gradle.api.internal.tasks.testing.TestSuiteExecutionException: Could not complete execution for Gradle Test Executor 4.
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:63)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:567)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
	at com.sun.proxy.$Proxy2.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:132)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:567)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:175)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:157)
	at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:404)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
	at java.base/java.lang.Thread.run(Thread.java:835)
Caused by: java.lang.IllegalAccessError: class org.junit.platform.launcher.core.LauncherFactory (in unnamed module @0x6358e614) cannot access class org.junit.platform.commons.util.Preconditions (in module org.junit.platform.commons) because module org.junit.platform.commons does not export org.junit.platform.commons.util to unnamed module @0x6358e614
	at org.junit.platform.launcher.core.LauncherFactory.create(LauncherFactory.java:83)
	at org.junit.platform.launcher.core.LauncherFactory.create(LauncherFactory.java:67)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:100)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:82)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:78)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
	... 25 more

junit-platform-commons seems to be loaded as unnamed module.
Is there something wrong with junit-platform-commons or can this be fixed by this plugin?

Seems not to handle automatic modules

It does not seem that plugin handles non-modular dependencies which becomes automatic modules.

build.gradle

dependencies {
    compile group: 'com.google.protobuf', name: 'protobuf-java', version: protobufVersion
}

module-info.java

module no.spacetec.mcpb {
    requires protobuf.java;
}

It cannot find this module on compileJava

10:08:12.933 [DEBUG] [org.gradle.api.internal.tasks.compile.NormalizingJavaCompiler] Compiler arguments: -source 11 -target 11 -d /home/sverre/workspace/application/build/classes/java/main -encoding UTF-8 -g -sourcepath  -proc:none -XDuseUnsharedTable=true -classpath /home/sverre/.gradle/caches/modules-2/files-2.1/com.google.protobuf/protobuf-java/3.1.0/e13484d9da178399d32d2d27ee21a77cfb4b7873/protobuf-java-3.1.0.jar 
/home/sverre/workspace/application/src/main/java/no/spacetec/protobuf/Users.java  
/home/sverre/workspace/application/src/main/java/no/spacetec/protobuf/Files.java 
/home/sverre/workspace/application/src/main/java/module-info.java
10:08:12.933 [INFO] [org.gradle.api.internal.tasks.compile.JdkJavaCompiler] Compiling with JDK Java compiler API.
10:08:13.424 [ERROR] [system.err] /home/sverre/workspace/application/src/main/java/module-info.java:3: error: module not found: protobuf.java
10:08:13.424 [ERROR] [system.err]     requires protobuf.java;
10:08:13.424 [ERROR] [system.err]                      ^
10:08:13.425 [ERROR] [system.err] 1 error

Proposal: More convenience methods in `modularity` extension

@paulbakker, let me know if you'd be interested in accepting PRs for the following:

1. Adding convenience methods modularity.addModules, etc.

ModuleOptions for various tasks (compileJava, compileTestJava, test, javadoc, run) currently share the following four config options: addModules, addReads, addExports, and addOpens. However, all of them have to be set separately for every task.

I propose adding convenience methods to ModularityExtension that would append to those config options for all applicable tasks.

Signatures:

interface ModularityExtension {
  // ...
  void addModules(String... moduleNames);
  void addReads(Map<String, String> addReads);
  void addExports(Map<String, String> addExports);
  void addOpens(Map<String, String> addOpens);
}

For example modularity.addModules 'java.sql' would add module java.sql to all the tasks.

This is related to #76.

2. Adding convenience method modularity.patchModules

I suggest adding:

interface ModularityExtension {
  // ...
  void patchModules(Map<String, String> patchModules);
}

For consistency, I changed the signature from List<String> (where strings are separated with =) to Map<String, String> so that it aligns with addReads, addExports, and addOpens (introduced by #74).

This would let us call:

modularity.patchModules [
        'java.annotation': 'jsr305-3.0.2.jar'
]

instead of

patchModules.config = [
        "java.annotation=jsr305-3.0.2.jar"
]

Moreover, since javac's --patch-module supports patching multiple JARs into a module (separated with :), this method would allow such notation. For example:

modularity.patchModules [
        'java.annotation': 'jsr305-3.0.2.jar:some-more-javax-annotations.jar'
]

would map to

patchModules.config = [
        "java.annotation=jsr305-3.0.2.jar",
        "java.annotation=some-more-javax-annotations.jar"
]

I also propose deprecating direct patchModules extension usage.

Can we make building non-modular libraries easier?

I need to integrate with some legacy libraries that have split packages. Updating these libraries and releasing new major versions is one solution, but not practical in my case due to the large number of consumers of the affected libraries and the reluctance of consumers to update this particular library.

In order to write code against these kinds of problematic packages while still using modularized jars, I need to place some jars on the classpath and some jars on the module-path. This plugin currently places all jars on the module-path which wont work for me since I need to be more selective and avoid split-packages breaking my builds.

The approach I've played with which seems to work is to create a project that isn't modularized and loads things from both the classpath and module-path. I scan through all of the dependencies of the project and use a ModuleFinder to determine which dependencies are not automatic modules. These kinds of modules must be on the module-path. All of the dependencies of these modules defined using a ModuleReference#requires are also recursively added to the module-path. Everything else is added to the classpath.

This project could look to see if the package being built defines a module-info.java. If it does, it uses it's current approach of placing everything on the module path. If it doesn't it could use the approach described above to separate the classpath from the module path.

Here's a quick example of what I hacked together to help illustrate what I'm doing: https://gist.github.com/mtdowling/39905d445829ab1ac58d0cafecf6bc9e

Is this a reasonable approach, or are there issues I'm not seeing? Is this something you think could be added to this library?

Generate META-INF/services from module-info.java

Based on the discussion in #84, I'm convinced that library authors that create modular JARs should define their service providers in both module-info.java and in META-INF/services to ensure that their JARs work on the classpath.

Given how important it is to make JARs work on the classpath and how poorly supported in general JPMS is today (excluding this library!), I think it would be very useful for this library to have the option to generate META-INF/services files for each service provider detected in a module-info.java. If the library detects that META-INF/services already exist or that any of the service providers use the static provides method, then it would fail the build.

NPE when trying to run application

This error started popping up tonight while I was working on my code, and I have been trying to debug it ever since. I am creating a JavaFX program, and am using the openjfx plugin to load the JavaFX modules. When I try to invoke the run task for my application, the build fails with this error. Any ideas? I can provide my gradle build files if that would be helpful.

Caused by: java.lang.NullPointerException: (No message provided)Close stacktrace
at org.javamodularity.moduleplugin.tasks.RunTaskMutator.getMainClass(RunTaskMutator.java:151)
at org.javamodularity.moduleplugin.tasks.RunTaskMutator.lambda$updateJavaExecTask$3(RunTaskMutator.java:126)
at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:702)
at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:669)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$5.run(ExecuteActionsTaskExecuter.java:404)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:402)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:394)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$1.execute(DefaultBuildOperationExecutor.java:165)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:250)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:158)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:92)
at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:393)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:376)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.access$200(ExecuteActionsTaskExecuter.java:80)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$TaskExecution.execute(ExecuteActionsTaskExecuter.java:213)
at org.gradle.internal.execution.steps.ExecuteStep.lambda$execute$1(ExecuteStep.java:33)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:33)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:26)
at org.gradle.internal.execution.steps.CleanupOutputsStep.execute(CleanupOutputsStep.java:58)
at org.gradle.internal.execution.steps.CleanupOutputsStep.execute(CleanupOutputsStep.java:35)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:48)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:33)
at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:39)
at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:73)
at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:54)
at org.gradle.internal.execution.steps.CatchExceptionStep.execute(CatchExceptionStep.java:35)
at org.gradle.internal.execution.steps.CreateOutputsStep.execute(CreateOutputsStep.java:51)
at org.gradle.internal.execution.steps.SnapshotOutputsStep.execute(SnapshotOutputsStep.java:45)
at org.gradle.internal.execution.steps.SnapshotOutputsStep.execute(SnapshotOutputsStep.java:31)
at org.gradle.internal.execution.steps.CacheStep.executeWithoutCache(CacheStep.java:201)
at org.gradle.internal.execution.steps.CacheStep.execute(CacheStep.java:70)
at org.gradle.internal.execution.steps.CacheStep.execute(CacheStep.java:45)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:49)
at org.gradle.internal.execution.steps.StoreSnapshotsStep.execute(StoreSnapshotsStep.java:43)
at org.gradle.internal.execution.steps.StoreSnapshotsStep.execute(StoreSnapshotsStep.java:32)
at org.gradle.internal.execution.steps.RecordOutputsStep.execute(RecordOutputsStep.java:38)
at org.gradle.internal.execution.steps.RecordOutputsStep.execute(RecordOutputsStep.java:24)
at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:96)
at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$0(SkipUpToDateStep.java:89)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:54)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:38)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:77)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:37)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:36)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:26)
at org.gradle.internal.execution.steps.ResolveCachingStateStep.execute(ResolveCachingStateStep.java:90)
at org.gradle.internal.execution.steps.ResolveCachingStateStep.execute(ResolveCachingStateStep.java:48)
at org.gradle.internal.execution.impl.DefaultWorkExecutor.execute(DefaultWorkExecutor.java:33)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:120)
at org.gradle.api.internal.tasks.execution.ResolveBeforeExecutionStateTaskExecuter.execute(ResolveBeforeExecutionStateTaskExecuter.java:75)
at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:62)
at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:108)
at org.gradle.api.internal.tasks.execution.ResolveBeforeExecutionOutputsTaskExecuter.execute(ResolveBeforeExecutionOutputsTaskExecuter.java:67)
at org.gradle.api.internal.tasks.execution.StartSnapshotTaskInputsBuildOperationTaskExecuter.execute(StartSnapshotTaskInputsBuildOperationTaskExecuter.java:62)
at org.gradle.api.internal.tasks.execution.ResolveAfterPreviousExecutionStateTaskExecuter.execute(ResolveAfterPreviousExecutionStateTaskExecuter.java:46)
at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:94)
at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:95)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:56)
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:73)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:49)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:416)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:406)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$1.execute(DefaultBuildOperationExecutor.java:165)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:250)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:158)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:102)
at org.gradle.internal.operations.DelegatingBuildOperationExecutor.call(DelegatingBuildOperationExecutor.java:36)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:49)
at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:43)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:355)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:343)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:336)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:322)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker$1.execute(DefaultPlanExecutor.java:134)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker$1.execute(DefaultPlanExecutor.java:129)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:202)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:193)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:129)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)```

Support `module-info.test` configuration file

Consider supporting a build-tool agnostic module-info.test text file that contains the java command line options needed to make white-box testing work.

For more details about the idea see https://sormuras.github.io/blog/2018-09-11-testing-in-the-modular-world#white-box-modular-testing-with-extra-java-command-line-options

An implementation can be was copied from here:

https://github.com/sormuras/junit-platform-maven-plugin/blob/92239b39d7d1112cf25f0b37287d38605d1f09ed/src/main/java/de/sormuras/junit/platform/maven/plugin/Patcher.java#L37-L63

...and landed in #18.

Test resources not patched into the module under test.

This might be related with #26.

When I modularize a simple application with a test that loads a test resource file by adding a module-info.java file in src/main/java and apply to moduleplugin to build.gradle, the test fails.

Here's a simple project that exhibits the issue: https://github.com/weiqigao/moduleplugin-test-resources-issue/tree/master/src

Runnin "gradle test --debug" reveals that module path includes

    ./build/classes/java/main
    ./build/classes/java/test
    ./build/resources/main
    ./build/resources/test

with only the ./build/classes/java/test patched into the module. To have the resources visible from inside the module, the ./build/resources/main and ./build/resources/test needs to be patched into the module as well.

Add support for testImplementation configuration

I've run into an issue in one of my pet projects. JUnit Platform was not detected as a test dependency hence I got this error:

java.lang.reflect.InaccessibleObjectException: Unable to make com.github.iherasymenko.DummyTest() accessible: module com.github.iherasymenko.playground does not "opens com.github.iherasymenko" to module org.junit.platform.commons`

Since the testCompile configuration has been deprecated I believe it would be better to replace all the mentions of "testCompile" in the plugin code as long as make the plugin compatible with the testImplemention configuration.

A PR is on its way.

DSL approach fails when no module-info.java found

The execJava task is only extended in case a module-info.java is found. Hence, moduleOptions() is not recognised in case the application your want to run itself is no module

foundModuleName.ifPresent(moduleName -> {
...
    new RunTask().configureRun(project, moduleName);
...
}

DSL support to --add-opens

If I try

run {
    jvmArgs = [
            "--add-opens java.base/java.lang=com.google.guice"
    ]
}

I run into java options order problem:

java --add-opens java.base/java.lang=com.google.guice --module-path ....
.....
Unrecognized option: --add-opens java.base/java.lang=com.google.guice

Maybe the DSL could also support --add-opens.

For example

run {
    moduleOptions {
        addOpens = ['java.base/java.lang=com.google.guice']
    }
}

Support for Eclipse plugin

Could this plugin assist with eclipse?

In order to put dependencies on the modulepath instead of classpath I have used the following eclipse configuration in my projects.

eclipse {
    classpath {
        file {
            whenMerged {
                entries.findAll { isModule(it) }.each {
                    it.entryAttributes['module'] = 'true'
                }

                entries.findAll { isSource(it) && isTestScope(it) }.each {
                    it.entryAttributes['test'] = 'true'
                }

                entries.findAll { isLibrary(it) && isTestScope(it) }.each {
                    it.entryAttributes['test'] = 'true'
                }
            }
        }
    }
}

boolean isLibrary(entry) { return entry.properties.kind.equals('lib') }
boolean isTestScope(entry) { return entry.entryAttributes.get('gradle_used_by_scope').equals('test'); }
boolean isModule(entry) { isLibrary(entry) && !isTestScope(entry); }
boolean isSource(entry) { return entry.properties.kind.equals('src'); }

Create DSL support to --add-modules

It should be easy to specify extra --add-modules. One use case is when extra modules are required when using the application plugin. This could also apply to scenarios when the actual application doesn't have a module-info.java.

Proposal: Some internal refactorings

@paulbakker, let me know if you'd be interested in accepting PRs for the following internal refactorings:

1. Cleanup of test-project and test-project-kotlin

Aligning to test-project-mixed:

  • removing duplication in build.gradle(.kts) and settings.gradle(.kts) files
  • removing redundant configuration (e.g. plugins { id 'org.javamodularity.moduleplugin' } at the start of every subproject)
  • introducing gradle.properties with library versions (jUnitVersion = 5.3.1)

2. Applying JavaProjectHelper in more places

Currently used only in CompileTask and CompileModuleInfoTask.

This would allow for splitting long methods into shorter ones for better readability.

Feature request: Separate compilation of module-info.java

Overview

I'd like to request a feature for library developers who need to target JDK 6-8 but who also want to provide a module-info.class (as per Option 3 by @jodastephen).

The proposed feature has two parts:

  1. Low-level API: separate compilation of module-info.java.
  2. High-level API: easy setting of javac --release option, separately for main Java code and for module-info.java.

Justification

General

Library maintainers need to target JDK 6-8 because these JDKs (especially JDK 8) are still very popular (taking ~95% share in 2018, and predicted to take ~90% in 2019).

Still, those maintainers could make the most of JPMS by:

  • providing module-info.class for consumers who put the library on module path,
  • compiling module-info.java against the remaining classes of this module and against other modules (which provides better encapsulation and prevents split packages).

So providing a module-info.class is like saying:

Hey, I'm JPMS-compatible! You can safely put me on the module path โ€” I guarantee to have no split packages!

To sum up, I see this feature request as great means of promoting JPMS! Would you agree, @paulbakker, @sandermak?

Examples

Here are some popular libraries that provide module-info.class while targeting JDK 6-8, e.g.:

  • Maven:
  • Ant:
    • Lombok (JDK 9 module-info.class in root dir)

There are also some libraries that:

Vavr /หˆveษช.vษš/

Let's have a closer look at vavr's case:

  1. It's a quite popular library (3000+ Github stars) targeting JDK 8.
  2. Its creator, @danieldietrich, is (or was?) a believer in JPMS.
  3. However, as vavr-io/vavr#2230 (comment) shows, he backed out of it (partly because the required Gradle configuration was too complex).
  4. I personally think it's a shame if such a popular library cannot be easily shipped with a module-info.class while still targeting JDK 8.

Proposed DSL

Note: Everything in this section (especially naming) is TBD.

Low-level API

Extra property on moduleOptions extension:

build.gradle
compileJava.moduleOptions.compileModuleInfoSeparately = true
build.gradle.kts
tasks {
  compileJava.moduleOptions.compileModuleInfoSeparately = true
}

High-level API

Special modularity extension with two functions:

  • for releasing to JDK 6-8:
    • mixedJavaRelease(int mainJavaRelease, int moduleInfoJavaRelease = 9)
  • for releasing to JDK 9+
    • standardJavaRelease(int mainJavaRelease)

For example, the most common configuration (JDK 8 + JDK 9 module-info.class) would be a one-liner:

build.gradle
modularity.mixedJavaRelease 8
build.gradle.kts
modularity.mixedJavaRelease(8)

Proposed behavior

module-info.java location

I propose to leave module-info.java in src/main/java (I don't see any need to put it in a separate source set directory).

module-info.class location

There are two places where module-info.class can end up:

  1. in the root output directory (natural; corresponds to module-info.java location)
  2. in META-INF/versions/9 (Multi-Release JAR, AKA MRJAR)

Having read a post on MRJARs by @melix, I'm rather suspicious of MRJARs, and so I prefer option 1.

On the other hand, @gunnarmorling claims that it's better to use option 2 because module-info.class "will be out of the way in most cases when being used on an older Java version". But I don't really know why, because as far as I understand, any tool that scans the classpath (like Guava's ClassPath) will return module-info.class no matter where it is (based on its .class extension). @gunnarmorling, would you care to give me a pointer here?

Alternatives

ModiTect

It's fair to mention ModiTect (by @gunnarmorling) and its Gradle plugin (by @siordache) here.

However, ModiTect does much more than I request here: ModiTect actually generates module-info.class from a special notation (there's even no module-info.java there edit: it can actually parse module-info.java). Personally, I find it too complex for my needs.

Badass-Jar plugin

@siordache also created a Gradle plugin that lets one "seamlessly create modular jars that target a Java release before 9".

It looks quite nice, however:

  • I'm not sure how it plays along with org.javamodularity.moduleplugin (and I'd like to use it for patching modules, testing on module-path, etc.)
  • in order to build the proper JAR and make sure your module-info.java is correct, you actually have to run the build twice (./gradlew jar and ./gradlew -PjavaCompatibility=9 jar) - too complex
  • it doesn't use javac's --release option, which (as opposed to using -source and -target) guarantees that only the right APIs are referenced (e.g. it throws when compiling optional.isEmpty() with --release 8)
  • it produces only Multi-Release JARs, and I'm somewhat skeptical about them (as I mentioned before)

Related StackOverflow questions

Failure to locate resources when the application plugin is in use

Using the application plugin in conjunction with the modularity plugin results in error when attempting to load resources found in the same package. Take the following project structure

.
โ”œโ”€โ”€ build.gradle
โ”œโ”€โ”€ gradle
โ”‚   โ””โ”€โ”€ wrapper
โ”‚       โ”œโ”€โ”€ gradle-wrapper.jar
โ”‚       โ””โ”€โ”€ gradle-wrapper.properties
โ”œโ”€โ”€ gradlew
โ”œโ”€โ”€ gradlew.bat
โ””โ”€โ”€ src
    โ””โ”€โ”€ main
        โ”œโ”€โ”€ java
        โ”‚   โ”œโ”€โ”€ com
        โ”‚   โ”‚   โ””โ”€โ”€ acme
        โ”‚   โ”‚       โ””โ”€โ”€ sample
        โ”‚   โ”‚           โ””โ”€โ”€ Launcher.java
        โ”‚   โ””โ”€โ”€ module-info.java
        โ””โ”€โ”€ resources
            โ””โ”€โ”€ com
                โ””โ”€โ”€ acme
                    โ””โ”€โ”€ sample
                        โ””โ”€โ”€ text.txt

Code to reproduce the problem can be found at https://github.com/aalmiray/sample-modularity

------------------------------------------------------------
Gradle 4.10.2
------------------------------------------------------------

Build time:   2018-09-19 18:10:15 UTC
Revision:     b4d8d5d170bb4ba516e88d7fe5647e2323d791dd

Kotlin DSL:   1.0-rc-6
Kotlin:       1.2.61
Groovy:       2.4.15
Ant:          Apache Ant(TM) version 1.9.11 compiled on March 23 2018
JVM:          11 (Oracle Corporation 11+28)
OS:           Mac OS X 10.13.6 x86_64

This problem was also reported at the Gradle forums: https://discuss.gradle.org/t/loading-resources-files-in-java-11/28704/11

Additional action for task ':startScripts': was implemented by the Java lambda

In version 1.4.0 I see the following warning when using --build-cache --info:

> Task :startScripts
Caching disabled for task ':startScripts': Caching has not been enabled for the task
Task ':startScripts' is not up-to-date because:
  Additional action for task ':startScripts': was implemented by the Java lambda 'org.javamodularity.moduleplugin.tasks.RunTaskMutator$$Lambda$533/0x0000000840ee3440'. Using Java lambdas is not supported, use an (anonymous) inner class instead.
Custom actions are attached to task ':startScripts'.

For me, it seems to be an issue in the lines of #54

module-info.test used in compileJava task

#17 My understanding is that the module-info.test file is only for test source compilation and running tests, but it is also used when compiling the main source set, see org.javamodularity.moduleplugin.tasks.CompileJavaTaskMutator:24.
This is problematic since the modules referenced in module-info.test may only be available as test dependencies (I kept getting "module not found" errors in the compileJava task).
I commented out the aforementioned line, all tests passed (in gradle-modules-plugin) and my project now builds correctly.

Feature request: allow any JavaExc task to be enhanced

Currently the org.javamodularity.moduleplugin.tasks.RunTask class enhances the JavaExec gradle task if and only the application plugin is applied. See https://github.com/java9-modularity/gradle-modules-plugin/blob/master/src/main/java/org/javamodularity/moduleplugin/tasks/RunTask.java#L29

I have the need to enhance other JavaExec tasks that are not provided by the application plugin (see https://github.com/groovyfx-project/groovyfx/blob/master/gradle/demo.gradle#L36).

This could be done by providing a subclass of JavaExec that is module aware.

addExports and addOpens for run task

What's the correct syntax to add addExports and addOpens statement to the run task? The following code does not work (i.e. adds no command line arguments).

run {
    moduleOptions {
        addExports = [
                'javafx.controls/com.sun.javafx.scene.control' : 'mymodule',
                'org.controlsfx.controls/impl.org.controlsfx.skin' : 'mymodule'
        ]

        addOpens = [
                'javafx.controls/javafx.scene.control' : 'mymodule',
                'org.controlsfx.controls/org.controlsfx.control.textfield' : 'mymodule'
        ]
    }
}

Consider transitive dependencies when resolving TestEngine

The current approach checks testCompile and testImplementation configurations, but fails to recognize the test engine if it happens to be defined in compile or implementation.

This is the case when a project defines test support and the test engine is part of its public API and dependencies.

How to use with google-truth

google-truth has a split package with the java 8 extensions and I'm struggling to figure out how to get it to work for my tests. This is what I have so far in module-info.test.

--add-modules
  truth

--add-reads
  com.github.moaxcp.split-package.moduleA=truth

--patch-module
  truth={location in gradle}...\4d01dfa5b3780632a3d109e14e101f01d10cce2c\truth-java8-extension-0.42.jar

I believe I need to patch the truth module with the extensions but it doesn't seem to work. Do I need the exact location of the jar? Is there a way to set this argument up with gradle? The error I am getting are.

error: the unnamed module reads package com.google.common.truth from both truth.java8.extension and truth
error: module truth.java8.extension reads package com.google.common.truth from both truth and truth.java8.extension
error: module hamcrest.core reads package com.google.common.truth from both truth and truth.java8.extension
error: module com.google.common reads package com.google.common.truth from both truth and truth.java8.extension
error: module checker.compat.qual reads package com.google.common.truth from both truth and truth.java8.extension
error: module checker.qual reads package com.google.common.truth from both truth and truth.java8.extension
error: module diffutils reads package com.google.common.truth from both truth and truth.java8.extension
error: module auto.value.annotations reads package com.google.common.truth from both truth and truth.java8.extension
error: module error.prone.annotations reads package com.google.common.truth from both truth and truth.java8.extension
error: module jsr305 reads package com.google.common.truth from both truth and truth.java8.extension
error: module j2objc.annotations reads package com.google.common.truth from both truth and truth.java8.extension
error: module animal.sniffer.annotations reads package com.google.common.truth from both truth and truth.java8.extension
error: module junit reads package com.google.common.truth from both truth and truth.java8.extension
error: module truth reads package com.google.common.truth from both truth.java8.extension and truth
error: module com.github.moaxcp.split.moduleA reads package com.google.common.truth from both truth and truth.java8.extension

Support testing both on class-path and module-path in one run

https://blog.joda.org/2018/03/jpms-negative-benefits.html

As a library author, you cannot control whether the class-path or module-path is used. You have no choice - you must test both, which you probably won't think to do.

With the current support:

test {
    moduleOptions {
        runOnClasspath = project.hasProperty('runOnClasspath')
    }
}

One would have to run Gradle twice:

$ ./gradlew test && ./gradlew test -PrunOnClasspath

Request

Something along the lines (true could possibly be the default):

test {
    moduleOptions {
        runOnClasspathAndModulepath = true
    }
}

A single execution would test both:

$ ./gradlew test

Discussion: revert non-modular support

When we started writing the JavaFX samples (https://github.com/openjfx/samples), this plugin was used to remove boiler plate code for handling modular projects. Within these samples, there is a clear distinction between modular and non-modular projects. For modular projects, there is no problem as the plugin works as intended. However, using JavaFX in non-modular projects is a bit tricky as there is a caveat: JavaFX must be provided on the module path to avoid the following error:

Error: JavaFX runtime components are missing, and are required to run this application

We have therefor provided a few pull requests (#24, #39 and #41) to be able to apply this plugin on non-modular projects.

Since then, a gradle javafx plugin was created, simplifying support for JavaFX in gradle projects. My current thinking is that the gradle modules plugin should no longer be responsible for supporting non-modular projects for two main reasons:

  1. This is the gradle modules plugin, and as such, it does not make much sense to apply it to a non-modular project.
  2. What we have now, is a mix between modular and non-modular behavior. When this plugin is applied to a non-modular project, it is trying hard to make it behave like a modular project. Which is not what the user is expecting. One big issue for instance, is that all compile dependencies are added to the module path. This works great when the project only depends on JavaFX (as it must reside on the module path). But it works not so great when more and more (non-modular) dependencies are added to the module path, with a high likelihood of resulting in split packages in the end.

Again, the only reason non-modular support was introduced here, was to be able to use it for non-modular projects that use JavaFX. I am not aware of other projects that have this requirement. Therefor, my suggestion is that we strip the non-modular support out of this plugin. That way it can focus on improving support for modules, without non-modular projects getting in the way. The non-modular support can then be handled solely within the JavaFX plugin.

Thoughts?

Proposal: Some API refactorings

@paulbakker, let me know if you'd be interested in accepting PRs for the following:

1. Moving extensions from tasks subpackage to extensions subpackage

Applies to ModuleOptions, TestModuleOptions and PatchModuleExtension.

CompileModuleOptions and ModularityExtension already are in extensions subpackage.

2. Extracting dedicated *ModuleOptions interfaces for every task

This is needed so that breaking changes for Kotlin DSL are no longer be possible (as happened when CompileModuleOptions.compileModuleInfoSeparately had to be introduced).

Layout:

  • compileJava.moduleOptions โ†’ is: CompileModuleOptions (already OK)
  • test.moduleOptions โ†’ is: TestModuleOptions (already OK)
  • compileTestJava.moduleOptions โ†’ to be: CompileTestModuleOptions (is: ModuleOptions)
  • javadoc.moduleOptions โ†’ to be: JavadocModuleOptions (is: ModuleOptions)
  • run.moduleOptions โ†’ to be: RunModuleOptions (is: ModuleOptions)

Note that the functionality will be exactly the same โ€” it's only about what types are exposed to Kotlin DSL (in case any of the types will need new methods in future).

3. Limiting ModuleOptions.addOpens to test and run tasks only

I just found out that --add-opens cannot be used with javac nor javadoc (it can only be used with java).

Therefore, I believe it should not be accessible from compileJava, compileTestJava nor javadoc tasks (such access was introduced by #74).

This is precisely why point 2 of this issue is so useful โ€” through interfaces, we can expose all the options specific to a given task, and the implementations can be reused whenever necessary (without changing the API for the consumer).


Note: (1) and (2) are potentially breaking changes for Kotlin DSL. But I believe it's better to introduce them now, when Kotlin DSL is not that popular yet.

Javadoc task fails (module not found)

The dependencies are configured like this:

dependencies {
	implementation(
		[...]
		'net.bytebuddy:byte-buddy:1.9.11',
		[...]
	)
	[...]
}

The module-info.java looks like this:

open module [...] {
	[...]
	requires net.bytebuddy;
	[...]
}

It results in an error when executing the javadoc task:

[...]\src\main\java\module-info.java:3: error: module not found: net.bytebuddy
requires net.bytebuddy;

ByteBuddy is a multi release jar with the module-info.class inside META-INF/versions/9

Seems to be related to this issue which is already closed: #13

Application plugin run task not working in sample project

Thanks for your work on this useful plugin!

Unfortunately, the application plugin run task is not working in the sample project. See the following for an example of the problem:

gradle-modules-plugin/test-project$ ../gradlew :greeter.runner:run

> Configure project :
Found module name 'greeter.api'

FAILURE: Build failed with an exception.

* Where:
Build file '..../gradle-modules-plugin/test-project/build.gradle' line: 44

* What went wrong:
A problem occurred evaluating root project 'moduleplugintests'.
> There's an extension registered with name 'patchModules'. You should not reassign it via a property setter.

* 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 0s

This was tested with the JDK from AdoptOpenJDK:

gradle-modules-plugin/test-project$ java -version
openjdk version "11" 2018-09-25
OpenJDK Runtime Environment AdoptOpenJDK (build 11+28)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11+28, mixed mode)

Reuse production ModuleOptions in test classes

Compilation of production and test sources rely on their own instance of moduleOptions, however test does not see any modifications applied to compileJava, nor does run for example.

Perhaps it would be a good idea for CompileTestJavaTask, TestTask, and RunTask to access the moduleOptions from CompileJava as well.

unexpected arguments in main

Hello,
plugin version - "1.4.0",
git - "gradle-modules-plugin-example",
project - "greater.runner'.
If we add to Runner.main(String[] args)
for (String arg: args) {
System.out.println(arg);
}
and run application, than we will find in output:
-Dfile.encoding=UTF-8
-Duser.country=US
-Duser.language=en
-Duser.variant
greeter.runner/examples.Runner
Hello and welcome!
It seems these arguments should not be.

Fix to support incremental builds

The compileJava is always not up-to-date because of the moduleplugin

Task ':compileJava' is not up-to-date because:
  Task ':compileJava' has an additional action that was implemented by the Java lambda 
 'org.javamodularity.moduleplugin.tasks.CompileTask$$Lambda$2
15/0x00000001005d0040'. Using Java lambdas is not supported, use an (anonymous) inner class instead.
All input files are considered out-of-date for incremental task ':compileJava'.

Does not matter if there is no changes in the java source, it will always run compileJava.

Proposal: Improved module name reporting

@paulbakker, let me know if you'd be interested in accepting a PR for improved module name reporting.

Module name found:

  • Currently, module name is reported at "lifecycle" level as:
    Found module name '{moduleName}'
  • Instead, I propose reporting as:
    {projectName}: found Java module named '{moduleName}'

Module name not found (module-info.java absent):

  • Currently, it's reported at "debug" level as:
    No module-info.java found in module {projectName}
  • Instead, I propose reporting at "lifecycle" level as:
    {projectName}: no module-info.java found

Rationale: the user will be able to:

  • see project name to module name mappings (e.g. to verify if they make sense)
  • see in which projects the plugin won't be applied (e.g. because they forgot adding a module-info.java)

Fails building with Javadoc

This plugin does not work when building with Javadoc

> Task :javadoc FAILED
/home/sverre/workspace/movies/src/main/java/module-info.java:6: error: module not found: 
javafx.controls
    requires javafx.controls;
               ^
/home/sverre/workspace/movies/src/main/java/module-info.java:7: error: module not found: javafx.fxml
    requires javafx.fxml;
               ^
/home/sverre/workspace/movies/src/main/java/module-info.java:8: error: module not found: javafx.web
    requires javafx.web;
               ^
/home/sverre/workspace/movies/src/main/java/module-info.java:9: error: module not found: 
javafx.graphics
    requires javafx.graphics;
                   ^
/home/sverre/workspace/movies/src/main/java/module-info.java:10: error: module not found: 
javafx.media
    requires javafx.media;
                   ^
/home/sverre/workspace/movies/src/main/java/module-info.java:12: error: module not found: 
org.apache.logging.log4j
    requires org.apache.logging.log4j;

build.gradle

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 jar
    archives sourcesJar
    archives javadocJar
}

Without this plugin I have a modules.gradle with the following Javadoc configuration:

javadoc {
    inputs.property("moduleName", moduleName)
    doFirst {
        exclude "**/module-info.java"
        options.addStringOption('-module-path', classpath.asPath)
        options.addStringOption('-add-modules', 'javafx.controls')
        options.addStringOption('-add-modules', 'javafx.fxml')
        options.addStringOption('-add-modules', 'javafx.web')
        options.addStringOption('-add-modules', 'javafx.graphics')
        options.addStringOption('-add-modules', 'javafx.media')
        options.addStringOption('-class-path', "")
        options.addBooleanOption('html5', true)
    }
}

Detect Hamcrest and others when testing?

Given that this plugin will already detect testing frameworks like JUnit and add the appropriate --add-reads and --add-opens, can we do the same thing for Hamcrest and other common testing libraries?

--patch-module must be declared before --module

The run task adds a --patch-modules jvm argument to the java process to allow it to access its resources. However, it seems that the option is ignored when the argument is added after the --modules argument.

Simply switching the arguments allows the java process to access its resources again.

Kotlin support

The plugin cannot be currently used to build Kotlin projects.
Do you think Kotlin support would be a useful feature?
If yes, I will submit a pull request shortly.

Patch module does not handle multiple jars

According to patch-module syntax from the jep, the --patch-module command should be able to support multiple argument jars. There seems to be partial support for this in PatchModuleExtension.java, using a partial matching. However, I could not get that to work

public PatchModuleResolver resolvePatched(FileCollection classpath) {
        return resolvePatched(jarName -> classpath.filter(jar -> jar.getName().endsWith(jarName)).getAsPath());
    }

Suggestion: Allow specifying a comma separated list of jars that will be resolved, like the addReads option currently does

The getClass().getResource returns null

Creating an URL to a resource suddenly returned null. I could not find out why.

Happened when running 'gradle run'.

URL location = getClass().getResource("movies.fxml");

Exception:

Caused by: java.lang.IllegalStateException: Location is not set.
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2459)
        at javafx.fxml/javafx.fxml.FXMLLoader.load(FXMLLoader.java:2435)
        at no.smeaworks.movies/no.smeaworks.movies.MoviesApplication.start(MoviesApplication.java:46)

Using the latest v1.2.0 of the plugin.

When I went back to not using the plugin it worked fine.
The run configuration in my own modules.gradle

run {
    inputs.property("moduleName", moduleName)
    doFirst {
        jvmArgs = [
            '--module-path', classpath.asPath,
            '--add-modules', 'javafx.controls',
            '--add-modules', 'javafx.fxml',
            '--add-modules', 'javafx.web',
            '--add-modules', 'javafx.graphics',
            '--add-modules', 'javafx.media'
        ]
    }
}

Problem only with the application plugin.
It works fine running the application from a runtime image.
Creating a runtime image with jpackager, OpenJDK 11.0.1, OpenJFX 11.0.1

Run task fails when opening resource-only package

I need to open up a resource package not containing any classes, but the run task fails.
Running the build task and then executing the distribution will work.

A workaround is creating a dummy class in the package.

I stumbled onto this bug while experimenting with OptaPlanner on the module path: mkroening/optaplanner-modulepath-example#2

I also modified the test-project to show this behaviour: mkroening@48c5793
Running $ ../gradlew :greeter.runner:run will fail, but $ ../gradlew :greeter.runner:build and then executing the distribution succeeds.

Feature request: handling split package modules issues

Certain well-known, often-used jar files exhibit split package issues when both were put on the module path. One pair that I encountered is the conflict between

"javax.annotation:javax.annotation-api:1.3.2"

and

"com.google.code.findbugs:jsr305:3.0.2"

Fortunately, this conflict can be resolved by putting only one of them on the module path and then patch it with the content of the other. This can be achieved by applying snippets of Gradle codes for the compileJava, compileTestJava, test, run, startScripts, and distributions tasks, such as the following:

compileJava {
    doFirst {
        def jsr305 = classpath.filter{it.name.contains('jsr305')}.asPath
        classpath = classpath.filter{!it.name.contains('jsr305')}
        options.compilerArgs += [
                '--patch-module', "java.annotation=$jsr305"
        ]
    }
}

Since these snippets are scattered repetitively throughout the build.gradle file, and the modifications they trigger are quite in line with that the moduleplugin does, I wonder if it makes sense for the moduleplugin to provide a declarative way to handle the situation.

Here's a simple test case that demonstrates the issue:

https://github.com/weiqigao/jsr305-split-package-issue

where the foo project is a non-modular Maven jar project that depends on both jars, and the bar project is a Gradle modular jar project that depends on the artifact of the foo project and therefore transitively depends on the conflicting two jars. The build.gradle in the bar project contains more snippets similar to the above but for the run, startScripts, and distributions tasks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.