/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path // Configure miscellaneous aspects required for supporting the java module system layer. // Debugging utilities. apply from: buildscript.sourceFile.toPath().resolveSibling("modules-debugging.gradle") allprojects { plugins.withType(JavaPlugin) { // We won't be using gradle's built-in automatic module finder. java { modularity.inferModulePath.set(false) } // // Configure modular extensions for each source set. // project.sourceSets.all { SourceSet sourceSet -> // Create and register a source set extension for manipulating classpath/ module-path ModularPathsExtension modularPaths = new ModularPathsExtension(project, sourceSet) sourceSet.extensions.add("modularPaths", modularPaths) // LUCENE-10344: We have to provide a special-case extension for ECJ because it does not // support all of the module-specific javac options. ModularPathsExtension modularPathsForEcj = modularPaths if (sourceSet.name == SourceSet.TEST_SOURCE_SET_NAME && project.path in [ ":lucene:spatial-extras", ":lucene:spatial3d", ]) { modularPathsForEcj = modularPaths.cloneWithMode(ModularPathsExtension.Mode.CLASSPATH_ONLY) } sourceSet.extensions.add("modularPathsForEcj", modularPathsForEcj) // TODO: the tests of these projects currently don't compile or work in // module-path mode. Make the modular paths extension use class path only. if (sourceSet.name == SourceSet.TEST_SOURCE_SET_NAME && project.path in [ // Circular dependency between artifacts or source set outputs, // causing package split issues at runtime. ":lucene:core", ":lucene:codecs", ":lucene:test-framework", ]) { modularPaths.mode = ModularPathsExtension.Mode.CLASSPATH_ONLY } // Configure the JavaCompile task associated with this source set. tasks.named(sourceSet.getCompileJavaTaskName()).configure({ JavaCompile task -> task.dependsOn modularPaths.compileModulePathConfiguration // GH-12742: add the modular path as inputs so that if anything changes, the task // is not up to date and is re-run. I [dw] believe this should be a @Classpath parameter // on the task itself... but I don't know how to implement this on an existing class. // this is a workaround but should work just fine though. task.inputs.files(modularPaths.compileModulePathConfiguration) // LUCENE-10327: don't allow gradle to emit an empty sourcepath as it would break // compilation of modules. task.options.setSourcepath(sourceSet.java.sourceDirectories) // Add modular dependencies and their transitive dependencies to module path. task.options.compilerArgumentProviders.add(modularPaths.compilationArguments) // LUCENE-10304: if we modify the classpath here, IntelliJ no longer sees the dependencies as compile-time // dependencies, don't know why. if (!rootProject.ext.isIdeaSync) { task.classpath = modularPaths.compilationClasspath } doFirst { modularPaths.logCompilationPaths(logger) } }) // For source sets that contain a module descriptor, configure a jar task that combines // classes and resources into a single module. if (sourceSet.name != SourceSet.MAIN_SOURCE_SET_NAME) { tasks.maybeCreate(sourceSet.getJarTaskName(), org.gradle.jvm.tasks.Jar).configure({ archiveClassifier = sourceSet.name from(sourceSet.output) }) } } // Connect modular configurations between their "test" and "main" source sets, this reflects // the conventions set by the Java plugin. project.configurations { moduleTestApi.extendsFrom moduleApi moduleTestImplementation.extendsFrom moduleImplementation moduleTestRuntimeOnly.extendsFrom moduleRuntimeOnly moduleTestCompileOnly.extendsFrom moduleCompileOnly } // Gradle's java plugin sets the compile and runtime classpath to be a combination // of configuration dependencies and source set's outputs. For source sets with modules, // this leads to split class and resource folders. // // We tweak the default source set path configurations here by assembling jar task outputs // of the respective source set, instead of their source set output folders. We also attach // the main source set's jar to the modular test implementation configuration. SourceSet mainSourceSet = project.sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME) boolean mainIsModular = mainSourceSet.modularPaths.hasModuleDescriptor() boolean mainIsEmpty = mainSourceSet.allJava.isEmpty() SourceSet testSourceSet = project.sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME) boolean testIsModular = testSourceSet.modularPaths.hasModuleDescriptor() // LUCENE-10304: if we modify the classpath here, IntelliJ no longer sees the dependencies as compile-time // dependencies, don't know why. if (!rootProject.ext.isIdeaSync) { def jarTask = project.tasks.getByName(mainSourceSet.getJarTaskName()) def testJarTask = project.tasks.getByName(testSourceSet.getJarTaskName()) // Consider various combinations of module/classpath configuration between the main and test source set. if (testIsModular) { if (mainIsModular || mainIsEmpty) { // If the main source set is empty, skip the jar task. def jarTaskOutputs = mainIsEmpty ? [] : jarTask.outputs // Fully modular tests - must have no split packages, proper access, etc. // Work around the split classes/resources problem by adjusting classpaths to // rely on JARs rather than source set output folders. testSourceSet.compileClasspath = project.objects.fileCollection().from( jarTaskOutputs, project.configurations.getByName(testSourceSet.getCompileClasspathConfigurationName()), ) testSourceSet.runtimeClasspath = project.objects.fileCollection().from( jarTaskOutputs, testJarTask.outputs, project.configurations.getByName(testSourceSet.getRuntimeClasspathConfigurationName()), ) project.dependencies { moduleTestImplementation files(jarTaskOutputs) moduleTestRuntimeOnly files(testJarTask.outputs) } } else { // This combination simply does not make any sense (in my opinion). throw GradleException("Test source set is modular and main source set is class-based, this makes no sense: " + project.path) } } else { if (mainIsModular) { // This combination is a potential candidate for patching the main sourceset's module with test classes. I could // not resolve all the difficulties that arise when you try to do it though: // - either a separate module descriptor is needed that opens test packages, adds dependencies via requires clauses // or a series of jvm arguments (--add-reads, --add-opens, etc.) has to be generated and maintained. This is // very low-level (ECJ doesn't support a full set of these instructions, for example). // // Fall back to classpath mode. } else { // This is the 'plain old classpath' mode: neither the main source set nor the test set are modular. } } } // // Configures a Test task associated with the provided source set to use module paths. // // There is no explicit connection between source sets and test tasks so there is no way (?) // to do this automatically, convention-style. // // This closure can be used to configure a different task, with a different source set, should we // have the need for it. Closure configureTestTaskForSourceSet = { Test task, SourceSet sourceSet -> task.configure { def forkProperties = file("${task.temporaryDir}/jvm-forking.properties") ModularPathsExtension modularPaths = sourceSet.modularPaths dependsOn modularPaths // Add modular dependencies and their transitive dependencies to module path. jvmArgumentProviders.add(modularPaths.runtimeArguments) // Modify the default classpath. classpath = modularPaths.runtimeClasspath doFirst { modularPaths.logRuntimePaths(logger) } // Pass all the required properties for tests which fork the JVM. We don't use // regular system properties here because this could affect task up-to-date checks. jvmArgumentProviders.add(new CommandLineArgumentProvider() { @Override Iterable asArguments() { return ["-Dtests.jvmForkArgsFile=" + forkProperties.absolutePath] } }) doFirst { List args = new ArrayList<>(modularPaths.runtimeArguments.asArguments().collect()) def cp = modularPaths.runtimeClasspath.asPath if (!cp.isBlank()) { args.addAll(["-cp", cp]) } // Sanity check. args.forEach(s -> { if (s.contains("\n")) { throw new RuntimeException("LF in forked jvm property?: " + s) } }) Files.createDirectories(forkProperties.toPath().getParent()) Files.writeString(forkProperties.toPath(), String.join("\n", args), StandardCharsets.UTF_8) } } } // Configure (tasks.test, sourceSets.test) tasks.matching { it.name ==~ /test(_[0-9]+)?/ }.all { Test task -> configureTestTaskForSourceSet(task, task.project.sourceSets.test) } // Configure module versions. tasks.withType(JavaCompile).configureEach { task -> // TODO: LUCENE-10267: workaround for gradle bug. Remove when the corresponding issue is fixed. task.options.compilerArgumentProviders.add((CommandLineArgumentProvider) { -> if (task.getClasspath().isEmpty()) { return ["--module-version", project.version.toString()] } else { return [] } }) task.options.javaModuleVersion.set(provider { return project.version.toString() }) } } } // // For a source set, create explicit configurations for declaring modular dependencies. // // These "modular" configurations correspond 1:1 to Gradle's conventions but have a 'module' prefix // and a capitalized remaining part of the conventional name. For example, an 'api' configuration in // the main source set would have a corresponding 'moduleApi' configuration for declaring modular // dependencies. // // Gradle's java plugin "convention" configurations extend from their modular counterparts // so all dependencies end up on classpath by default for backward compatibility with other // tasks and gradle infrastructure. // // At the same time, we also know which dependencies (and their transitive graph of dependencies!) // should be placed on module-path only. // // Note that an explicit configuration of modular dependencies also opens up the possibility of automatically // validating whether the dependency configuration for a gradle project is consistent with the information in // the module-info descriptor because there is a (nearly?) direct correspondence between the two: // // moduleApi - 'requires transitive' // moduleImplementation - 'requires' // moduleCompileOnly - 'requires static' // class ModularPathsExtension implements Cloneable, Iterable { /** * Determines how paths are split between module path and classpath. */ enum Mode { /** * Dependencies and source set outputs are placed on classpath, even if declared on modular * configurations. This would be the 'default' backward-compatible mode. */ CLASSPATH_ONLY, /** * Dependencies from modular configurations are placed on module path. Source set outputs * are placed on classpath. */ DEPENDENCIES_ON_MODULE_PATH } Project project SourceSet sourceSet Configuration compileModulePathConfiguration Configuration runtimeModulePathConfiguration Configuration modulePatchOnlyConfiguration // The mode of splitting paths for this source set. Mode mode = Mode.DEPENDENCIES_ON_MODULE_PATH // More verbose debugging for paths. private boolean debugPaths /** * A list of module name - path provider entries that will be converted * into {@code --patch-module} options. */ private List>> modulePatches = new ArrayList<>() ModularPathsExtension(Project project, SourceSet sourceSet) { this.project = project this.sourceSet = sourceSet debugPaths = Boolean.parseBoolean(project.propertyOrDefault('build.debug.paths', 'false')) ConfigurationContainer configurations = project.configurations // Create modular configurations for gradle's java plugin convention configurations. Configuration moduleApi = createModuleConfigurationForConvention(sourceSet.apiConfigurationName) Configuration moduleImplementation = createModuleConfigurationForConvention(sourceSet.implementationConfigurationName) Configuration moduleRuntimeOnly = createModuleConfigurationForConvention(sourceSet.runtimeOnlyConfigurationName) Configuration moduleCompileOnly = createModuleConfigurationForConvention(sourceSet.compileOnlyConfigurationName) // Apply hierarchy relationships to modular configurations. moduleImplementation.extendsFrom(moduleApi) // Patched modules have to end up in the implementation configuration for IDEs, which // otherwise get terribly confused. Configuration modulePatchOnly = createModuleConfigurationForConvention( SourceSet.isMain(sourceSet) ? "patchOnly" : sourceSet.name + "PatchOnly") modulePatchOnly.canBeResolved(true) moduleImplementation.extendsFrom(modulePatchOnly) this.modulePatchOnlyConfiguration = modulePatchOnly // This part of convention configurations seems like a very esoteric use case, leave out for now. // sourceSet.compileOnlyApiConfigurationName // We have to ensure configurations are using assembled resources and classes (jar variant) as a single // module can't be expanded into multiple folders. Closure ensureJarVariant = { Configuration c -> c.attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements, LibraryElements.JAR)) } // Set up compilation module path configuration combining corresponding convention configurations. Closure createResolvableModuleConfiguration = { String configurationName -> Configuration conventionConfiguration = configurations.maybeCreate(configurationName) Configuration moduleConfiguration = configurations.maybeCreate(moduleConfigurationNameFor(conventionConfiguration.name)) moduleConfiguration.canBeConsumed(false) moduleConfiguration.canBeResolved(true) ensureJarVariant(moduleConfiguration) project.logger.info("Created resolvable module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}") return moduleConfiguration } ensureJarVariant(configurations.maybeCreate(sourceSet.compileClasspathConfigurationName)) ensureJarVariant(configurations.maybeCreate(sourceSet.runtimeClasspathConfigurationName)) this.compileModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.compileClasspathConfigurationName) compileModulePathConfiguration.extendsFrom(moduleCompileOnly, moduleImplementation) this.runtimeModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.runtimeClasspathConfigurationName) runtimeModulePathConfiguration.extendsFrom(moduleRuntimeOnly, moduleImplementation) } /** * Adds {@code --patch-module} option for the provided module name and the provider of a * folder or JAR file. * * @param moduleName * @param pathProvider */ void patchModule(String moduleName, Provider pathProvider) { modulePatches.add(Map.entry(moduleName, pathProvider)); } private FileCollection getCompilationModulePath() { if (mode == Mode.CLASSPATH_ONLY) { return project.files() } return compileModulePathConfiguration - modulePatchOnlyConfiguration } private FileCollection getRuntimeModulePath() { if (mode == Mode.CLASSPATH_ONLY) { if (hasModuleDescriptor()) { // The source set is itself a module. throw new GradleException("Source set contains a module but classpath-only" + " dependencies requested: ${project.path}, source set '${sourceSet.name}'") } return project.files() } return runtimeModulePathConfiguration - modulePatchOnlyConfiguration } FileCollection getCompilationClasspath() { if (mode == Mode.CLASSPATH_ONLY) { return sourceSet.compileClasspath } // Modify the default classpath by removing anything already placed on module path. // Use a lazy provider to delay computation. project.files({ -> return sourceSet.compileClasspath - compileModulePathConfiguration - modulePatchOnlyConfiguration }) } CommandLineArgumentProvider getCompilationArguments() { return new CommandLineArgumentProvider() { @Override Iterable asArguments() { FileCollection modulePath = ModularPathsExtension.this.compilationModulePath if (modulePath.isEmpty()) { return [] } ArrayList extraArgs = [] extraArgs += ["--module-path", modulePath.join(File.pathSeparator)] if (!hasModuleDescriptor()) { // We're compiling what appears to be a non-module source set so we'll // bring everything on module path in the resolution graph, // otherwise modular dependencies wouldn't be part of the resolved module graph and this // would result in class-not-found compilation problems. extraArgs += ["--add-modules", "ALL-MODULE-PATH"] } // Add module-patching. extraArgs += getPatchModuleArguments(modulePatches) return extraArgs } } } FileCollection getRuntimeClasspath() { if (mode == Mode.CLASSPATH_ONLY) { return sourceSet.runtimeClasspath } // Modify the default classpath by removing anything already placed on module path. // Use a lazy provider to delay computation. project.files({ -> return sourceSet.runtimeClasspath - runtimeModulePath - modulePatchOnlyConfiguration }) } CommandLineArgumentProvider getRuntimeArguments() { return new CommandLineArgumentProvider() { @Override Iterable asArguments() { FileCollection modulePath = ModularPathsExtension.this.runtimeModulePath if (modulePath.isEmpty()) { return [] } def extraArgs = [] // Add source set outputs to module path. extraArgs += ["--module-path", modulePath.files.join(File.pathSeparator)] // Ideally, we should only add the sourceset's module here, everything else would be resolved via the // module descriptor. But this would require parsing the module descriptor and may cause JVM version conflicts // so keeping it simple. extraArgs += ["--add-modules", "ALL-MODULE-PATH"] // Add module-patching. extraArgs += getPatchModuleArguments(modulePatches) return extraArgs } } } boolean hasModuleDescriptor() { return sourceSet.allJava.srcDirs.stream() .map(dir -> new File(dir, "module-info.java")) .anyMatch(file -> file.exists()) } private List getPatchModuleArguments(List>> patches) { def args = [] patches.each { args.add("--patch-module"); args.add(it.key + "=" + it.value.get()) } return args } private static String toList(FileCollection files) { return files.isEmpty() ? " [empty]" : ("\n " + files.sort().join("\n ")) } private static String toList(List>> patches) { return patches.isEmpty() ? " [empty]" : ("\n " + patches.collect {"${it.key}=${it.value.get()}"}.join("\n ")) } public void logCompilationPaths(Logger logger) { def value = "Modular extension, compilation paths, source set=${sourceSet.name}${hasModuleDescriptor() ? " (module)" : ""}, mode=${mode}:\n" + " Module path:${toList(compilationModulePath)}\n" + " Class path: ${toList(compilationClasspath)}\n" + " Patches: ${toList(modulePatches)}" if (debugPaths) { logger.lifecycle(value) } else { logger.info(value) } } public void logRuntimePaths(Logger logger) { def value = "Modular extension, runtime paths, source set=${sourceSet.name}${hasModuleDescriptor() ? " (module)" : ""}, mode=${mode}:\n" + " Module path:${toList(runtimeModulePath)}\n" + " Class path: ${toList(runtimeClasspath)}\n" + " Patches : ${toList(modulePatches)}" if (debugPaths) { logger.lifecycle(value) } else { logger.info(value) } } public ModularPathsExtension clone() { return (ModularPathsExtension) super.clone() } ModularPathsExtension cloneWithMode(Mode newMode) { def cloned = this.clone() cloned.mode = newMode return cloned } // Map convention configuration names to "modular" corresponding configurations. static String moduleConfigurationNameFor(String configurationName) { return "module" + configurationName.capitalize().replace("Classpath", "Path") } // Create module configuration for the corresponding convention configuration. private Configuration createModuleConfigurationForConvention(String configurationName) { ConfigurationContainer configurations = project.configurations Configuration conventionConfiguration = configurations.maybeCreate(configurationName) Configuration moduleConfiguration = configurations.maybeCreate(moduleConfigurationNameFor(configurationName)) moduleConfiguration.canBeConsumed(false) moduleConfiguration.canBeResolved(false) conventionConfiguration.extendsFrom(moduleConfiguration) project.logger.info("Created module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}") return moduleConfiguration } /** * Provide internal dependencies for tasks willing to depend on this modular paths object. */ @Override Iterator iterator() { return [ compileModulePathConfiguration, runtimeModulePathConfiguration ].iterator() } }