Skip to content

Skip extendsFrom(rootConfiguration) for consumable-only configurations#1614

Draft
FinlayRJW wants to merge 9 commits intodevelopfrom
finlayw/fix-gradle9-variant-resolution
Draft

Skip extendsFrom(rootConfiguration) for consumable-only configurations#1614
FinlayRJW wants to merge 9 commits intodevelopfrom
finlayw/fix-gradle9-variant-resolution

Conversation

@FinlayRJW
Copy link
Contributor

@FinlayRJW FinlayRJW commented Mar 10, 2026

Before this PR

GCV applies conf.extendsFrom(rootConfiguration) to all configurations on every project. rootConfiguration contains a ProjectDependency on the root project. This creates a variant model cycle when two conditions are met on the root project:

  1. A consumable-only configuration exists (e.g. created by a plugin like gradle-idea-language-injector)
  2. That consumable config's outgoing artifacts are backed by a provider that resolves another configuration (e.g. compileClasspath)

Both configurations extend rootConfiguration (via GCV). On Gradle 9.4.0, gradle/gradle#36245 changed extendsFrom from eagerly storing parent configs in a Set<Configuration> to lazily storing them as Provider<Configuration>. When Gradle computes the root project's variant model, it now iterates consumable configs and evaluates their lazy extendsFrom chains. Following rootConfigurationProjectDependency(root) → re-enters variant model computation on root → cycle.

The real-world trigger is gradle-idea-language-injector, which creates a consumable config on the root project whose outgoing artifacts resolve compileClasspath to scan jars for annotations. See palantir/gradle-idea-language-injector#149 for a downstream workaround.

After this PR

Skips extendsFrom(rootConfiguration) for consumable-only configurations (canBeConsumed=true, canBeResolved=false). These are published variants that never resolve dependencies, so inheriting version constraints serves no purpose.

No behavior change for existing configs:

  • implementation, api, etc. — canBeConsumed=false → still gets extendsFrom
  • compileClasspath, runtimeClasspathcanBeResolved=true → still gets extendsFrom
  • apiElements, runtimeElements — already handled by JAVA_PUBLISHED_CONFIGURATION_NAMES

Possible downsides?

We are no longer extending from rootConfiguration for any consumable configuration - this could have some unseen side effect, but I don't think it should as we don't care about version constraints from the rootConfiguration on a consumable as they are never resolved

Testing

Consumable-only configurations (canBeConsumed=true, canBeResolved=false) are
published variants that never resolve dependencies, so inheriting version
constraints via extendsFrom is useless. It also creates a variant model cycle
when rootConfiguration contains a ProjectDependency on the root project,
which became a build failure in Gradle 9.4.0 due to gradle/gradle#36245.
@changelog-app
Copy link

changelog-app bot commented Mar 10, 2026

Generate changelog in changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

Skip extendsFrom(rootConfiguration) for consumable-only configurations

Check the box to generate changelog(s)

  • Generate changelog entry

allprojects {
repositories {
maven { url "file:///%s" }
maven { url uri("%s") }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for some reason fails on gradle 9.4.0 without this?

Caused by: java.net.URISyntaxException: Illegal character in path at index 121: file:////Volumes/git/gradle-consistent-versions/build/gradle-plugin-testing/VersionsPropsPluginIntegrationTest/consumable only configuration on root project does not cause variant model cycle/9.4.0/build/mavenrepo/localMavenRepository
	at org.gradle.api.internal.file.FileNotationConverter.convert(FileNotationConverter.java:96)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is said reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well gradle 8.14.3 -> gradle 9.4.0 gives:

file:////Volumes/git/gradle-consistent-versions/build/gradle-plugin-testing/VersionsPropsPluginIntegrationTest/consumable only configuration on root project does not cause variant model cycle/8.14.3/build/mavenrepo/localMavenRepository

vs

file:////Volumes/git/gradle-consistent-versions/build/gradle-plugin-testing/VersionsPropsPluginIntegrationTest/consumable only configuration on root project does not cause variant model cycle/9.4.0/build/mavenrepo/localMavenRepository

Whats weird is that the illegal character the error references to is a space which is present in both

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe gradle 9 is stricter on having spaces in url strings?

Copy link
Contributor Author

@FinlayRJW FinlayRJW Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@felixdesouza felixdesouza Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk, you tell me! can you go and verify that please? thank you! what do you think that means for some of our excavators?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be okay we have an explicit callout to use uri I added a bit ago:

// ALSO CORRECT - allprojects block when subprojects also need the repo
rootProject.buildGradle().append("""
    allprojects {
        repositories {
            maven { url uri("%s") }
        }
    }
    """, repo.path());

https://github.com/palantir/gradle-plugin-testing/blob/develop/docs/junit-migration-excavator-instructions.md

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not say that it's explicit, it's just an example of adding repositories. If we're seeing this come up again, we should have an explicit callout along the line of:

  • don't use file:///... when in build.gradle test files, use uri("%s") instead as it is deprecated

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one could argue, it's a separate concern as it's gradle 9 stuff, but I think it's far easier to nip it in the bud now whilst we're doing the migrations and it migrates to the proper thing, that's forward compatible, as we probably won't run a similar style AI assisted migration for gradle 9 stuffs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@changelog-app
Copy link

changelog-app bot commented Mar 11, 2026

Successfully generated changelog entry!

Need to regenerate?

Simply interact with the changelog bot comment again to regenerate these entries.


📋Changelog Preview

💡 Improvements

  • Skip extendsFrom(rootConfiguration) for consumable-only configurations (#1614)

@FinlayRJW FinlayRJW requested a review from kelvinou01 March 11, 2026 09:29
}

@Test
@AdditionallyRunWithGradle("9.4.0")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when all the tests are moved over we can start testing everything on 9.4.0

});
}

private static void setupConfigurationDependencies(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a worthwhile change, please undo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have undone - but now require @SuppressWarnings("checkstyle:CyclomaticComplexity") I assume thats okay?

Comment on lines +176 to +180
// Consumable-only configurations are published variants of a project — they never resolve
// dependencies, so version constraints from rootConfiguration are useless on them.
// Additionally, extending rootConfiguration here creates a variant model cycle on
// Gradle 9.4.0+. If a consumable config's outgoing artifacts provider resolves another config (e.g.
// compileClasspath) that also extends rootConfiguration.
Copy link
Contributor

@felixdesouza felixdesouza Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • when you say "published" what do you mean?

  • so version constraints from rootConfiguration are useless on them.

    I'm not sure I buy this, can you explain your reasoning a bit more please? What happens to configurations that are all three types? this happens in a lot of places. Will they be broken when they get to 9.x? Probably, but I'm concerned about existing behaviour changing in subtle ways for 8.x. This is also for all consumable-only configurations, not just the root project as that was your initial problem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a consumable config's outgoing artifacts provider resolves another config (e.g. compileClasspath) that also extends rootConfiguration.

I can't parse this sentence, what are you trying to say here

Copy link
Contributor Author

@FinlayRJW FinlayRJW Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when you say "published" what do you mean?

sorry, I mean consumable-only configurations are outgoing variants — they expose artifacts to consumers but never resolve dependencies themselves. Since they never resolve, version constraints inherited from rootConfiguration surely will have no effect on them

I can't parse this sentence, what are you trying to say here

Sorry I'm not 100% sure I fully understand but my best guess is extending rootConfiguration on a consumable-only config creates a variant model cycle on Gradle 9.4.0+ when that configurations outgoing artifacts resolve a configuration (e.g. compileClasspath) that also extends rootConfiguration. rootConfiguration ends up reachable from both sides — Gradle includes it in the variant model through the consumable config, and also needs to resolve it through compileClasspath. That circular dependency between variant discovery and resolution is what Gradle 9.4.0+ now rejects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also for all consumable-only configurations

True I'll add a test to see if this can happen only within a subproject or if this is only an issue with root and tighten the check accordingly

Copy link
Contributor

@felixdesouza felixdesouza Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my qualm with this approach is that, underlying behaviour has changed in gradle 9.4, this change happens to avoid this new behaviour you don't want. However, what I don't get from this PR or its description is what the underlying problem is for this change to be deemed the correct fix. It just feels like because it solves the problem that it's then good to go, which I fundamentally disagree with.

FWIW, this might actually be the right fix, but it's hard to know that given what I'm seeing here, especially considering it may change behaviour for non gradle 9 use cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah thats fair - I'll give it some more thought I think I need to more fully understand how this has changed - will close this PR for now / convert to draft and try and figure out exactly what has changed and what the best fix is

@FinlayRJW FinlayRJW marked this pull request as draft March 11, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants