mirror of https://github.com/apache/lucene.git
531 lines
22 KiB
Groovy
531 lines
22 KiB
Groovy
/*
|
|
* 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.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
|
|
|
|
// 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.isIdea) {
|
|
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.isIdea) {
|
|
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<Void> configureTestTaskForSourceSet = { Test task, SourceSet sourceSet ->
|
|
task.configure {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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<Object> {
|
|
/**
|
|
* 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<Map.Entry<String, Provider<Path>>> 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<Void> 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<Configuration> 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<Path> 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<String> asArguments() {
|
|
FileCollection modulePath = ModularPathsExtension.this.compilationModulePath
|
|
|
|
if (modulePath.isEmpty()) {
|
|
return []
|
|
}
|
|
|
|
ArrayList<String> 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<String> 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<String> getPatchModuleArguments(List<Map.Entry<String, Provider<Path>>> 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<Map.Entry<String, Provider<Path>>> 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<Object> iterator() {
|
|
return [
|
|
compileModulePathConfiguration,
|
|
runtimeModulePathConfiguration
|
|
].iterator()
|
|
}
|
|
} |