Skip to content

Add skipLocalJars option and error message when used at configuration time #165

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 1 commit into from
Feb 14, 2025
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Extra Java Module Info Gradle Plugin - Changelog

## Version 1.11
* [New] [#161](https://github.com/gradlex-org/extra-java-module-info/pull/161) - Add 'skipLocalJars' option
* [New] [#106](https://github.com/gradlex-org/extra-java-module-info/pull/106) - Actionable error message when plugin is used at configuration time

## Version 1.10.1
* [Fix] [#164](https://github.com/gradlex-org/extra-java-module-info/pull/164) - fix: 'preserveExisting' does not duplicate 'provides' entries

Expand Down
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ plugins {

// add module information for all direct and transitive dependencies that are not modules
extraJavaModuleInfo {
// failOnMissingModuleInfo.set(false)
// failOnMissingModuleInfo = false
// failOnAutomaticModules = true
// skipLocalJars = true
module("commons-beanutils:commons-beanutils", "org.apache.commons.beanutils") {
exports("org.apache.commons.beanutils")
// or granuarly allowing access to a package by specific modules
Expand Down Expand Up @@ -164,6 +166,13 @@ sourceSets.all {
}
```

## How do I deactivate the plugin functionality for my own Jars?

A major use case of the plugin is to transform Jars from 3rd party repositories that you do not control.
By default, however, the plugin looks at all Jars on the module paths – including the Jars Gradle builds from you own modules.
This is working well in most cases. The jars are analyzed and the plugin detects that they are infact modules and does not modify them.
You can still optimize the plugin execution to completely skip analysis of locally-built Jars by setting `skipLocalJars = true`.

## How do I add `provides ... with ...` declarations to the `module-info.class` descriptor?

The plugin will automatically retrofit all the available `META-INF/services/*` descriptors into `module-info.class` for you. The `META-INF/services/*` descriptors will be preserved so that a transformed JAR will continue to work if it is placed on the classpath.
Expand Down Expand Up @@ -263,15 +272,15 @@ The plugin provides a set of `<sourceSet>moduleDescriptorRecommendations` tasks

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)`.
NOTE: This functionality requires Gradle to be run with Java 11+ and failing on missing module information should be disabled via `failOnMissingModuleInfo = 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)
failOnAutomaticModules = true
}
```

Expand All @@ -282,7 +291,7 @@ dependencies {
implementation("org.yaml:snakeyaml:1.33")
}
extraJavaModuleInfo {
failOnAutomaticModules.set(true)
failOnAutomaticModules = true
module("org.yaml:snakeyaml", "org.yaml.snakeyaml") {
closeModule()
exports("org.yaml.snakeyaml")
Expand Down Expand Up @@ -351,7 +360,7 @@ However, if you get started and just want things to be put on the Module Path, y

```
extraJavaModuleInfo {
deriveAutomaticModuleNamesFromFileNames.set(true)
deriveAutomaticModuleNamesFromFileNames = true
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public void apply(Project project) {
ExtraJavaModuleInfoPluginExtension extension = project.getExtensions().create("extraJavaModuleInfo", ExtraJavaModuleInfoPluginExtension.class);
extension.getFailOnMissingModuleInfo().convention(true);
extension.getFailOnAutomaticModules().convention(false);
extension.getSkipLocalJars().convention(false);
extension.getDeriveAutomaticModuleNamesFromFileNames().convention(false);

// setup the transform and the tasks for all projects in the build
Expand Down Expand Up @@ -166,11 +167,32 @@ private void configureTransform(Project project, ExtraJavaModuleInfoPluginExtens
Configuration runtimeClasspath = project.getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName());
Configuration compileClasspath = project.getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName());
Configuration annotationProcessor = project.getConfigurations().getByName(sourceSet.getAnnotationProcessorConfigurationName());
Configuration runtimeElements = project.getConfigurations().findByName(sourceSet.getRuntimeElementsConfigurationName());
Configuration apiElements = project.getConfigurations().findByName(sourceSet.getApiElementsConfigurationName());

// compile, runtime and annotation processor classpaths express that they only accept modules by requesting the javaModule=true attribute
runtimeClasspath.getAttributes().attribute(javaModule, true);
compileClasspath.getAttributes().attribute(javaModule, true);
annotationProcessor.getAttributes().attribute(javaModule, true);

// outgoing variants may express that they already provide a modular Jar and can hence skip the transform altogether
if (GradleVersion.current().compareTo(GradleVersion.version("7.4")) >= 0) {
if (runtimeElements != null) {
runtimeElements.getOutgoing().getAttributes().attributeProvider(javaModule, extension.getSkipLocalJars());
}
if (apiElements != null) {
apiElements.getOutgoing().getAttributes().attributeProvider(javaModule, extension.getSkipLocalJars());
}
} else {
project.afterEvaluate(p -> {
if (runtimeElements != null) {
runtimeElements.getOutgoing().getAttributes().attribute(javaModule, extension.getSkipLocalJars().get());
}
if (apiElements != null) {
apiElements.getOutgoing().getAttributes().attribute(javaModule, extension.getSkipLocalJars().get());
}
});
}
});

// Jars may be transformed (or merged into) Module Jars
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public abstract class ExtraJavaModuleInfoPluginExtension {
public abstract MapProperty<String, ModuleSpec> getModuleSpecs();
public abstract Property<Boolean> getFailOnMissingModuleInfo();
public abstract Property<Boolean> getFailOnAutomaticModules();
public abstract Property<Boolean> getSkipLocalJars();
public abstract Property<Boolean> getDeriveAutomaticModuleNamesFromFileNames();
public abstract Property<String> getVersionsProvidingConfiguration();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ public void transform(TransformOutputs outputs) {
return;
}

checkInputExists(originalJar);

// We return the original Jar without further analysis, if there is
// (1) no spec (2) no auto-module check (3) no missing module-info check (4) no auto-name derivation
if (moduleSpec == null
Expand Down Expand Up @@ -184,6 +186,19 @@ public void transform(TransformOutputs outputs) {
}
}

private void checkInputExists(File jar) {
if (!jar.isFile()) {
// If the jar does not exist, it is most likely a locally-built Jar that does not yet exist because the
// transform was triggered at configuration time. See:
// - https://github.com/gradle/gradle/issues/26155
// - https://github.com/gradlex-org/extra-java-module-info/issues/15
// - https://github.com/gradlex-org/extra-java-module-info/issues/78
throw new RuntimeException("File does not exist: " + jar
+ "\n This is likely because a tool or another plugin performs early dependency resolution."
+ "\n You can prevent this error by setting 'skipLocalJars = true'.");
}
}

@Nullable
private ModuleSpec findModuleSpec(File originalJar) {
Map<String, ModuleSpec> moduleSpecs = getParameters().getModuleSpecs().get();
Expand Down Expand Up @@ -219,15 +234,6 @@ private boolean willBeMerged(File originalJar, Collection<ModuleSpec> modules) {
}

private boolean isModule(File jar) {
if (!jar.isFile()) {
// If the jar does not exist, we assume that the file, which is produced later is a local artifact and a module.
// For local files this behavior is ok, because this transform is targeting published artifacts.
// Still, this can cause an error: https://github.com/gradle/gradle/issues/27372
// See also:
// - https://github.com/gradlex-org/extra-java-module-info/issues/15
// - https://github.com/gradlex-org/extra-java-module-info/issues/78
return true;
}
try (JarInputStream inputStream = new JarInputStream(Files.newInputStream(jar.toPath()))) {
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
ZipEntry next = inputStream.getNextEntry();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package org.gradlex.javamodule.moduleinfo.test

import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild
import spock.lang.Specification

class LocalJarTransformFunctionalTest extends Specification {

@Delegate
GradleBuild build = new GradleBuild()

def setup() {
settingsFile << '''
rootProject.name = "test-project"
include(":sub")
'''
file("sub/build.gradle.kts") << '''
plugins {
id("java-library")
id("org.gradlex.extra-java-module-info")
id("maven-publish")
}
'''
buildFile << '''
plugins {
id("java-library")
id("org.gradlex.extra-java-module-info")
}
dependencies {
implementation(project(":sub"))
}
'''
}

def "a locally produced Jar is transformed"() {
given:
buildFile << '''
extraJavaModuleInfo {
// transform local Jar to assert that it has gone through transformation
module("sub.jar", "org.example.sub")
}
tasks.register("printCP") {
inputs.files(configurations.runtimeClasspath)
doLast { println(inputs.files.files.map { it.name }) }
}
'''

when:
def result = task('printCP', '-q')

then:
result.output.trim() == "[sub-module.jar]"
}

def "transformation of locally produced Jars can be deactivates"() {
given:
buildFile << '''
tasks.register("printCP") {
inputs.files(configurations.runtimeClasspath)
doLast { println(inputs.files.files.map { it.name }) }
}
'''
file("sub/build.gradle.kts") << """
extraJavaModuleInfo { skipLocalJars.set(true) }
"""

when:
def result = task('printCP', '-q')

then:
result.output.trim() == "[sub.jar]"
}


def "deactivation of locally produced Jars does not cause additional attributes to be published"() {
given:
def repo = file("repo")
file("sub/build.gradle.kts") << """
group = "foo"
version = "1"
publishing {
publications.create<MavenPublication>("lib").from(components["java"])
repositories.maven("${repo.absolutePath}")
}
extraJavaModuleInfo { skipLocalJars.set(true) }
"""

when:
task('publish')

then:
!new File(repo, 'foo/sub/1/sub-1.module').text.contains('"javaModule":')
}

def "if transform fails due to missing local Jar, an actionable error message is given"() {
given:
buildFile << '''
tasks.register("printCP") {
inputs.files(configurations.runtimeClasspath.get().files) // provoke error: access at configuration time
doLast { println(inputs.files.files.map { it.name }) }
}
'''

when:
def result = failTask('printCP', '-q')

then:
result.output.contains("File does not exist:")
result.output.contains("You can prevent this error by setting 'skipLocalJars = true'")
}

def "resolving early does not fail if transformation is disabled for locally produced Jars"() {
given:
buildFile << '''
tasks.register("printCP") {
inputs.files(configurations.runtimeClasspath.get().files) // provoke resolution at configuration time
doLast { println(inputs.files.files.map { it.name }) }
}
'''
file("sub/build.gradle.kts") << '''
extraJavaModuleInfo { skipLocalJars.set(true) }
'''

when:
def result = task('printCP', '-q')

then:
result.output.trim() == "[sub.jar]"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class GradleBuild {
runner(taskNames).build()
}

BuildResult failTask(String... taskNames) {
runner(taskNames).buildAndFail()
}

GradleRunner runner(String... args) {
if (buildFile.exists()) {
buildFile << '\nrepositories.mavenCentral()'
Expand Down
Loading