-
Notifications
You must be signed in to change notification settings - Fork 17
Skip extendsFrom(rootConfiguration) for consumable-only configurations
#1614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
cc969f1
1c78358
3ab332b
c285c10
162bdfd
8d8148c
b8e9799
8daecce
567d839
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -173,6 +173,16 @@ private static void setupConfiguration( | |
| return; | ||
| } | ||
|
|
||
| // 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. | ||
| if (conf.isCanBeConsumed() && !conf.isCanBeResolved()) { | ||
| log.debug("Not configuring consumable-only configuration: {}", conf); | ||
| return; | ||
| } | ||
|
|
||
| // Must do all this in a withDependencies block so that it's run lazily, so that | ||
| // `extension.shouldExcludeConfiguration` isn't queried too early (before the user had the change to configure). | ||
| // However, we must not make this lazy using an afterEvaluate. | ||
|
|
@@ -184,46 +194,58 @@ private static void setupConfiguration( | |
| // conf.getDependencies() is called. | ||
| AtomicBoolean wasConfigured = new AtomicBoolean(); | ||
| conf.withDependencies(deps -> { | ||
| if (!wasConfigured.compareAndSet(false, true)) { | ||
| // We are configuring a copy of the original dependency, as they inherit the withDependenciesActions. | ||
| log.debug("Not configuring {} because it's a copy of an already configured configuration.", conf); | ||
| return; | ||
| } | ||
| if (extension.shouldExcludeConfiguration(conf.getName())) { | ||
| log.debug("Not configuring {} because it's excluded", conf); | ||
| return; | ||
| } | ||
| setupConfigurationDependencies( | ||
| subproject, extension, rootConfiguration, versionsProps, conf, deps, wasConfigured); | ||
| }); | ||
| } | ||
|
|
||
| private static void setupConfigurationDependencies( | ||
|
||
| Project subproject, | ||
| VersionRecommendationsExtension extension, | ||
| Configuration rootConfiguration, | ||
| VersionsProps versionsProps, | ||
| Configuration conf, | ||
| DependencySet deps, | ||
| AtomicBoolean wasConfigured) { | ||
| if (!wasConfigured.compareAndSet(false, true)) { | ||
| // We are configuring a copy of the original dependency, as they inherit the withDependenciesActions. | ||
| log.debug("Not configuring {} because it's a copy of an already configured configuration.", conf); | ||
| return; | ||
| } | ||
| if (extension.shouldExcludeConfiguration(conf.getName())) { | ||
| log.debug("Not configuring {} because it's excluded", conf); | ||
| return; | ||
| } | ||
|
|
||
| // This will ensure that dependencies declared in almost all configurations - including ancestors of | ||
| // published configurations (such as `compile`, `runtimeOnly`) - have a version if there only | ||
| // a star-constraint in versions.props that matches them. | ||
| provideVersionsFromStarDependencies(versionsProps, deps); | ||
| // This will ensure that dependencies declared in almost all configurations - including ancestors of | ||
| // published configurations (such as `compile`, `runtimeOnly`) - have a version if there only | ||
| // a star-constraint in versions.props that matches them. | ||
| provideVersionsFromStarDependencies(versionsProps, deps); | ||
|
|
||
| // But don't configure any _ancestors_ of our published configurations to extend rootConfiguration, as we | ||
| // explicitly DO NOT WANT to republish the constraints that come from it (that come from versions.props). | ||
| if (configurationWillAffectPublishedConstraints(subproject, conf)) { | ||
| log.debug("Not configuring published java configuration or its ancestor: {}", conf); | ||
| return; | ||
| } | ||
| // But don't configure any _ancestors_ of our published configurations to extend rootConfiguration, as we | ||
| // explicitly DO NOT WANT to republish the constraints that come from it (that come from versions.props). | ||
| if (configurationWillAffectPublishedConstraints(subproject, conf)) { | ||
| log.debug("Not configuring published java configuration or its ancestor: {}", conf); | ||
| return; | ||
| } | ||
|
|
||
| conf.extendsFrom(rootConfiguration); | ||
| conf.extendsFrom(rootConfiguration); | ||
|
|
||
| // We must allow unifiedClasspath to be resolved at configuration-time. | ||
| if (VersionsLockPlugin.UNIFIED_CLASSPATH_CONFIGURATION_NAME.equals(conf.getName())) { | ||
| return; | ||
| } | ||
| // We must allow unifiedClasspath to be resolved at configuration-time. | ||
| if (VersionsLockPlugin.UNIFIED_CLASSPATH_CONFIGURATION_NAME.equals(conf.getName())) { | ||
| return; | ||
| } | ||
|
|
||
| // Add fail-safe error reporting | ||
| conf.getIncoming().beforeResolve(_resolvableDependencies -> { | ||
| if (GradleWorkarounds.isConfiguring(subproject.getState())) { | ||
| throw new GradleException(String.format( | ||
| "Not allowed to resolve %s at " | ||
| + "configuration time (https://guides.gradle.org/performance/" | ||
| + "#don_t_resolve_dependencies_at_configuration_time). Please upgrade your " | ||
| + "plugins and double-check your gradle scripts (see stacktrace)", | ||
| conf)); | ||
| } | ||
| }); | ||
| // Add fail-safe error reporting | ||
| conf.getIncoming().beforeResolve(_resolvableDependencies -> { | ||
| if (GradleWorkarounds.isConfiguring(subproject.getState())) { | ||
| throw new GradleException(String.format( | ||
| "Not allowed to resolve %s at " | ||
| + "configuration time (https://guides.gradle.org/performance/" | ||
| + "#don_t_resolve_dependencies_at_configuration_time). Please upgrade your " | ||
| + "plugins and double-check your gradle scripts (see stacktrace)", | ||
| conf)); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; | ||
| import com.palantir.gradle.testing.execution.GradleInvoker; | ||
| import com.palantir.gradle.testing.execution.InvocationResult; | ||
| import com.palantir.gradle.testing.junit.AdditionallyRunWithGradle; | ||
| import com.palantir.gradle.testing.junit.DisabledConfigurationCache; | ||
| import com.palantir.gradle.testing.junit.GradlePluginTests; | ||
| import com.palantir.gradle.testing.maven.MavenArtifact; | ||
|
|
@@ -83,7 +84,7 @@ void setup(MavenRepo repo, RootProject rootProject) { | |
|
|
||
| allprojects { | ||
| repositories { | ||
| maven { url "file:///%s" } | ||
| maven { url uri("%s") } | ||
|
||
| } | ||
| } | ||
|
|
||
|
|
@@ -298,6 +299,30 @@ void creates_rootconfiguration_even_if_versions_props_file_missing(GradleInvoker | |
| gradle.withArgs().buildsSuccessfully(); | ||
| } | ||
|
|
||
| @Test | ||
| @AdditionallyRunWithGradle("9.4.0") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| void consumable_only_configuration_on_root_project_does_not_cause_variant_model_cycle( | ||
| GradleInvoker gradle, RootProject rootProject) { | ||
| rootProject.propertiesFile("versions.props").setProperty("org.slf4j:*", "1.7.24"); | ||
|
|
||
| rootProject.buildGradle().plugins().add("java"); | ||
|
|
||
| rootProject.buildGradle().append(""" | ||
| configurations.consumable('customOutgoing') { | ||
| attributes { | ||
| attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, 'custom-usage')) | ||
| } | ||
| outgoing.artifacts(provider { | ||
| configurations.compileClasspath.incoming.artifactView { | ||
| attributes { attribute(Attribute.of('artifactType', String), 'jar') } | ||
| }.files.files | ||
| }) | ||
| } | ||
| """); | ||
|
|
||
| gradle.withArgs("resolveConfigurations").buildsSuccessfully(); | ||
| } | ||
|
|
||
| @Test | ||
| void build_succeeds_without_versions_props_or_versions_lock(GradleInvoker gradle, SubProject foo) { | ||
| foo.buildGradle().plugins().add("java"); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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?
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't parse this sentence, what are you trying to say here
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Sorry I'm not 100% sure I fully understand but my best guess is extending
rootConfigurationon 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 extendsrootConfiguration.rootConfigurationends up reachable from both sides — Gradle includes it in the variant model through the consumable config, and also needs to resolve it throughcompileClasspath. That circular dependency between variant discovery and resolution is what Gradle 9.4.0+ now rejects.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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