Skip to content

Separate compilation of module-info.java #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 94 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,10 @@ See `src/test/java/module-info.test` and `src/test/java/greeter/ScriptingTest.ja
Fall-back to classpath mode
----

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

<details open>
<summary>Groovy DSL</summary>

```groovy
test {
Expand All @@ -275,6 +278,22 @@ test {
}
```

</details>
<details>
<summary>Kotlin DSL</summary>

```kotlin
tasks {
test {
extensions.configure(TestModuleOptions::class) {
runOnClasspath = true
}
}
}
```

</details>

Blackbox testing
===

Expand All @@ -286,7 +305,7 @@ This module `requires` and/or `uses` the module under test, and tests it's exter
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 the module it's testing, e.g. `greeter.provider.test`.
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.

Expand Down Expand Up @@ -473,6 +492,75 @@ patchModules.config = [
]
```

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.:
- Java 11, which is the current [Long-Term Support (LTS) release](https://www.oracle.com/technetwork/java/java-se-support-roadmap.html),
- Java 8, whose production use in 2018 was almost 85%, according to [this survey](https://www.baeldung.com/java-in-2018).

You can do that by setting the Java compiler [`--release`][javacRelease] 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`](https://docs.oracle.com/en/java/javase/11/tools/javac.html) option,
- JDK 12: you can only target Java 7-12 using its
[`--release`](https://docs.oracle.com/en/java/javase/12/tools/javac.html) 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 &mdash; see below).

Concluding, to configure your project to support JPMS and target:
- Java **6-8**: call the [`modularity.mixedJavaRelease`][ModularityExtension] function
(see [Separate compilation of `module-info.java`](#separate-compilation-of-module-infojava) for details),
- Java **9+**: call the [`modularity.standardJavaRelease`][ModularityExtension] function,

and the plugin will take care of setting the [`--release`][javacRelease] 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 `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`][ModularityExtension] function, which implicitly sets
`compileJava.compileModuleInfoSeparately = true` and configures the [`--release`][javacRelease] 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)`:

<details open>
<summary>Groovy DSL</summary>

```groovy
modularity.mixedJavaRelease 8
```

</details>
<details>
<summary>Kotlin DSL</summary>

```kotlin
modularity.mixedJavaRelease(8)
```

</details>

Note that `modularity.mixedJavaRelease` does *not* configure a
[multi-release JAR](https://docs.oracle.com/javase/9/docs/specs/jar/jar.html#Multi-release)
(in other words, `module-info.class` remains in the root directory of the JAR).

Limitations
===

Expand All @@ -495,3 +583,7 @@ 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.


[javacRelease]: http://openjdk.java.net/jeps/247
[ModularityExtension]: src/main/java/org/javamodularity/moduleplugin/extensions/ModularityExtension.java
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ test {

testLogging {
events 'PASSED', 'FAILED', 'SKIPPED'
stackTraceFilters = []
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.javamodularity.moduleplugin;

import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.compile.JavaCompile;

import java.util.Optional;

/**
* Generic helper for Gradle {@link Project} API that has {@link JavaPlugin} applied.
*/
public final class JavaProjectHelper {

private final Project project;

public JavaProjectHelper(Project project) {
this.project = project;
}

public Project project() {
return project;
}

//region SOURCE SETS
public SourceSetContainer sourceSets() {
return project.getExtensions().getByType(SourceSetContainer.class);
}

public SourceSet sourceSet(String sourceSetName) {
return sourceSets().getByName(sourceSetName);
}

public SourceSet mainSourceSet() {
return sourceSet(SourceSet.MAIN_SOURCE_SET_NAME);
}

public SourceSet testSourceSet(String sourceSetName) {
return sourceSet(SourceSet.TEST_SOURCE_SET_NAME);
}
//endregion

//region TASKS
public Task task(String taskName) {
return project.getTasks().getByName(taskName);
}

public JavaCompile compileJavaTask(String taskName) {
return (JavaCompile) task(taskName);
}

public Optional<Task> findTask(String taskName) {
return Optional.ofNullable(project.getTasks().findByName(taskName));
}

public Optional<JavaCompile> findCompileJavaTask(String taskName) {
return findTask(taskName).map(JavaCompile.class::cast);
}
//endregion

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,33 @@

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.plugins.JavaPlugin;
import org.javamodularity.moduleplugin.extensions.DefaultModularityExtension;
import org.javamodularity.moduleplugin.extensions.ModularityExtension;
import org.javamodularity.moduleplugin.tasks.*;

import java.util.Optional;

public class ModuleSystemPlugin implements Plugin<Project> {

@Override
public void apply(Project project) {
project.getPlugins().apply(JavaPlugin.class);
Optional<String> foundModuleName = new ModuleName().findModuleName(project);
foundModuleName.ifPresent(moduleName -> {
project.getExtensions().add("moduleName", moduleName);
project.getExtensions().create("patchModules", PatchModuleExtension.class);
new ModuleName().findModuleName(project).ifPresent(moduleName -> configureModularity(project, moduleName));
}

private void configureModularity(Project project, String moduleName) {
ExtensionContainer extensions = project.getExtensions();
extensions.add("moduleName", moduleName);
extensions.create("patchModules", PatchModuleExtension.class);
extensions.create(ModularityExtension.class, "modularity", DefaultModularityExtension.class, project);

new CompileTask().configureCompileJava(project);
new CompileTestTask().configureCompileTestJava(project, moduleName);
new TestTask().configureTestJava(project, moduleName);
new RunTask().configureRun(project, moduleName);
new JavadocTask().configureJavaDoc(project);
ModularJavaExec.configure(project, moduleName);
ModularCreateStartScripts.configure(project, moduleName);
});
new CompileTask(project).configureCompileJava();
new CompileModuleInfoTask(project).configureCompileModuleInfoJava();
new CompileTestTask().configureCompileTestJava(project, moduleName);
new TestTask().configureTestJava(project, moduleName);
new RunTask().configureRun(project, moduleName);
new JavadocTask().configureJavaDoc(project);
ModularJavaExec.configure(project, moduleName);
ModularCreateStartScripts.configure(project, moduleName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.javamodularity.moduleplugin.extensions;

import org.gradle.api.Project;
import org.gradle.api.tasks.compile.JavaCompile;
import org.javamodularity.moduleplugin.tasks.ModuleOptions;

public class CompileModuleOptions extends ModuleOptions {

/**
* Name of the extra Java compile task created if {@code compileModuleInfoSeparately} is {@code true}.
*/
public static final String COMPILE_MODULE_INFO_TASK_NAME = "compileModuleInfoJava";

private final Project project;

private boolean compileModuleInfoSeparately = false;

public CompileModuleOptions(Project project) {
super(project);
this.project = project;
}

public boolean getCompileModuleInfoSeparately() {
return compileModuleInfoSeparately;
}

public void setCompileModuleInfoSeparately(boolean compileModuleInfoSeparately) {
if (compileModuleInfoSeparately) {
// we need to create "compileModuleInfoJava" task eagerly so that the user can configure it immediately
project.getTasks().maybeCreate(COMPILE_MODULE_INFO_TASK_NAME, JavaCompile.class);
}
this.compileModuleInfoSeparately = compileModuleInfoSeparately;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.javamodularity.moduleplugin.extensions;

import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.tasks.compile.JavaCompile;
import org.javamodularity.moduleplugin.JavaProjectHelper;

import java.util.List;

public class DefaultModularityExtension implements ModularityExtension {

private final Project project;

public DefaultModularityExtension(Project project) {
this.project = project;
}

@Override
public void standardJavaRelease(int mainJavaRelease) {
if (mainJavaRelease < 9) {
throw new IllegalArgumentException(String.format(
"Invalid main --release value: %d. Use 'mixedJavaRelease' instead.", mainJavaRelease
));
}
project.afterEvaluate(p -> configureStandardJavaRelease(mainJavaRelease));
}

private void configureStandardJavaRelease(int mainJavaRelease) {
JavaCompile compileJava = helper().compileJavaTask(JavaPlugin.COMPILE_JAVA_TASK_NAME);
setJavaRelease(compileJava, mainJavaRelease);
}

@Override
public void mixedJavaRelease(int mainJavaRelease, int moduleInfoJavaRelease) {
validateMixedJavaReleaseArgs(mainJavaRelease, moduleInfoJavaRelease);

CompileModuleOptions moduleOptions = helper().compileJavaTask(JavaPlugin.COMPILE_JAVA_TASK_NAME)
.getExtensions().getByType(CompileModuleOptions.class);
moduleOptions.setCompileModuleInfoSeparately(true);

project.afterEvaluate(p -> configureMixedJavaRelease(mainJavaRelease, moduleInfoJavaRelease));
}

private static void validateMixedJavaReleaseArgs(int mainJavaRelease, int moduleInfoJavaRelease) {
if (mainJavaRelease < 6) {
throw new IllegalArgumentException("Invalid main --release value: " + mainJavaRelease);
}
if (mainJavaRelease > 8) {
throw new IllegalArgumentException(String.format(
"Invalid main --release value: %d. Use 'standardJavaRelease' instead.", mainJavaRelease
));
}
if (moduleInfoJavaRelease < 9) {
throw new IllegalArgumentException("Invalid module-info --release value: " + moduleInfoJavaRelease);
}
}

private void configureMixedJavaRelease(int mainJavaRelease, int moduleInfoJavaRelease) {
var compileJava = helper().compileJavaTask(JavaPlugin.COMPILE_JAVA_TASK_NAME);
setJavaRelease(compileJava, mainJavaRelease);

var compileModuleInfoJava = helper().compileJavaTask(CompileModuleOptions.COMPILE_MODULE_INFO_TASK_NAME);
setJavaRelease(compileModuleInfoJava, moduleInfoJavaRelease);
}

// TODO: Remove this method when Gradle supports it natively: https://github.com/gradle/gradle/issues/2510
private void setJavaRelease(JavaCompile javaCompile, int javaRelease) {
String currentJavaVersion = JavaVersion.current().toString();
if (!javaCompile.getSourceCompatibility().equals(currentJavaVersion)) {
throw new IllegalStateException("sourceCompatibility should not be set together with --release option");
}
if (!javaCompile.getTargetCompatibility().equals(currentJavaVersion)) {
throw new IllegalStateException("targetCompatibility should not be set together with --release option");
}

List<String> compilerArgs = javaCompile.getOptions().getCompilerArgs();
if (compilerArgs.contains("--release")) {
throw new IllegalStateException("--release option is already set in compiler args");
}

compilerArgs.add("--release");
compilerArgs.add(String.valueOf(javaRelease));
}

private JavaProjectHelper helper() {
return new JavaProjectHelper(project);
}

}
Loading