Repository: gradle/gradle
Pull request #32649: Forbid selecting variants by configuration name from external components
Author: login: jvandort
Created: 2025-03-10T14:50:13Z | Merged: 2025-03-14T15:27:03Z

Forbid selecting variants by name from maven components and virtual platforms. Currently Maven internally uses 'configurations' to manage its state, however this is an Ivy concept. This change allows us to restructure Maven components to better reflect their actual structure. The current structure is an internal implementation detail

### Reviewing cheatsheet

Before merging the PR, comments starting with 
- ❌ ❓**must** be fixed
- 🤔 💅 **should** be fixed
- 💭 **may** be fixed
- 🎉 celebrate happy things



Diff:
diff --git a/platforms/software/dependency-management/src/integTest/groovy/org/gradle/integtests/resolve/maven/MavenScopesIntegrationTest.groovy b/platforms/software/dependency-management/src/integTest/groovy/org/gradle/integtests/resolve/maven/MavenScopesIntegrationTest.groovy
index 0598de30ab939..0665e99fccde7 100644
--- a/platforms/software/dependency-management/src/integTest/groovy/org/gradle/integtests/resolve/maven/MavenScopesIntegrationTest.groovy
+++ b/platforms/software/dependency-management/src/integTest/groovy/org/gradle/integtests/resolve/maven/MavenScopesIntegrationTest.groovy
@@ -20,22 +20,27 @@ import org.gradle.integtests.fixtures.AbstractDependencyResolutionTest
 import org.gradle.integtests.fixtures.resolve.ResolveTestFixture
 
 class MavenScopesIntegrationTest extends AbstractDependencyResolutionTest {
+
     def resolve = new ResolveTestFixture(buildFile, "conf")
 
     def setup() {
-        resolve.prepare()
-        resolve.addDefaultVariantDerivationStrategy()
         settingsFile << """
             rootProject.name = 'testproject'
         """
+
         buildFile << """
-            repositories {
-                maven { url = '${mavenRepo.uri}' }
+            plugins {
+                id("jvm-ecosystem")
             }
+
+            ${mavenTestRepository()}
+
             configurations {
                 conf
             }
-"""
+        """
+
+        resolve.prepare()
     }
 
     def "prefers the runtime variant of a Maven module"() {
@@ -64,103 +69,19 @@ class MavenScopesIntegrationTest extends AbstractDependencyResolutionTest {
             .publish()
 
         buildFile << """
-dependencies {
-    conf 'test:target:1.0'
-}
-"""
-        expect:
-        succeeds 'checkDep'
-        resolve.expectDefaultConfiguration("runtime")
-        resolve.expectGraph {
-            root(':', ':testproject:') {
-                module('test:target:1.0') {
-                    module('test:test5:1.0') {
-                        module('test:test1:1.0')
-                        module('test:test2:1.0')
-                    }
-                    module('test:test6:1.0') {
-                        module('test:test3:1.0')
-                        module('test:test4:1.0')
-                    }
-                }
+            dependencies {
+                conf 'test:target:1.0'
             }
-        }
-    }
-
-    def "can reference compile scope to include compile scoped dependencies of module"() {
-        def notRequired = mavenRepo.module('test', 'dont-include-me', '1.0')
-        def m1 = mavenRepo.module('test', 'test1', '1.0').publish()
-        def m2 = mavenRepo.module('test', 'test2', '1.0')
-            .dependsOn(m1, scope: 'compile')
-            .dependsOn(notRequired, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-        mavenRepo.module('test', 'target', '1.0')
-            .dependsOn(m2, scope: 'compile')
-            .dependsOn(notRequired, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-
-        buildFile << """
-dependencies {
-    conf group: 'test', name: 'target', version: '1.0', configuration: 'compile'
-}
-"""
+        """
 
-        expect:
-        executer.expectDocumentedDeprecationWarning("Selecting a variant by configuration name from a non-Ivy external component. This behavior has been deprecated. This will fail with an error in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_8.html#selecting_variant_by_configuration_name")
+        when:
         succeeds 'checkDep'
-        resolve.expectGraph {
-            root(':', ':testproject:') {
-                module('test:target:1.0') {
-                    configuration = 'compile'
-                    module('test:test2:1.0') {
-                        module('test:test1:1.0')
-                    }
-                }
-            }
-        }
-    }
 
-    def "can reference runtime scope to include runtime dependencies of compile and runtime scoped dependencies of module"() {
-        def notRequired = mavenRepo.module('test', 'dont-include-me', '1.0')
-        def m1 = mavenRepo.module('test', 'test1', '1.0').publish()
-        def m2 = mavenRepo.module('test', 'test2', '1.0').publish()
-        def m3 = mavenRepo.module('test', 'test3', '1.0').publish()
-        def m4 = mavenRepo.module('test', 'test4', '1.0').publish()
-        def m5 = mavenRepo.module('test', 'test5', '1.0')
-            .dependsOn(m1, scope: 'compile')
-            .dependsOn(m2, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-        def m6 = mavenRepo.module('test', 'test6', '1.0')
-            .dependsOn(m3, scope: 'compile')
-            .dependsOn(m4, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-        mavenRepo.module('test', 'target', '1.0')
-            .dependsOn(m5, scope: 'compile')
-            .dependsOn(m6, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-
-        buildFile << """
-dependencies {
-    conf group: 'test', name: 'target', version: '1.0', configuration: 'runtime'
-}
-"""
-        expect:
-        executer.expectDocumentedDeprecationWarning("Selecting a variant by configuration name from a non-Ivy external component. This behavior has been deprecated. This will fail with an error in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_8.html#selecting_variant_by_configuration_name")
-        succeeds 'checkDep'
+        then:
+        resolve.expectDefaultConfiguration("runtime")
         resolve.expectGraph {
             root(':', ':testproject:') {
                 module('test:target:1.0') {
-                    configuration = 'runtime'
                     module('test:test5:1.0') {
                         module('test:test1:1.0')
                         module('test:test2:1.0')
@@ -174,217 +95,41 @@ dependencies {
         }
     }
 
-    def "can reference provided scope to include runtime dependencies of provided scoped dependencies of module"() {
-        def notRequired = mavenRepo.module('test', 'dont-include-me', '1.0')
-        def m1 = mavenRepo.module('test', 'test1', '1.0').publish()
-        def m2 = mavenRepo.module('test', 'test2', '1.0').publish()
-        def m3 = mavenRepo.module('test', 'test3', '1.0').publish()
-        def m4 = mavenRepo.module('test', 'test4', '1.0')
-            .dependsOn(m1, scope: 'compile')
-            .dependsOn(m2, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-        def m5 = mavenRepo.module('test', 'test5', '1.0')
-            .dependsOn(m3, scope: 'compile')
-            .dependsOn(m4, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-        mavenRepo.module('test', 'target', '1.0')
-            .dependsOn(notRequired, scope: 'compile')
-            .dependsOn(notRequired, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(m5, scope: 'provided')
-            .publish()
+    def "fails when referencing a scope explicitly by configuration name"() {
+        mavenRepo.module('test', 'target', '1.0').publish()
 
         buildFile << """
-dependencies {
-    conf group: 'test', name: 'target', version: '1.0', configuration: 'provided'
-}
-"""
-        expect:
-        executer.expectDocumentedDeprecationWarning("Selecting a variant by configuration name from a non-Ivy external component. This behavior has been deprecated. This will fail with an error in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_8.html#selecting_variant_by_configuration_name")
-        succeeds 'checkDep'
-        resolve.expectGraph {
-            root(':', ':testproject:') {
-                module('test:target:1.0') {
-                    configuration = 'provided'
-                    module('test:test5:1.0') {
-                        module('test:test3:1.0')
-                        module('test:test4:1.0') {
-                            module('test:test2:1.0')
-                            module('test:test1:1.0')
-                        }
-                    }
-                }
+            dependencies {
+                conf group: 'test', name: 'target', version: '1.0', configuration: 'compile'
             }
-        }
-    }
-
-    def "can reference test scope to include test dependencies of compile, runtime and test scoped dependencies of module"() {
-        def notRequired = mavenRepo.module('test', 'dont-include-me', '1.0')
-        def m1 = mavenRepo.module('test', 'test1', '1.0').publish()
-        def m2 = mavenRepo.module('test', 'test2', '1.0').publish()
-        def m3 = mavenRepo.module('test', 'test3', '1.0').publish()
-        def m4 = mavenRepo.module('test', 'test4', '1.0').publish()
-        def m5 = mavenRepo.module('test', 'test5', '1.0').publish()
-        def m6 = mavenRepo.module('test', 'test6', '1.0').publish()
-        def m7 = mavenRepo.module('test', 'test7', '1.0')
-            .dependsOn(m1, scope: 'compile')
-            .dependsOn(m2, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-        def m8 = mavenRepo.module('test', 'test8', '1.0')
-            .dependsOn(m3, scope: 'compile')
-            .dependsOn(m4, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-        def m9 = mavenRepo.module('test', 'test9', '1.0')
-            .dependsOn(m5, scope: 'compile')
-            .dependsOn(m6, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-        mavenRepo.module('test', 'target', '1.0')
-            .dependsOn(m7, scope: 'compile')
-            .dependsOn(m8, scope: 'runtime')
-            .dependsOn(m9, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
-
-        buildFile << """
-dependencies {
-    conf group: 'test', name: 'target', version: '1.0', configuration: 'test'
-}
-"""
-        expect:
-        executer.expectDocumentedDeprecationWarning("Selecting a variant by configuration name from a non-Ivy external component. This behavior has been deprecated. This will fail with an error in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_8.html#selecting_variant_by_configuration_name")
-        succeeds 'checkDep'
-        resolve.expectGraph {
-            root(':', ':testproject:') {
-                module('test:target:1.0') {
-                    configuration = 'test'
-                    module('test:test7:1.0') {
-                        module('test:test1:1.0')
-                        module('test:test2:1.0')
-                    }
-                    module('test:test8:1.0') {
-                        module('test:test3:1.0')
-                        module('test:test4:1.0')
-                    }
-                    module('test:test9:1.0') {
-                        module('test:test5:1.0')
-                        module('test:test6:1.0')
-                    }
-                }
-            }
-        }
-    }
-
-    // This test is documenting behaviour for backwards compatibility purposes
-    def "can reference 'default' configuration to include runtime dependencies of module"() {
-        def notRequired = mavenRepo.module('test', 'dont-include-me', '1.0')
-        def m1 = mavenRepo.module('test', 'test1', '1.0').publish()
-        def m2 = mavenRepo.module('test', 'test2', '1.0').publish()
-        mavenRepo.module('test', 'target', '1.0')
-            .dependsOn(m1, scope: 'compile')
-            .dependsOn(m2, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
+        """
 
-        buildFile << """
-dependencies {
-    conf group: 'test', name: 'target', version: '1.0', configuration: 'default'
-}
-"""
-        expect:
-        executer.expectDocumentedDeprecationWarning("Selecting a variant by configuration name from a non-Ivy external component. This behavior has been deprecated. This will fail with an error in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_8.html#selecting_variant_by_configuration_name")
-        succeeds 'checkDep'
-        resolve.expectGraph {
-            root(':', ':testproject:') {
-                module('test:target:1.0') {
-                    module('test:test1:1.0')
-                    module('test:test2:1.0')
-                }
-            }
-        }
-    }
+        when:
+        fails('checkDep')
 
-    // This test is documenting behaviour for backwards compatibility purposes
-    def "can reference 'optional' configuration to include optional dependencies of module"() {
-        def notRequired = mavenRepo.module('test', 'dont-include-me', '1.0')
-        def m1 = mavenRepo.module('test', 'test1', '1.0').publish()
-        def m2 = mavenRepo.module('test', 'test2', '1.0').publish()
-        mavenRepo.module('test', 'target', '1.0')
-            .dependsOn(notRequired, scope: 'compile')
-            .dependsOn(notRequired, scope: 'runtime')
-            .dependsOn(m1, scope: 'compile', optional: true)
-            .dependsOn(m2, scope: 'runtime', optional: true)
-            .publish()
+        then:
+        failure.assertHasCause("Could not resolve test:target:1.0.\nRequired by:\n    root project :")
+        failure.assertHasCause("Cannot select a variant by configuration name from 'test:target:1.0'.")
 
-        buildFile << """
-dependencies {
-    conf group: 'test', name: 'target', version: '1.0', configuration: 'optional'
-}
-"""
-        expect:
-        executer.expectDocumentedDeprecationWarning("Selecting a variant by configuration name from a non-Ivy external component. This behavior has been deprecated. This will fail with an error in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_8.html#selecting_variant_by_configuration_name")
-        succeeds 'checkDep'
-        resolve.expectGraph {
-            root(':', ':testproject:') {
-                module('test:target:1.0') {
-                    configuration = 'optional'
-                    module('test:test1:1.0')
-                    module('test:test2:1.0')
-                }
-            }
-        }
+        where:
+        scopeName << ["compile", "runtime", "test", "provided"]
     }
 
-    // This test is documenting behaviour for backwards compatibility purposes
-    def "can reference 'master' configuration to include artifact only"() {
-        def notRequired = mavenRepo.module('test', 'dont-include-me', '1.0')
-        mavenRepo.module('test', 'target', '1.0')
-            .dependsOn(notRequired, scope: 'compile')
-            .dependsOn(notRequired, scope: 'runtime')
-            .dependsOn(notRequired, scope: 'test')
-            .dependsOn(notRequired, scope: 'provided')
-            .publish()
+    def "fails when referencing a scope that does not exist"() {
+        mavenRepo.module('test', 'target', '1.0').publish()
 
         buildFile << """
-dependencies {
-    conf group: 'test', name: 'target', version: '1.0', configuration: 'master'
-}
-"""
-        expect:
-        executer.expectDocumentedDeprecationWarning("Selecting a variant by configuration name from a non-Ivy external component. This behavior has been deprecated. This will fail with an error in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_8.html#selecting_variant_by_configuration_name")
-        succeeds 'checkDep'
-        resolve.expectGraph {
-            root(':', ':testproject:') {
-                module('test:target:1.0') {
-                    configuration = 'master'
-                }
+            dependencies {
+                conf group: 'test', name: 'target', version: '1.0', configuration: 'x86_windows'
             }
-        }
-    }
+        """
 
-    def "fails when referencing a scope that does not exist"() {
-        mavenRepo.module('test', 'target', '1.0')
-            .publish()
+        when:
+        fails('checkDep')
 
-        buildFile << """
-dependencies {
-    conf group: 'test', name: 'target', version: '1.0', configuration: 'x86_windows'
-}
-"""
-        expect:
-        executer.expectDocumentedDeprecationWarning("Selecting a variant by configuration name from a non-Ivy external component. This behavior has been deprecated. This will fail with an error in Gradle 9.0. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_8.html#selecting_variant_by_configuration_name")
-        fails 'checkDep'
+        then:
         failure.assertHasCause("Could not resolve test:target:1.0.\nRequired by:\n    root project :")
-        failure.assertHasCause("A dependency was declared on configuration 'x86_windows' of 'test:target:1.0' but no variant with that configuration name exists.")
+        failure.assertHasCause("Cannot select a variant by configuration name from 'test:target:1.0'.")
     }
+
 }
diff --git a/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/resolveengine/graph/builder/LenientPlatformGraphResolveState.java b/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/resolveengine/graph/builder/LenientPlatformGraphResolveState.java
index 94dc802f95663..b56dbca57de1d 100644
--- a/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/resolveengine/graph/builder/LenientPlatformGraphResolveState.java
+++ b/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/resolveengine/graph/builder/LenientPlatformGraphResolveState.java
@@ -49,7 +49,6 @@
 import org.gradle.internal.component.model.VariantGraphResolveMetadata;
 import org.gradle.internal.component.model.VariantGraphResolveState;
 import org.gradle.internal.component.model.VariantResolveMetadata;
-import org.gradle.internal.deprecation.DeprecationLogger;
 import org.gradle.internal.lazy.Lazy;
 import org.jspecify.annotations.Nullable;
 
@@ -176,29 +175,8 @@ public List<? extends VariantGraphResolveState> getVariantsForAttributeMatching(
             return Collections.emptyList();
         }
 
-        @Nullable
         @Override
         public VariantGraphResolveState getLegacyVariant() {
-            return doGetVariantByConfigurationName(Dependency.DEFAULT_CONFIGURATION);
-        }
-
-        @Nullable
-        @Override
-        public VariantGraphResolveState getVariantByConfigurationName(String name) {
-            DeprecationLogger.deprecateBehaviour("Selecting a variant by configuration name from a non-Ivy external component.")
-                .willBecomeAnErrorInGradle9()
-                .withUpgradeGuideSection(8, "selecting_variant_by_configuration_name")
-                .nagUser();
-
-            return doGetVariantByConfigurationName(name);
-        }
-
-        @Nullable
-        private VariantGraphResolveState doGetVariantByConfigurationName(String name) {
-            if (!name.equals(Dependency.DEFAULT_CONFIGURATION)) {
-                return null;
-            }
-
             return implicitVariantState.get();
         }
 
diff --git a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/external/model/ivy/DefaultIvyComponentGraphResolveState.java b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/external/model/ivy/DefaultIvyComponentGraphResolveState.java
index dc62791a3b185..db84a4c8555de 100644
--- a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/external/model/ivy/DefaultIvyComponentGraphResolveState.java
+++ b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/external/model/ivy/DefaultIvyComponentGraphResolveState.java
@@ -76,8 +76,8 @@ public IvyComponentArtifactResolveMetadata getArtifactMetadata() {
     }
 
     @Override
-    public GraphSelectionCandidates getCandidatesForGraphVariantSelection() {
-        return new IvyGraphSelectionCandidates(
+    public IvyGraphSelectionCandidates getCandidatesForGraphVariantSelection() {
+        return new DefaultIvyGraphSelectionCandidates(
             super.getCandidatesForGraphVariantSelection(),
             this::getConfigurationAsVariant
         );
@@ -102,11 +102,12 @@ public ImmutableList<? extends ComponentArtifactMetadata> getConfigurationArtifa
         }
     }
 
-    private static class IvyGraphSelectionCandidates implements GraphSelectionCandidates {
+    private static class DefaultIvyGraphSelectionCandidates implements IvyGraphSelectionCandidates {
+
         private final GraphSelectionCandidates candidates;
         private final Function<String, VariantGraphResolveState> configurationSupplier;
 
-        public IvyGraphSelectionCandidates(
+        public DefaultIvyGraphSelectionCandidates(
             GraphSelectionCandidates candidates,
             Function<String, VariantGraphResolveState> configurationSupplier
         ) {
@@ -130,6 +131,7 @@ public VariantGraphResolveState getLegacyVariant() {
         public VariantGraphResolveState getVariantByConfigurationName(String name) {
             return configurationSupplier.apply(name);
         }
+
     }
 
 }
diff --git a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/external/model/ivy/IvyComponentGraphResolveState.java b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/external/model/ivy/IvyComponentGraphResolveState.java
index a071648f72981..326178bd554ec 100644
--- a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/external/model/ivy/IvyComponentGraphResolveState.java
+++ b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/external/model/ivy/IvyComponentGraphResolveState.java
@@ -18,6 +18,8 @@
 
 import org.gradle.internal.component.external.model.ExternalModuleComponentGraphResolveState;
 import org.gradle.internal.component.model.ConfigurationGraphResolveState;
+import org.gradle.internal.component.model.GraphSelectionCandidates;
+import org.gradle.internal.component.model.VariantGraphResolveState;
 import org.jspecify.annotations.Nullable;
 
 import java.util.Set;
@@ -38,4 +40,17 @@ public interface IvyComponentGraphResolveState extends ExternalModuleComponentGr
     @Nullable
     ConfigurationGraphResolveState getConfiguration(String configurationName);
 
+    @Override
+    IvyGraphSelectionCandidates getCandidatesForGraphVariantSelection();
+
+    interface IvyGraphSelectionCandidates extends GraphSelectionCandidates {
+
+        /**
+         * Returns the variant that is identified by the given configuration name.
+         */
+        @Nullable
+        VariantGraphResolveState getVariantByConfigurationName(String name);
+
+    }
+
 }
diff --git a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/local/model/LocalComponentGraphResolveState.java b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/local/model/LocalComponentGraphResolveState.java
index 3e5f75a84a2dd..c87f37f0bb786 100644
--- a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/local/model/LocalComponentGraphResolveState.java
+++ b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/local/model/LocalComponentGraphResolveState.java
@@ -20,6 +20,7 @@
 import org.gradle.api.artifacts.component.ComponentIdentifier;
 import org.gradle.internal.component.model.ComponentGraphResolveState;
 import org.gradle.internal.component.model.GraphSelectionCandidates;
+import org.gradle.internal.component.model.VariantGraphResolveState;
 import org.jspecify.annotations.Nullable;
 
 import javax.annotation.concurrent.ThreadSafe;
@@ -41,7 +42,7 @@ public interface LocalComponentGraphResolveState extends ComponentGraphResolveSt
      * to migrate away from this method for that purpose.
      *
      * TODO: This is a legacy mechanism, and does not verify that the named configuration is
-     * consumable. Prefer {@link GraphSelectionCandidates#getVariantByConfigurationName(String)}.
+     * consumable. Prefer {@link LocalComponentGraphSelectionCandidates#getVariantByConfigurationName(String)}.
      *
      * <strong>Do not use this method, as it will be removed in Gradle 9.0.</strong>
      */
@@ -87,5 +88,11 @@ interface LocalComponentGraphSelectionCandidates extends GraphSelectionCandidate
          */
         List<LocalVariantGraphResolveState> getAllSelectableVariants();
 
+        /**
+         * Returns the variant that is identified by the given configuration name.
+         */
+        @Nullable
+        VariantGraphResolveState getVariantByConfigurationName(String name);
+
     }
 }
diff --git a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/DefaultExternalModuleComponentGraphResolveState.java b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/DefaultExternalModuleComponentGraphResolveState.java
index 1e6d4d3b499da..bfe2a5d01348f 100644
--- a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/DefaultExternalModuleComponentGraphResolveState.java
+++ b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/DefaultExternalModuleComponentGraphResolveState.java
@@ -31,7 +31,6 @@
 import org.gradle.internal.component.external.model.ExternalModuleComponentGraphResolveMetadata;
 import org.gradle.internal.component.external.model.ExternalModuleComponentGraphResolveState;
 import org.gradle.internal.component.external.model.ImmutableCapabilities;
-import org.gradle.internal.deprecation.DeprecationLogger;
 import org.gradle.internal.lazy.Lazy;
 import org.jspecify.annotations.Nullable;
 
@@ -248,29 +247,13 @@ public List<? extends VariantGraphResolveState> getVariantsForAttributeMatching(
         @Nullable
         @Override
         public VariantGraphResolveState getLegacyVariant() {
-            return doGetVariantByConfigurationName(Dependency.DEFAULT_CONFIGURATION);
-        }
-
-        @Nullable
-        @Override
-        public VariantGraphResolveState getVariantByConfigurationName(String name) {
-
-            DeprecationLogger.deprecateBehaviour("Selecting a variant by configuration name from a non-Ivy external component.")
-                .willBecomeAnErrorInGradle9()
-                .withUpgradeGuideSection(8, "selecting_variant_by_configuration_name")
-                .nagUser();
-
-            return doGetVariantByConfigurationName(name);
-        }
-
-        @Nullable
-        private VariantGraphResolveState doGetVariantByConfigurationName(String name) {
-            ModuleConfigurationMetadata configuration = (ModuleConfigurationMetadata) component.getMetadata().getConfiguration(name);
-            if (configuration == null) {
+            ConfigurationGraphResolveMetadata defaultVariant = component.getMetadata().getConfiguration(Dependency.DEFAULT_CONFIGURATION);
+            if (defaultVariant == null) {
                 return null;
             }
 
-            return component.resolveStateFor(configuration).asVariant();
+            return component.resolveStateFor((ModuleConfigurationMetadata) defaultVariant).asVariant();
         }
+
     }
 }
diff --git a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphSelectionCandidates.java b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphSelectionCandidates.java
index a1da5d264d1cd..1744b9a044087 100644
--- a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphSelectionCandidates.java
+++ b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphSelectionCandidates.java
@@ -34,9 +34,4 @@ public interface GraphSelectionCandidates {
     @Nullable
     VariantGraphResolveState getLegacyVariant();
 
-    /**
-     * Returns the variant that is identified by the given configuration name.
-     */
-    @Nullable
-    VariantGraphResolveState getVariantByConfigurationName(String name);
 }
diff --git a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphVariantSelector.java b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphVariantSelector.java
index 773a5a4e9346f..472e27a326c9c 100644
--- a/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphVariantSelector.java
+++ b/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphVariantSelector.java
@@ -32,6 +32,8 @@
 import org.gradle.api.internal.capabilities.ShadowedCapability;
 import org.gradle.internal.component.external.model.ImmutableCapabilities;
 import org.gradle.internal.component.external.model.ModuleComponentArtifactMetadata;
+import org.gradle.internal.component.external.model.ivy.IvyComponentGraphResolveState;
+import org.gradle.internal.component.local.model.LocalComponentGraphResolveState;
 import org.gradle.internal.component.resolution.failure.ResolutionFailureHandler;
 import org.gradle.internal.deprecation.DeprecationLogger;
 import org.gradle.internal.service.scopes.Scope;
@@ -179,7 +181,16 @@ public VariantGraphResolveState selectLegacyVariant(ImmutableAttributes consumer
      * Select the variant that is identified by the given configuration name.
      */
     public VariantGraphResolveState selectVariantByConfigurationName(String name, ImmutableAttributes consumerAttributes, ComponentGraphResolveState targetComponentState, ImmutableAttributesSchema consumerSchema) {
-        VariantGraphResolveState conf = targetComponentState.getCandidatesForGraphVariantSelection().getVariantByConfigurationName(name);
+
+        VariantGraphResolveState conf;
+        if (targetComponentState instanceof IvyComponentGraphResolveState) {
+            conf = ((IvyComponentGraphResolveState) targetComponentState).getCandidatesForGraphVariantSelection().getVariantByConfigurationName(name);
+        } else if (targetComponentState instanceof LocalComponentGraphResolveState) {
+            conf = ((LocalComponentGraphResolveState) targetComponentState).getCandidatesForGraphVariantSelection().getVariantByConfigurationName(name);
+        } else {
+            throw new IllegalArgumentException("Cannot select a variant by configuration name from '" + targetComponentState.getId() + "'.");
+        }
+
         if (conf == null) {
             throw failureHandler.configurationDoesNotExistFailure(targetComponentState, name);
         }
diff --git a/platforms/software/dependency-management/src/test/groovy/org/gradle/api/internal/artifacts/ivyservice/resolveengine/DependencyGraphBuilderTest.groovy b/platforms/software/dependency-management/src/test/groovy/org/gradle/api/internal/artifacts/ivyservice/resolveengine/DependencyGraphBuilderTest.groovy
index 4919ceaf7e2e8..b57750d531fa2 100644
--- a/platforms/software/dependency-management/src/test/groovy/org/gradle/api/internal/artifacts/ivyservice/resolveengine/DependencyGraphBuilderTest.groovy
+++ b/platforms/software/dependency-management/src/test/groovy/org/gradle/api/internal/artifacts/ivyservice/resolveengine/DependencyGraphBuilderTest.groovy
@@ -1190,7 +1190,7 @@ class DependencyGraphBuilderTest extends Specification {
         dependencyMetaData = new DslOriginDependencyMetadataWrapper(dependencyMetaData, Stub(ModuleDependency) {
             getAttributes() >> ImmutableAttributes.EMPTY
         })
-        from.candidatesForGraphVariantSelection.getVariantByConfigurationName("default").dependencies.add(dependencyMetaData)
+        from.candidatesForGraphVariantSelection.getLegacyVariant().dependencies.add(dependencyMetaData)
         return componentSelector
     }
 
diff --git a/platforms/software/dependency-management/src/test/groovy/org/gradle/internal/component/model/LocalComponentDependencyMetadataTest.groovy b/platforms/software/dependency-management/src/test/groovy/org/gradle/internal/component/model/LocalComponentDependencyMetadataTest.groovy
index 8de0401269776..9dbffe90b66a6 100644
--- a/platforms/software/dependency-management/src/test/groovy/org/gradle/internal/component/model/LocalComponentDependencyMetadataTest.groovy
+++ b/platforms/software/dependency-management/src/test/groovy/org/gradle/internal/component/model/LocalComponentDependencyMetadataTest.groovy
@@ -33,6 +33,9 @@ import org.gradle.api.internal.attributes.ImmutableAttributes
 import org.gradle.api.internal.attributes.immutable.ImmutableAttributesSchema
 import org.gradle.internal.component.external.descriptor.DefaultExclude
 import org.gradle.internal.component.external.model.ImmutableCapabilities
+import org.gradle.internal.component.local.model.LocalComponentGraphResolveMetadata
+import org.gradle.internal.component.local.model.LocalComponentGraphResolveState
+import org.gradle.internal.component.local.model.LocalVariantGraphResolveState
 import org.gradle.internal.component.resolution.failure.exception.VariantSelectionByAttributesException
 import org.gradle.internal.component.resolution.failure.exception.VariantSelectionByNameException
 import org.gradle.util.AttributeTestUtil
@@ -48,11 +51,14 @@ class LocalComponentDependencyMetadataTest extends Specification {
     ComponentIdentifier toComponentId = Stub(ComponentIdentifier) {
         getDisplayName() >> "[target]"
     }
-    ComponentGraphResolveMetadata toComponentMetadata = Mock(ComponentGraphResolveMetadata) {
-        getId() >> toComponentId
-        getModuleVersionId() >> Stub(ModuleVersionIdentifier)
-        getAttributesSchema() >> ImmutableAttributesSchema.EMPTY
-    }
+
+    ComponentGraphResolveMetadata toComponentMetadata = new LocalComponentGraphResolveMetadata(
+        Stub(ModuleVersionIdentifier),
+        toComponentId,
+        "status",
+        ImmutableAttributesSchema.EMPTY
+    )
+
     TestComponentState toComponent = Mock(TestComponentState) {
         getMetadata() >> toComponentMetadata
         getId() >> toComponentId
@@ -382,14 +388,14 @@ Configuration 'bar':
     }
 
 
-    interface TestComponentState extends ComponentGraphResolveState {
+    interface TestComponentState extends LocalComponentGraphResolveState {
         @Override
         TestGraphCandidates getCandidatesForGraphVariantSelection()
     }
 
     interface TestConfigurationState extends ConfigurationGraphResolveState, VariantGraphResolveState {}
 
-    class TestGraphCandidates implements GraphSelectionCandidates {
+    class TestGraphCandidates implements LocalComponentGraphResolveState.LocalComponentGraphSelectionCandidates {
         List<VariantGraphResolveState> variants = []
 
         @Override
@@ -406,5 +412,10 @@ Configuration 'bar':
         VariantGraphResolveState getVariantByConfigurationName(String name) {
             variants.find { it -> it.name == name }
         }
+
+        @Override
+        List<LocalVariantGraphResolveState> getAllSelectableVariants() {
+            return Collections.emptyList()
+        }
     }
 }


Reviews:
user: login: tresat
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-13T14:51:43Z
user: login: jvandort
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T00:32:15Z
user: login: tresat
body: This seems more self-explanatory about where selecting by name is allowed.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T11:59:22Z

Review comments:
diff_hunk: @@ -179,26 +178,6 @@ public List<? extends VariantGraphResolveState> getVariantsForAttributeMatching(
         @Nullable
         @Override
         public VariantGraphResolveState getLegacyVariant() {
-            return doGetVariantByConfigurationName(Dependency.DEFAULT_CONFIGURATION);
-        }
-
-        @Nullable
-        @Override
-        public VariantGraphResolveState getVariantByConfigurationName(String name) {
-            DeprecationLogger.deprecateBehaviour("Selecting a variant by configuration name from a non-Ivy external component.")
-                .willBecomeAnErrorInGradle9()
-                .withUpgradeGuideSection(8, "selecting_variant_by_configuration_name")
-                .nagUser();
-
-            return doGetVariantByConfigurationName(name);
-        }
-
-        @Nullable
-        private VariantGraphResolveState doGetVariantByConfigurationName(String name) {
-            if (!name.equals(Dependency.DEFAULT_CONFIGURATION)) {
-                return null;
-            }
-
path: platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/resolveengine/graph/builder/LenientPlatformGraphResolveState.java
user: login: tresat
body: ❓ Does this still leave `getLegacyVariant` as `@Nullable` or should this be changed?
created_at: 2025-03-13T14:01:24Z
author_association: MEMBER
diff_hunk: @@ -37,5 +39,8 @@ public interface GraphSelectionCandidates {
      * Returns the variant that is identified by the given configuration name.
      */
     @Nullable
-    VariantGraphResolveState getVariantByConfigurationName(String name);
+    default VariantGraphResolveState getVariantByConfigurationName(String name)  {
path: platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphSelectionCandidates.java
user: login: tresat
body: ❗ This is overriden by a few types - shouldn't these method overrides be removed so that subtypes also throw this exception?

Alternatively, it would be good to either deprecate + update Javadoc on this method or just remove it entirely to avoid the internal API looking like it makes this functionality available. 
created_at: 2025-03-13T14:13:59Z
author_association: MEMBER
diff_hunk: @@ -37,5 +39,8 @@ public interface GraphSelectionCandidates {
      * Returns the variant that is identified by the given configuration name.
      */
     @Nullable
-    VariantGraphResolveState getVariantByConfigurationName(String name);
+    default VariantGraphResolveState getVariantByConfigurationName(String name)  {
path: platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphSelectionCandidates.java
user: login: jvandort
body: Switched to instanceof + casting instead of having an interface method that only some implementations implement
created_at: 2025-03-14T00:32:15Z
author_association: MEMBER

PR comments:
user: login: jvandort
created_at: 2025-03-11T20:17:56Z
body: @bot-gradle test this
author_association: MEMBER
user: login: bot-gradle
created_at: 2025-03-11T20:17:59Z
body: <!-- {"replyTargetCommentId":2715611543,"triggeredTeamCityBuilds":[{"buildConfigurationId":"Gradle_Master_Check_Stage_PullRequestFeedback_Trigger","buildId":"95646310","headRef":"a43f539610c00382bc6484507b34eef66f21c35f"}]} -->
I've triggered the following builds:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/build/95646310)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=a43f539610c00382bc6484507b34eef66f21c35f,PullRequestFeedback&amp;)

author_association: COLLABORATOR
user: login: bot-gradle
created_at: 2025-03-11T21:16:12Z
body: The following builds have failed:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/buildConfiguration/Gradle_Master_Check_Stage_PullRequestFeedback_Trigger/95646310)
  - [Build Scan](https://ge.gradle.org/scans?search.buildOutcome=failure&amp;search.tags=CI&amp;search.names=gitCommitId&amp;search.values=a43f539610c00382bc6484507b34eef66f21c35f&amp;)

author_association: COLLABORATOR
user: login: jvandort
created_at: 2025-03-14T12:38:08Z
body: @bot-gradle test this
author_association: MEMBER
user: login: bot-gradle
created_at: 2025-03-14T12:38:14Z
body: <!-- {"replyTargetCommentId":2724561744,"triggeredTeamCityBuilds":[{"buildConfigurationId":"Gradle_Master_Check_Stage_PullRequestFeedback_Trigger","buildId":"95789981","headRef":"738f5adb4215e20372e9664fd2a72d5c778d64d1"}]} -->
I've triggered the following builds:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/build/95789981)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=738f5adb4215e20372e9664fd2a72d5c778d64d1,PullRequestFeedback&amp;)

author_association: COLLABORATOR
user: login: bot-gradle
created_at: 2025-03-14T14:26:51Z
body: The following builds have passed:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/buildConfiguration/Gradle_Master_Check_Stage_PullRequestFeedback_Trigger/95789981)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=738f5adb4215e20372e9664fd2a72d5c778d64d1,PullRequestFeedback&amp;)

author_association: COLLABORATOR