Skip to content
Open
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
7 changes: 7 additions & 0 deletions changelog/@unreleased/pr-1520.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type: fix
fix:
description: Fix GcvConsumable configuration creation for Gradle 9+ compatibility
by pre-creating configurations during plugin setup and making them visible for
variant selection discovery.
links:
- https://github.com/palantir/gradle-consistent-versions/pull/1520
147 changes: 119 additions & 28 deletions src/main/java/com/palantir/gradle/versions/VersionsLockPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,11 @@ private void setupDependenciesToProject(Project rootProject, Configuration unifi
conf.getAttributes().attribute(GCV_USAGE_ATTRIBUTE, GcvUsage.GCV_SOURCE);
});

project.getPluginManager().withPlugin("java", _plugin -> {
ensureBaseGcvConsumable(project, JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
ensureBaseGcvConsumable(project, JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
});

project.getConfigurations().register(CONSISTENT_VERSIONS_PRODUCTION, conf -> {
conf.setDescription(
"Outgoing configuration for production dependencies meant to be used by consistent-versions");
Expand Down Expand Up @@ -474,6 +479,48 @@ private void setupPublishConstraintsForProject(Project subproject) {
});
}

private void ensureBaseGcvConsumable(Project project, String baseConfName) {
if (!project.getConfigurations().getNames().contains(baseConfName)) {
return;
}
String gcvName = baseConfName + "GcvConsumable";
if (project.getConfigurations().findByName(gcvName) != null) {
return;
}

Configuration baseConf = project.getConfigurations().getByName(baseConfName);
Configuration copiedTargetConfResolvable = baseConf.copyRecursive();

if (GradleVersion.current().compareTo(GradleVersion.version("8.14")) < 0) {
project.getConfigurations().add(copiedTargetConfResolvable);
copiedTargetConfResolvable.outgoing(outgoing -> outgoing.capability(String.format(
"gcv:%s-%s-%s-%s:extra",
project.getGroup(),
project.getName(),
project.getVersion(),
copiedTargetConfResolvable.getName())));
}

project.getConfigurations().register(gcvName, conf -> {
conf.setDescription(String.format(
"Copy of the '%s' configuration that can be resolved by"
+ " com.palantir.consistent-versions without resolving the '%s' configuration itself.",
baseConf.getName(), baseConf.getName()));
conf.setCanBeResolved(false);
conf.setCanBeConsumed(true);
conf.extendsFrom(copiedTargetConfResolvable);
conf.attributes(getGcvAttributes()::configureGcvBaseAttributes);

if (GradleVersion.current().compareTo(GradleVersion.version("9.0")) < 0) {
conf.setVisible(false);
}

conf.outgoing(outgoing -> outgoing.capability(String.format(
"gcv:%s-%s-%s-%s:extra",
project.getGroup(), project.getName(), project.getVersion(), conf.getName())));
});
}

private static Map<String, String> capabilityFor(Project project, GcvScope scope) {
// Note: don't reference project.group() here as it is mutable so could change throughout the build evaluation.
return ImmutableMap.of(
Expand Down Expand Up @@ -675,36 +722,34 @@ private void recursivelyCopyProjectDependenciesWithScope(
copiedTargetConfResolvable.getName())));
}

Configuration copiedConf = projectDep
.getConfigurations()
.register(targetConf.getName() + "GcvConsumable", conf -> {
conf.setDescription(String.format(
"Copy of the '%s' configuration that can be resolved by"
+ " com.palantir.consistent-versions without resolving the '%s' configuration"
+ " itself.",
targetConf.getName(), targetConf.getName()));
conf.setCanBeResolved(false);
// Must set this because we depend on this configuration when resolving unifiedClasspath.
conf.setCanBeConsumed(true);
conf.extendsFrom(copiedTargetConfResolvable);
conf.attributes(getGcvAttributes()::configureGcvBaseAttributes);

// Since we only depend on these from the same project (via CONSISTENT_VERSIONS_PRODUCTION or
// CONSISTENT_VERSIONS_TEST), we shouldn't allow them to be visible outside this project.
conf.setVisible(false);
String copiedName = targetConf.getName() + "GcvConsumable";
Configuration existingCopied = projectDep.getConfigurations().findByName(copiedName);

// If the configuration already exists (created by ensureBaseGcvConsumable during setup),
// we need to update its extendsFrom to use the latest copy that includes all dependencies
// (dependencies are only fully available in afterEvaluate)
if (existingCopied != null) {
// Check if configuration state allows modification
org.gradle.api.internal.artifacts.configurations.ConfigurationInternal confInternal =
(org.gradle.api.internal.artifacts.configurations.ConfigurationInternal) existingCopied;
if (confInternal.isCanBeMutated()) {
existingCopied.setExtendsFrom(java.util.Collections.singleton(copiedTargetConfResolvable));
}
}

// This is so we can get back the scope from the ResolutionResult.
conf.getAllDependencies()
.withType(ExternalModuleDependency.class)
.all(externalDep -> dependencyScopes.record(externalDep.getModule(), scope));
Configuration copiedConf;
if (existingCopied != null) {
copiedConf = existingCopied;
} else {
copiedConf = registerGcvConsumableConfiguration(
projectDep, copiedName, targetConf, copiedTargetConfResolvable, dependencyScopes, scope);
}

// To avoid capability based conflict detection between all these copied configurations (where
// they conflict as each has no capabilities), we give each of them a capability
conf.outgoing(outgoing -> outgoing.capability(String.format(
"gcv:%s-%s-%s-%s:extra",
projectDep.getGroup(), projectDep.getName(), projectDep.getVersion(), conf.getName())));
})
.get();
// Ensure scope recording is applied even if the configuration already existed.
copiedConf
.getAllDependencies()
.withType(ExternalModuleDependency.class)
.all(externalDep -> dependencyScopes.record(externalDep.getModule(), scope));

// Update state about what we've seen
copiedConfigurationsCache.put(targetConf, copiedConf.getName());
Expand All @@ -727,6 +772,52 @@ private void recursivelyCopyProjectDependenciesWithScope(
});
}

private Configuration registerGcvConsumableConfiguration(
Project projectDep,
String copiedName,
Configuration targetConf,
Configuration copiedTargetConfResolvable,
DirectDependencyScopes.Builder dependencyScopes,
GcvScope scope) {
return projectDep
.getConfigurations()
.register(copiedName, conf -> {
conf.setDescription(String.format(
"Copy of the '%s' configuration that can be resolved by"
+ " com.palantir.consistent-versions without resolving the '%s'"
+ " configuration itself.",
targetConf.getName(), targetConf.getName()));
conf.setCanBeResolved(false);
// Must set this because we depend on this configuration when resolving unifiedClasspath.
conf.setCanBeConsumed(true);
conf.extendsFrom(copiedTargetConfResolvable);
conf.attributes(getGcvAttributes()::configureGcvBaseAttributes);

// In Gradle 9+, setVisible(false) prevents this configuration from being discovered
// by variant selection from other projects. Since inherited project dependencies
// (like api(project(":foo"))) need to resolve to this configuration via
// variant selection, we need to keep it visible in Gradle 9+.
if (GradleVersion.current().compareTo(GradleVersion.version("9.0")) < 0) {
// Since we only depend on these from the same project (via
// CONSISTENT_VERSIONS_PRODUCTION or CONSISTENT_VERSIONS_TEST), we shouldn't allow
// them to be visible outside this project.
conf.setVisible(false);
}

// This is so we can get back the scope from the ResolutionResult.
conf.getAllDependencies()
.withType(ExternalModuleDependency.class)
.all(externalDep -> dependencyScopes.record(externalDep.getModule(), scope));

// To avoid capability based conflict detection between all these copied configurations
// (where they conflict as each has no capabilities), we give each of them a capability
conf.outgoing(outgoing -> outgoing.capability(String.format(
"gcv:%s-%s-%s-%s:extra",
projectDep.getGroup(), projectDep.getName(), projectDep.getVersion(), conf.getName())));
})
.get();
}

/**
* This causes {@link Configuration#withDependencies} actions to be run eagerly.
*
Expand Down