diff --git a/CHANGELOG.md b/CHANGELOG.md index 239b0a3..4866d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index a7bcac6..2261c49 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -263,7 +272,7 @@ The plugin provides a set of `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? @@ -271,7 +280,7 @@ If your goal is to fully modularize your application, you should enable the foll ``` extraJavaModuleInfo { - failOnAutomaticModules.set(true) + failOnAutomaticModules = true } ``` @@ -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") @@ -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 } ``` diff --git a/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPlugin.java b/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPlugin.java index 2a5c1ac..87d721c 100644 --- a/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPlugin.java +++ b/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPlugin.java @@ -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 @@ -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 diff --git a/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPluginExtension.java b/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPluginExtension.java index 811901b..a80e6bb 100644 --- a/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPluginExtension.java +++ b/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPluginExtension.java @@ -40,6 +40,7 @@ public abstract class ExtraJavaModuleInfoPluginExtension { public abstract MapProperty getModuleSpecs(); public abstract Property getFailOnMissingModuleInfo(); public abstract Property getFailOnAutomaticModules(); + public abstract Property getSkipLocalJars(); public abstract Property getDeriveAutomaticModuleNamesFromFileNames(); public abstract Property getVersionsProvidingConfiguration(); diff --git a/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoTransform.java b/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoTransform.java index eed486a..96fac44 100644 --- a/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoTransform.java +++ b/src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoTransform.java @@ -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 @@ -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 moduleSpecs = getParameters().getModuleSpecs().get(); @@ -219,15 +234,6 @@ private boolean willBeMerged(File originalJar, Collection 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(); diff --git a/src/test/groovy/org/gradlex/javamodule/moduleinfo/test/LocalJarTransformFunctionalTest.groovy b/src/test/groovy/org/gradlex/javamodule/moduleinfo/test/LocalJarTransformFunctionalTest.groovy new file mode 100644 index 0000000..892793b --- /dev/null +++ b/src/test/groovy/org/gradlex/javamodule/moduleinfo/test/LocalJarTransformFunctionalTest.groovy @@ -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("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]" + } +} diff --git a/src/test/groovy/org/gradlex/javamodule/moduleinfo/test/fixture/GradleBuild.groovy b/src/test/groovy/org/gradlex/javamodule/moduleinfo/test/fixture/GradleBuild.groovy index 089a259..206bfe8 100644 --- a/src/test/groovy/org/gradlex/javamodule/moduleinfo/test/fixture/GradleBuild.groovy +++ b/src/test/groovy/org/gradlex/javamodule/moduleinfo/test/fixture/GradleBuild.groovy @@ -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()'