Skip to content

Commit

Permalink
Merge pull request #75 from iherasymenko/going-fully-modular
Browse files Browse the repository at this point in the history
Enhancements: failOnAutomaticModules, Real Module Patching, and Module Specs Generation
  • Loading branch information
jjohannes authored Oct 31, 2023
2 parents 011cebb + bbe947c commit b7cb869
Show file tree
Hide file tree
Showing 11 changed files with 1,296 additions and 15 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,39 @@ extraJavaModuleInfo {
}
```

## I have many automatic modules in my project. How can I convert them into proper modules and control what they export or require?

The plugin provides a set of `<sourceSet>moduleDescriptorRecommendations` tasks that generate the real module declarations utilizing [jdeps](https://docs.oracle.com/en/java/javase/11/tools/jdeps.html) and dependency metadata.

This task generates module info spec for the JARs that do not contain the proper `module-info.class` descriptors.

NOTE: This functionality requires Gradle to be run with Java 11+ and failing on missing module information should be disabled via `failOnMissingModuleInfo.set(false)`.

## How can I ensure there are no automatic modules in my dependency graph?

If your goal is to fully modularize your application, you should enable the following configuration setting, which is disabled by default.

```
extraJavaModuleInfo {
failOnAutomaticModules.set(true)
}
```

With this setting enabled, the build will fail unless you define a module override for every automatic module that appears in your dependency tree, as shown below.

```
dependencies {
implementation("org.yaml:snakeyaml:1.33")
}
extraJavaModuleInfo {
failOnAutomaticModules.set(true)
module("org.yaml:snakeyaml", "org.yaml.snakeyaml") {
closeModule()
exports("org.yaml.snakeyaml")
}
}
```

## What do I do in a 'split package' situation?

The Java Module System does not allow the same package to be used in more than one _module_.
Expand All @@ -186,6 +219,23 @@ This plugin offers the option to merge multiple Jars into one in such situations

Note: The merged Jar will include the *first* appearance of duplicated files (like the `MANIFEST.MF`).

## How can I fix a library with a broken `module-info.class`?

To fix a library with a broken `module-info.class`, you can override the modular descriptor in the same way it is done with non-modular JARs. However, you need to specify `patchRealModule()` in order to avoid unintentional overrides.

```
extraJavaModuleInfo {
module("org.apache.tomcat.embed:tomcat-embed-core", "org.apache.tomcat.embed.core") {
patchRealModule()
requires("java.desktop")
requires("java.instrument")
...
}
}
```

This opt-in behavior is designed to prevent over-patching real modules, especially during version upgrades. For example, when a newer version of a library already contains the proper `module-info.class`, the extra module info overrides should be removed.

# Disclaimer

Gradle and the Gradle logo are trademarks of Gradle, Inc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,16 @@
import org.gradle.api.file.Directory;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFile;
import org.gradle.api.plugins.HelpTasksPlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.util.GradleVersion;
import org.gradlex.javamodule.moduleinfo.tasks.ModuleDescriptorRecommendation;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -63,9 +64,60 @@ public void apply(Project project) {
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
ExtraJavaModuleInfoPluginExtension extension = project.getExtensions().create("extraJavaModuleInfo", ExtraJavaModuleInfoPluginExtension.class);
extension.getFailOnMissingModuleInfo().convention(true);
extension.getFailOnAutomaticModules().convention(false);

// setup the transform for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
// setup the transform and the tasks for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> {
configureTransform(project, extension);
configureModuleDescriptorTasks(project);
});
}

private void configureModuleDescriptorTasks(Project project) {
project.getExtensions().getByType(SourceSetContainer.class).all(sourceSet -> {
String name = sourceSet.getTaskName("", "moduleDescriptorRecommendations");
project.getTasks().register(name, ModuleDescriptorRecommendation.class, task -> {
Transformer<@Nullable List<File>, Configuration> artifactsTransformer = configuration -> {
//noinspection CodeBlock2Expr
return configuration.getIncoming()
.getArtifacts()
.getArtifacts()
.stream()
.sorted(Comparator.comparing(artifact -> artifact.getId().getComponentIdentifier().toString()))
.map(ResolvedArtifactResult::getFile)
.collect(Collectors.toList());
};

Transformer<@Nullable List<ResolvedComponentResult>, Configuration> componentsTransformer = configuration -> {
Set<ComponentIdentifier> artifacts = configuration.getIncoming()
.getArtifacts()
.getArtifacts()
.stream()
.map(artifact -> artifact.getId().getComponentIdentifier())
.collect(Collectors.toSet());
return configuration.getIncoming()
.getResolutionResult()
.getAllComponents()
.stream()
.filter(component -> artifacts.contains(component.getId()))
.sorted(Comparator.comparing(artifact -> artifact.getId().toString()))
.collect(Collectors.toList());
};

Provider<Configuration> compileClasspath = project.getConfigurations().named(sourceSet.getCompileClasspathConfigurationName());
task.getCompileArtifacts().set(compileClasspath.map(artifactsTransformer));
task.getCompileResolvedComponentResults().set(compileClasspath.map(componentsTransformer));

Provider<Configuration> runtimeClasspath = project.getConfigurations().named(sourceSet.getRuntimeClasspathConfigurationName());
task.getRuntimeArtifacts().set(runtimeClasspath.map(artifactsTransformer));
task.getRuntimeResolvedComponentResults().set(runtimeClasspath.map(componentsTransformer));

task.getRelease().convention(21);

task.setGroup(HelpTasksPlugin.HELP_GROUP);
task.setDescription("Generates module descriptors for 'org.gradlex.extra-java-module-info' plugin based on the dependency and class file analysis of automatic modules and non-modular dependencies");
});
});
}

private void configureTransform(Project project, ExtraJavaModuleInfoPluginExtension extension) {
Expand Down Expand Up @@ -121,6 +173,7 @@ private void registerTransform(String fileExtension, Project project, ExtraJavaM
t.parameters(p -> {
p.getModuleSpecs().set(extension.getModuleSpecs());
p.getFailOnMissingModuleInfo().set(extension.getFailOnMissingModuleInfo());
p.getFailOnAutomaticModules().set(extension.getFailOnAutomaticModules());

// See: https://github.com/adammurdoch/dependency-graph-as-task-inputs/blob/main/plugins/src/main/java/TestPlugin.java
Provider<Set<ResolvedArtifactResult>> artifacts = project.provider(() ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public abstract class ExtraJavaModuleInfoPluginExtension {

public abstract MapProperty<String, ModuleSpec> getModuleSpecs();
public abstract Property<Boolean> getFailOnMissingModuleInfo();
public abstract Property<Boolean> getFailOnAutomaticModules();

/**
* Add full module information for a given Jar file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public interface Parameter extends TransformParameters {
@Input
Property<Boolean> getFailOnMissingModuleInfo();
@Input
Property<Boolean> getFailOnAutomaticModules();
@Input
ListProperty<String> getMergeJarIds();
@InputFiles
ListProperty<RegularFile> getMergeJars();
Expand All @@ -99,25 +101,38 @@ public interface Parameter extends TransformParameters {

@Override
public void transform(TransformOutputs outputs) {
Map<String, ModuleSpec> moduleSpecs = getParameters().getModuleSpecs().get();
Parameter parameters = getParameters();
Map<String, ModuleSpec> moduleSpecs = parameters.getModuleSpecs().get();
File originalJar = getInputArtifact().get().getAsFile();

ModuleSpec moduleSpec = findModuleSpec(originalJar);

if (willBeMerged(originalJar, moduleSpecs.values())) { // No output if this Jar will be merged
return;
}

boolean realModule = isModule(originalJar);
if (moduleSpec instanceof ModuleInfo) {
if (realModule && !((ModuleInfo) moduleSpec).patchRealModule) {
throw new RuntimeException("Patching of real modules must be explicitly enabled with 'patchRealModule()'");
}
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), (ModuleInfo) moduleSpec);
} else if (moduleSpec instanceof AutomaticModuleName) {
if (realModule) {
throw new RuntimeException("Patching of real modules must be explicitly enabled with 'patchRealModule()' and can only be done with 'module()'");
}
if (parameters.getFailOnAutomaticModules().get()) {
throw new RuntimeException("Use of 'automaticModule()' is prohibited. Use 'module()' instead: " + originalJar.getName());
}
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), (AutomaticModuleName) moduleSpec);
} else if (isModule(originalJar)) {
} else if (realModule) {
outputs.file(originalJar);
} else if (isAutoModule(originalJar)) {
if (parameters.getFailOnAutomaticModules().get()) {
throw new RuntimeException("Found an automatic module: " + originalJar.getName());
}
outputs.file(originalJar);
} else {
if (getParameters().getFailOnMissingModuleInfo().get()) {
if (parameters.getFailOnMissingModuleInfo().get()) {
throw new RuntimeException("Not a module and no mapping defined: " + originalJar.getName());
} else {
outputs.file(originalJar);
Expand Down Expand Up @@ -254,7 +269,7 @@ private void copyAndExtractProviders(JarInputStream inputStream, JarOutputStream
providers.get(key).addAll(extractImplementations(content));
}

if (!JAR_SIGNATURE_PATH.matcher(entryName).matches() && !"META-INF/MANIFEST.MF".equals(jarEntry.getName())) {
if (!JAR_SIGNATURE_PATH.matcher(entryName).matches() && !"META-INF/MANIFEST.MF".equals(entryName) && !isModuleInfoClass(entryName)) {
if (!willMergeJars || !isFileInServicesFolder) { // service provider files will be merged later
jarEntry.setCompressedSize(-1);
try {
Expand Down Expand Up @@ -432,4 +447,8 @@ private String gaToModuleName(String ga) {
}
return moduleSpec.getModuleName();
}

private static boolean isModuleInfoClass(String jarEntryName) {
return "module-info.class".equals(jarEntryName) || MODULE_INFO_CLASS_MRJAR_PATH.matcher(jarEntryName).matches();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class ModuleInfo extends ModuleSpec {

boolean exportAllPackages;
boolean requireAllDefinedDependencies;
boolean patchRealModule;

ModuleInfo(String identifier, String moduleName, String moduleVersion, ObjectFactory objectFactory) {
super(identifier, moduleName);
Expand Down Expand Up @@ -131,4 +132,12 @@ public void exportAllPackages() {
public void requireAllDefinedDependencies() {
this.requireAllDefinedDependencies = true;
}

/**
* Explicitly allow patching real (JARs with module-info.class) modules
*/
public void patchRealModule() {
this.patchRealModule = true;
}

}
Loading

0 comments on commit b7cb869

Please sign in to comment.