diff --git a/CHANGELOG.md b/CHANGELOG.md index d4cf1680..f5234638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Java Module Dependencies Gradle Plugin - Changelog +## Version 1.12 +* [#245](https://github.com/gradlex-org/java-module-dependencies/issues/195) Add option to generate META-INF/services configuration files +* [#245](https://github.com/gradlex-org/java-module-dependencies/issues/175) Improve parsing of module-info +* Update module name mappings + ## Version 1.11 * [#245](https://github.com/gradlex-org/java-module-dependencies/issues/245) Add 'allLocalModules' access to extension * [#245](https://github.com/gradlex-org/java-module-dependencies/issues/247) Defining a 'versions' project in settings supports applying additional plugins diff --git a/README.MD b/README.MD index 17615687..0691d06e 100644 --- a/README.MD +++ b/README.MD @@ -297,6 +297,17 @@ org-junit-jupiter-api = "5.7.2" - Note that the TOML notation does not support `.` as separater in the Module Names, but allows you to use `_` or `-` instead. - _If_ you use a catalog with a custom name (not `libs`), you can tell the plugin using `versionCatalogName.set("customName")`. +## Generate META-INF/services configuration files + +Turn on the following option to generate service provider configuration files in `META_INF/services` for all +`provides ... with ...` directives in module-info files. + +```kotlin +javaModuleDependencies { + generateMetaInfServices() +} +``` + ## Find the latest stable version of a Module The `recommendModuleVersions` help task prints the latest available versions of the Modules you require. diff --git a/src/main/java/org/gradlex/javamodule/dependencies/JavaModuleDependenciesExtension.java b/src/main/java/org/gradlex/javamodule/dependencies/JavaModuleDependenciesExtension.java index 4c2360ae..dc3c6a77 100644 --- a/src/main/java/org/gradlex/javamodule/dependencies/JavaModuleDependenciesExtension.java +++ b/src/main/java/org/gradlex/javamodule/dependencies/JavaModuleDependenciesExtension.java @@ -46,9 +46,13 @@ import org.gradle.api.provider.Provider; import org.gradle.api.provider.ProviderFactory; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; +import org.gradle.language.jvm.tasks.ProcessResources; import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfo; import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfoCache; +import org.gradlex.javamodule.dependencies.tasks.MetaInfServicesGenerate; import org.gradlex.javamodule.dependencies.tasks.SyntheticModuleInfoFoldersGenerate; import org.jspecify.annotations.Nullable; @@ -144,6 +148,31 @@ private Provider> parsedModulesProperties() { }); } + /** + * If a module-info.java contains 'provides' directives, generate the corresponding META_INF/services entries for backward compatibility to load the module on the classpath. + */ + public void generateMetaInfServices() { + SourceSetContainer sourceSets = getProject().getExtensions().getByType(SourceSetContainer.class); + sourceSets.all(sourceSet -> { + ModuleInfo moduleInfo = getModuleInfoCache().get().get(sourceSet, getProviders()); + if (!moduleInfo.getProvides().isEmpty()) { + String taskName = sourceSet.getTaskName("generate", "MetaInfServices"); + Provider destinationDirectory = + getLayout().getBuildDirectory().dir("tmp/" + taskName); + Provider generateMetaInfServices = getTasks() + .register(taskName, MetaInfServicesGenerate.class, t -> { + t.getModuleInfo().convention(moduleInfo); + t.getDestinationDirectory().convention(destinationDirectory); + }); + getTasks() + .named( + sourceSet.getProcessResourcesTaskName(), + ProcessResources.class, + t -> t.from(generateMetaInfServices)); + } + }); + } + /** * Converts 'Module Name' to GA coordinates that can be used in * dependency declarations as String: "group:name" @@ -562,4 +591,7 @@ private Provider errorIfNotFound(Provider moduleName) { @Inject protected abstract ConfigurationContainer getConfigurations(); + + @Inject + protected abstract TaskContainer getTasks(); } diff --git a/src/main/java/org/gradlex/javamodule/dependencies/tasks/MetaInfServicesGenerate.java b/src/main/java/org/gradlex/javamodule/dependencies/tasks/MetaInfServicesGenerate.java new file mode 100644 index 00000000..82481b68 --- /dev/null +++ b/src/main/java/org/gradlex/javamodule/dependencies/tasks/MetaInfServicesGenerate.java @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +package org.gradlex.javamodule.dependencies.tasks; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; +import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfo; + +@CacheableTask +public abstract class MetaInfServicesGenerate extends DefaultTask { + + @Input + @Optional + public abstract Property getModuleInfo(); + + @OutputDirectory + public abstract DirectoryProperty getDestinationDirectory(); + + @TaskAction + public void generateServiceProviderConfigurationFiles() throws IOException { + Map> serviceProvides = getModuleInfo().get().getProvides(); + + Path services = getDestinationDirectory() + .dir("META-INF/services") + .get() + .getAsFile() + .toPath(); + + Files.createDirectories(services); + + for (String service : serviceProvides.keySet()) { + Path configurationFile = services.resolve(service); + String serviceProvider = String.join("\n", serviceProvides.get(service)); + Files.write(configurationFile, serviceProvider.getBytes()); + } + } +} diff --git a/src/test/java/org/gradlex/javamodule/dependencies/test/provides/GenerateMetaInfServicesTest.java b/src/test/java/org/gradlex/javamodule/dependencies/test/provides/GenerateMetaInfServicesTest.java new file mode 100644 index 00000000..b09e9127 --- /dev/null +++ b/src/test/java/org/gradlex/javamodule/dependencies/test/provides/GenerateMetaInfServicesTest.java @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +package org.gradlex.javamodule.dependencies.test.provides; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.gradlex.javamodule.dependencies.test.fixture.GradleBuild; +import org.junit.jupiter.api.Test; + +class GenerateMetaInfServicesTest { + + GradleBuild build = new GradleBuild(true); + + @Test + void generates_meta_inf_services_files() { + build.libBuildFile.appendText( + """ + javaModuleDependencies { generateMetaInfServices() } + dependencies.constraints { + implementation("org.junit.platform:junit-platform-engine:6.0.2") + testFixturesImplementation("org.junit.platform:junit-platform-engine:6.0.2") + } + """); + build.file("lib/src/main/java/abc/lib/NoOpTestEngine.java") + .writeText( + """ + package abc.lib; + import org.junit.platform.engine.*; + public class NoOpTestEngine implements TestEngine { + public String getId() { return "noop"; } + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { return null; }; + public void execute(ExecutionRequest request) {}; + } + """); + build.file("lib/src/testFixtures/java/abc/lib/fixtures/NoOpTestEngine.java") + .writeText( + """ + package abc.lib.fixtures; + import org.junit.platform.engine.*; + public class NoOpTestEngine implements TestEngine { + public String getId() { return "noop"; } + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { return null; }; + public void execute(ExecutionRequest request) {}; + } + """); + + build.libModuleInfoFile.writeText( + """ + module abc.lib { + requires org.junit.platform.engine; + provides org.junit.platform.engine.TestEngine + with abc.lib.NoOpTestEngine; + } + """); + build.file("lib/src/testFixtures/java/module-info.java") + .writeText( + """ + module abc.lib.test.fixtures { + requires org.junit.platform.engine; + provides org.junit.platform.engine.TestEngine + with abc.lib.fixtures.NoOpTestEngine; + } + """); + + var result = build.runner("jar", "testFixturesJar").build(); + BuildTask libMain = result.task(":lib:generateMetaInfServices"); + BuildTask libTestFixtures = result.task(":lib:generateTestFixturesMetaInfServices"); + BuildTask appMain = result.task(":app:generateMetaInfServices"); + assertThat(libMain).isNotNull(); + assertThat(libMain.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(libTestFixtures).isNotNull(); + assertThat(libTestFixtures.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(appMain).isNull(); + + assertThat(build.file("lib/build/resources/main/META-INF/services/org.junit.platform.engine.TestEngine") + .getAsPath()) + .hasContent("abc.lib.NoOpTestEngine"); + assertThat(build.file("lib/build/resources/testFixtures/META-INF/services/org.junit.platform.engine.TestEngine") + .getAsPath()) + .hasContent("abc.lib.fixtures.NoOpTestEngine"); + } +}