Use base target's toolchain context to resolve its dependencies during aspect propagation

The toolchain context of the aspect is currently used to configure the dependencies of the base target, which is incorrect and results into a number of issues due to missing execution groups or different ones being picked up.

This CL modifies `AspectFunction` to always prepare the `baseTargetUnloadedToolchainContexts` and pass it to `PrerequisiteParameters`. `PrerequisiteParameters#baseTargetUnloadedToolchainContexts` is then used for the target's dependencies while `PrerequisiteParameters#toolchainContexts` continues to be used for the aspect owned dependencies.

The base target toolchains and execution groups will not be available in the aspect implementation function via `ctx.rule.toolchains` and `ctx.rule.exec_groups` unless the aspects path propagates to toolchains.

PiperOrigin-RevId: 687376763
Change-Id: I78167bab7f8648e5855178d5dbcc3fcb02357a38
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolutionHelpers.java b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolutionHelpers.java
index d540714..2099962 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolutionHelpers.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolutionHelpers.java
@@ -185,9 +185,35 @@
   }
 
   public static ExecutionPlatformResult getExecutionPlatformLabel(
-      DependencyKind kind,
+      AttributeDependencyKind kind,
       @Nullable ToolchainCollection<ToolchainContext> toolchainContexts,
+      @Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts,
       ImmutableList<Aspect> aspectsList) {
+    if (aspectsList.isEmpty() || isMainAspect(aspectsList, kind.getOwningAspect())) {
+      return getExecutionPlatformLabel(kind, toolchainContexts);
+    } else if (kind.getOwningAspect() == null) {
+      // During aspect evaluation, use {@code baseTargetUnloadedToolchainContexts} for the base
+      // target's dependencies.
+      return getExecutionPlatformLabel(kind, baseTargetUnloadedToolchainContexts);
+    } else {
+      ExecutionPlatformResult executionPlatformResult =
+          getExecutionPlatformLabel(kind, toolchainContexts);
+      if (executionPlatformResult.kind() == ExecutionPlatformResult.Kind.ERROR) {
+        // TODO(b/373963347): Make the toolchain contexts of base aspects available to be used with
+        // their corresponding dependencies.
+        // Currently dependencies of the base aspects are resolved with the toolchain context of the
+        // main aspect, skip errors as actual errors would be reported during the base aspect
+        // evaluation.
+        return ExecutionPlatformResult.ofSkip();
+      } else {
+        return executionPlatformResult;
+      }
+    }
+  }
+
+  private static ExecutionPlatformResult getExecutionPlatformLabel(
+      AttributeDependencyKind kind,
+      @Nullable ToolchainCollection<? extends ToolchainContext> toolchainContexts) {
     if (toolchainContexts == null) {
       return ExecutionPlatformResult.ofNullLabel();
     }
@@ -210,16 +236,6 @@
           : ExecutionPlatformResult.ofLabel(platform.label());
     }
 
-    // `execGroup` could not be found. If `aspectsList` is non-empty, `toolchainContexts` only
-    // contains the exec groups of the main aspect. Skips the dependency if it's not the main
-    // aspect.
-    //
-    // TODO(b/256617733): Make a decision on whether the exec groups of the target and the base
-    // aspects should be merged in `toolchainContexts`.
-    if (!aspectsList.isEmpty() && !isMainAspect(aspectsList, kind.getOwningAspect())) {
-      return ExecutionPlatformResult.ofSkip();
-    }
-
     return ExecutionPlatformResult.ofError(
         String.format(
             "Attr '%s' declares a transition for non-existent exec group '%s'",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/producers/DependencyProducer.java b/src/main/java/com/google/devtools/build/lib/analysis/producers/DependencyProducer.java
index 485a139..2738e01 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/producers/DependencyProducer.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/producers/DependencyProducer.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.analysis.AnalysisRootCauseEvent;
 import com.google.devtools.build.lib.analysis.DependencyKind;
+import com.google.devtools.build.lib.analysis.DependencyKind.AttributeDependencyKind;
 import com.google.devtools.build.lib.analysis.DependencyKind.ToolchainDependencyKind;
 import com.google.devtools.build.lib.analysis.DependencyResolutionHelpers;
 import com.google.devtools.build.lib.analysis.DependencyResolutionHelpers.ExecutionPlatformResult;
@@ -159,7 +160,11 @@
             .attributes(parameters.attributeMap())
             .analysisData(parameters.starlarkTransitionProvider());
     ExecutionPlatformResult executionPlatformResult =
-        getExecutionPlatformLabel(kind, parameters.toolchainContexts(), parameters.aspects());
+        getExecutionPlatformLabel(
+            (AttributeDependencyKind) kind,
+            parameters.toolchainContexts(),
+            parameters.baseTargetToolchainContexts(),
+            parameters.aspects());
     switch (executionPlatformResult.kind()) {
       case LABEL -> transitionData.executionPlatform(executionPlatformResult.label());
       case NULL_LABEL -> transitionData.executionPlatform(null);
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
index 106bbd9..4f40af0 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
@@ -390,7 +390,7 @@
       }
 
       ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts = null;
-      if (canAspectsPropagateToToolchains(topologicalAspectPath, target)) {
+      if (target.isRule()) {
         Pair<ToolchainCollection<UnloadedToolchainContext>, Boolean> contextOrRestart =
             getBaseTargetUnloadedToolchainContexts(
                 state, targetAndConfiguration, key.getBaseConfiguredTargetKey(), env);
@@ -463,7 +463,11 @@
       try {
         baseTargetToolchainContexts =
             getBaseTargetToolchainContexts(
-                baseTargetUnloadedToolchainContexts, aspect, target, depValueMap);
+                baseTargetUnloadedToolchainContexts,
+                aspect,
+                target,
+                topologicalAspectPath,
+                depValueMap);
       } catch (MergingException e) {
         env.getListener().handle(Event.error(target.getLocation(), e.getMessage()));
         throw new AspectFunctionException(
@@ -524,9 +528,11 @@
           ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts,
           Aspect aspect,
           Target target,
+          ImmutableList<Aspect> aspectsPath,
           OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> depValueMap)
           throws MergingException {
-    if (baseTargetUnloadedToolchainContexts == null) {
+    if (baseTargetUnloadedToolchainContexts == null
+        || !canAspectsPropagateToToolchains(aspectsPath, target)) {
       return null;
     }
     String description =
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/BUILD b/src/test/java/com/google/devtools/build/lib/starlark/BUILD
index 926ace0..03d5145 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/starlark/BUILD
@@ -48,6 +48,7 @@
         "//src/main/java/com/google/devtools/build/lib/analysis:config/starlark_defined_config_transition",
         "//src/main/java/com/google/devtools/build/lib/analysis:configured_target",
         "//src/main/java/com/google/devtools/build/lib/analysis:file_provider",
+        "//src/main/java/com/google/devtools/build/lib/analysis:platform_options",
         "//src/main/java/com/google/devtools/build/lib/analysis:run_environment_info",
         "//src/main/java/com/google/devtools/build/lib/analysis:starlark/args",
         "//src/main/java/com/google/devtools/build/lib/analysis:starlark/starlark_exec_group_collection",
@@ -85,6 +86,7 @@
         "//src/test/java/com/google/devtools/build/lib/analysis/util",
         "//src/test/java/com/google/devtools/build/lib/bazel/bzlmod:util",
         "//src/test/java/com/google/devtools/build/lib/exec/util",
+        "//src/test/java/com/google/devtools/build/lib/packages:testutil",
         "//src/test/java/com/google/devtools/build/lib/rules/python:PythonTestUtils",
         "//src/test/java/com/google/devtools/build/lib/starlark/util",
         "//src/test/java/com/google/devtools/build/lib/testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkAspectsToolchainPropagationTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkAspectsToolchainPropagationTest.java
index 74f4ff4..f6cac3a 100644
--- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkAspectsToolchainPropagationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkAspectsToolchainPropagationTest.java
@@ -14,8 +14,11 @@
 package com.google.devtools.build.lib.starlark;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.MoreCollectors.onlyElement;
+import static com.google.common.collect.Streams.stream;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.devtools.build.lib.skyframe.BzlLoadValue.keyForBuild;
+import static org.junit.Assert.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -24,6 +27,9 @@
 import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.analysis.AnalysisResult;
 import com.google.devtools.build.lib.analysis.ConfiguredAspect;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.PlatformOptions;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
 import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
 import com.google.devtools.build.lib.analysis.util.MockRule;
 import com.google.devtools.build.lib.analysis.util.TestAspects.DepsVisitingFileAspect;
@@ -31,9 +37,11 @@
 import com.google.devtools.build.lib.packages.RuleClass.ToolchainResolutionMode;
 import com.google.devtools.build.lib.packages.StarlarkInfo;
 import com.google.devtools.build.lib.packages.StarlarkProvider;
+import com.google.devtools.build.lib.packages.util.Crosstool.CcToolchainConfig;
 import com.google.devtools.build.lib.skyframe.AspectKeyCreator.AspectKey;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
 import com.google.devtools.build.skyframe.SkyKey;
+import com.google.testing.junit.testparameterinjector.TestParameter;
 import com.google.testing.junit.testparameterinjector.TestParameterInjector;
 import com.google.testing.junit.testparameterinjector.TestParameters;
 import org.junit.Before;
@@ -182,28 +190,41 @@
           toolchain = ":foo_for_all",
           toolchain_type = "//rule:toolchain_type_3",
         )
+
+        toolchain(
+          name = "foo_toolchain_exec_1",
+          toolchain = ":foo",
+          exec_compatible_with = ['//platforms:constraint_1'],
+          toolchain_type = "//rule:toolchain_type_1",
+        )
+
+        toolchain(
+          name = "foo_toolchain_exec_2",
+          toolchain = ":foo",
+          exec_compatible_with = ['//platforms:constraint_2'],
+          toolchain_type = "//rule:toolchain_type_2",
+        )
         """);
 
     scratch.overwriteFile(
         "platforms/BUILD",
         """
-        constraint_setting(name = "setting")
+        constraint_setting(name = "setting_1")
+        constraint_setting(name = "setting_2")
 
         constraint_value(
             name = "constraint_1",
-            constraint_setting = ":setting",
+            constraint_setting = ":setting_1",
         )
-
         constraint_value(
             name = "constraint_2",
-            constraint_setting = ":setting",
+            constraint_setting = ":setting_2",
         )
 
         platform(
             name = "platform_1",
             constraint_values = [":constraint_1"],
         )
-
         platform(
             name = "platform_2",
             constraint_values = [":constraint_2"],
@@ -213,6 +234,14 @@
             },
         )
         """);
+    getAnalysisMock()
+        .ccSupport()
+        .setupCcToolchainConfig(
+            mockToolsConfig,
+            CcToolchainConfig.builder()
+                .withToolchainTargetConstraints("@@//platforms:constraint_1")
+                .withToolchainExecConstraints("@@//platforms:constraint_1")
+                .withCpu("fake"));
   }
 
   @Before
@@ -1298,7 +1327,8 @@
   }
 
   @Test
-  public void aspectDoesNotPropagatesToToolchain_cannotSeeTargetToolchains() throws Exception {
+  public void aspectDoesNotPropagatesToToolchain_cannotSeeTargetToolchains(
+      @TestParameter boolean autoExecGroups) throws Exception {
     scratch.file(
         "test/defs.bzl",
         """
@@ -1307,7 +1337,7 @@
           print(ctx.rule.toolchains['//rule:toolchain_type_1'])
           return [AspectProvider(value = [])]
 
-        toolchain_aspect = aspect(
+        non_toolchain_aspect = aspect(
           implementation = _impl,
         )
 
@@ -1325,18 +1355,58 @@
         load('//test:defs.bzl', 'r1')
         r1(name = 't1')
         """);
-    useConfiguration("--extra_toolchains=//toolchain:foo_toolchain");
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain",
+        "--incompatible_auto_exec_groups=" + autoExecGroups);
 
     reporter.removeHandler(failFastHandler);
-    try {
-      var unused = update(ImmutableList.of("//test:defs.bzl%toolchain_aspect"), "//test:t1");
-    } catch (Exception unused) {
-      // expect to fail
-    }
+    assertThrows(
+        ViewCreationFailedException.class,
+        () -> update(ImmutableList.of("//test:defs.bzl%non_toolchain_aspect"), "//test:t1"));
     assertContainsEvent("Error: Toolchains are not valid in this context");
   }
 
   @Test
+  public void aspectDoesNotPropagatesToToolchain_cannotSeeTargetExecGroups(
+      @TestParameter boolean autoExecGroups) throws Exception {
+    scratch.file(
+        "test/defs.bzl",
+        """
+        AspectProvider = provider()
+        def _impl(target, ctx):
+          print(ctx.rule.exec_groups['gp'])
+          return [AspectProvider(value = [])]
+
+        non_toolchain_aspect = aspect(
+          implementation = _impl,
+        )
+
+        def _rule_impl(ctx):
+          pass
+
+        r1 = rule(
+          implementation = _rule_impl,
+          exec_groups = {"gp": exec_group(toolchains = ['//rule:toolchain_type_1'])},
+        )
+        """);
+    scratch.file(
+        "test/BUILD",
+        """
+        load('//test:defs.bzl', 'r1')
+        r1(name = 't1')
+        """);
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain",
+        "--incompatible_auto_exec_groups=" + autoExecGroups);
+
+    reporter.removeHandler(failFastHandler);
+    assertThrows(
+        ViewCreationFailedException.class,
+        () -> update(ImmutableList.of("//test:defs.bzl%non_toolchain_aspect"), "//test:t1"));
+    assertContainsEvent("Error: exec_groups are not valid in this context");
+  }
+
+  @Test
   public void requiredAspectPropagatesToToolchain_providersCollected() throws Exception {
     scratch.file(
         "test/defs.bzl",
@@ -1836,6 +1906,336 @@
         .containsExactly("required_aspect on @@//test:target_without_toolchain");
   }
 
+  @Test
+  public void aspectUsesBaseTargetToolchainsToConfigureTargetDepsWithDefaultExecGp_autoExecGps()
+      throws Exception {
+    scratch.file(
+        "test/defs.bzl",
+        """
+        def _impl(target, ctx):
+          return []
+
+        my_aspect = aspect(
+          implementation = _impl,
+          toolchains = ['//rule:toolchain_type_1'],
+          attr_aspects = ['_tool'],
+        )
+
+        def _rule_impl(ctx):
+          pass
+
+        r1 = rule(
+          implementation = _rule_impl,
+          toolchains = ['//rule:toolchain_type_2'],
+          attrs = {
+            "_tool": attr.label(default='//test:tool', cfg='exec'),
+          },
+        )
+        """);
+    scratch.file(
+        "test/BUILD",
+        """
+        load('//test:defs.bzl', 'r1')
+        r1(name = 't1')
+        sh_binary(name = 'tool', srcs = ['test.sh'])
+        """);
+    scratch.file("test/test.sh", "");
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain_exec_1,//toolchain:foo_toolchain_exec_2",
+        "--extra_execution_platforms=//platforms:platform_1,//platforms:platform_2",
+        "--incompatible_auto_exec_groups=True",
+        "--incompatible_enable_cc_toolchain_resolution");
+
+    var analysisResult = update(ImmutableList.of("//test:defs.bzl%my_aspect"), "//test:t1");
+
+    ConfiguredTarget topLevelTarget = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    var topLevelTargetDeps =
+        getDirectDeps(ConfiguredTargetKey.fromConfiguredTarget(topLevelTarget));
+
+    ConfiguredTargetKey toolDependencyFromTarget =
+        (ConfiguredTargetKey)
+            stream(topLevelTargetDeps)
+                .filter(e -> isConfiguredTarget(e, "//test:tool"))
+                .collect(onlyElement());
+
+    AspectKey aspectOnToolDependnecyKey =
+        Iterables.getOnlyElement(getAspectKeys("//test:tool", "//test:defs.bzl%my_aspect"));
+
+    // The aspect used the base target's toolchain to request the target's dependency, so the
+    // two keys are equal.
+    assertThat(toolDependencyFromTarget)
+        .isEqualTo(aspectOnToolDependnecyKey.getBaseConfiguredTargetKey());
+
+    // The //test:tool target is requested only once and its key contains the execution platform of
+    // its parent's (//test:t1) toolchain
+    ImmutableList<ConfiguredTargetKey> toolDependencyKey = getConfiguredTargetKey("//test:tool");
+    assertThat(toolDependencyKey).hasSize(1);
+
+    // //test:tool gets the execution platform of the default exec gp, when automatic execution
+    // groups are enabled, the default exec gp will have the basic execution platform.
+    assertThat(
+            toolDependencyKey
+                .get(0)
+                .getConfigurationKey()
+                .getOptions()
+                .get(PlatformOptions.class)
+                .platforms)
+        .containsExactly(Label.parseCanonicalUnchecked("//platforms:platform_1"));
+  }
+
+  @Test
+  public void aspectUsesBaseTargetToolchainsToConfigureTargetDepsWithDefaultExecGp_noAutoExecGps()
+      throws Exception {
+    scratch.file(
+        "test/defs.bzl",
+        """
+        def _impl(target, ctx):
+          return []
+
+        my_aspect = aspect(
+          implementation = _impl,
+          toolchains = ['//rule:toolchain_type_1'],
+          attr_aspects = ['_tool'],
+        )
+
+        def _rule_impl(ctx):
+          pass
+
+        r1 = rule(
+          implementation = _rule_impl,
+          toolchains = ['//rule:toolchain_type_2'],
+          attrs = {
+            "_tool": attr.label(default='//test:tool', cfg='exec'),
+          },
+        )
+        """);
+    scratch.file(
+        "test/BUILD",
+        """
+        load('//test:defs.bzl', 'r1')
+        r1(name = 't1')
+        sh_binary(name = 'tool', srcs = ['test.sh'])
+        """);
+    scratch.file("test/test.sh", "");
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain_exec_1,//toolchain:foo_toolchain_exec_2",
+        "--extra_execution_platforms=//platforms:platform_1,//platforms:platform_2",
+        "--incompatible_auto_exec_groups=False",
+        "--incompatible_enable_cc_toolchain_resolution");
+
+    var analysisResult = update(ImmutableList.of("//test:defs.bzl%my_aspect"), "//test:t1");
+
+    ConfiguredTarget topLevelTarget = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    var topLevelTargetDeps =
+        getDirectDeps(ConfiguredTargetKey.fromConfiguredTarget(topLevelTarget));
+
+    ConfiguredTargetKey toolDependencyFromTarget =
+        (ConfiguredTargetKey)
+            stream(topLevelTargetDeps)
+                .filter(e -> isConfiguredTarget(e, "//test:tool"))
+                .collect(onlyElement());
+
+    AspectKey aspectOnToolDependnecyKey =
+        Iterables.getOnlyElement(getAspectKeys("//test:tool", "//test:defs.bzl%my_aspect"));
+
+    // The aspect used the base target's toolchain to request the target's dependency, so the
+    // two keys are equal.
+    assertThat(toolDependencyFromTarget)
+        .isEqualTo(aspectOnToolDependnecyKey.getBaseConfiguredTargetKey());
+
+    // The //test:tool target is requested only once and its key contains the execution platform of
+    // its parent's (//test:t1) toolchain
+    ImmutableList<ConfiguredTargetKey> toolDependencyKey = getConfiguredTargetKey("//test:tool");
+    assertThat(toolDependencyKey).hasSize(1);
+
+    // //test:tool gets the execution platform of the default exec gp, when automatic execution
+    // groups are disabled, the default exec gp will have the execution platform of the only
+    // toolchain type it has.
+    assertThat(
+            toolDependencyKey
+                .get(0)
+                .getConfigurationKey()
+                .getOptions()
+                .get(PlatformOptions.class)
+                .platforms)
+        .containsExactly(Label.parseCanonicalUnchecked("//platforms:platform_2"));
+  }
+
+  @Test
+  public void aspectUsesBaseTargetToolchainsToConfigureTargetDepsWithCustomExecGp(
+      @TestParameter boolean autoExecGroups) throws Exception {
+    scratch.file(
+        "test/defs.bzl",
+        """
+        def _impl(target, ctx):
+          return []
+
+        my_aspect = aspect(
+          implementation = _impl,
+          exec_groups = {"gp": exec_group(toolchains = ['//rule:toolchain_type_1'])},
+          attr_aspects = ['_tool'],
+        )
+
+        def _rule_impl(ctx):
+          pass
+
+        r1 = rule(
+          implementation = _rule_impl,
+          exec_groups = {"gp": exec_group(toolchains = ['//rule:toolchain_type_2'])},
+          attrs = {
+            "_tool": attr.label(default='//test:tool', cfg = config.exec(exec_group = 'gp')),
+          },
+        )
+        """);
+    scratch.file(
+        "test/BUILD",
+        """
+        load('//test:defs.bzl', 'r1')
+        r1(name = 't1')
+        sh_binary(name = 'tool', srcs = ['test.sh'])
+        """);
+    scratch.file("test/test.sh", "");
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain_exec_1,//toolchain:foo_toolchain_exec_2",
+        "--extra_execution_platforms=//platforms:platform_1,//platforms:platform_2",
+        "--incompatible_auto_exec_groups=" + autoExecGroups,
+        "--incompatible_enable_cc_toolchain_resolution");
+
+    var analysisResult = update(ImmutableList.of("//test:defs.bzl%my_aspect"), "//test:t1");
+
+    ConfiguredTarget topLevelTarget = Iterables.getOnlyElement(analysisResult.getTargetsToBuild());
+    var topLevelTargetDeps =
+        getDirectDeps(ConfiguredTargetKey.fromConfiguredTarget(topLevelTarget));
+
+    ConfiguredTargetKey toolDependencyFromTarget =
+        (ConfiguredTargetKey)
+            stream(topLevelTargetDeps)
+                .filter(e -> isConfiguredTarget(e, "//test:tool"))
+                .collect(onlyElement());
+
+    AspectKey aspectOnToolDependnecyKey =
+        Iterables.getOnlyElement(getAspectKeys("//test:tool", "//test:defs.bzl%my_aspect"));
+
+    // The aspect used the base target's toolchain to request the target's dependency, so the
+    // two keys are equal.
+    assertThat(toolDependencyFromTarget)
+        .isEqualTo(aspectOnToolDependnecyKey.getBaseConfiguredTargetKey());
+
+    // The //test:tool target is requested only once and its key contains the execution platform of
+    // the exec group 'gp' from its parent (//test:t1).
+    ImmutableList<ConfiguredTargetKey> toolDependencyKey = getConfiguredTargetKey("//test:tool");
+    assertThat(toolDependencyKey).hasSize(1);
+    assertThat(
+            toolDependencyKey
+                .get(0)
+                .getConfigurationKey()
+                .getOptions()
+                .get(PlatformOptions.class)
+                .platforms)
+        .containsExactly(Label.parseCanonicalUnchecked("//platforms:platform_2"));
+  }
+
+  @Test
+  public void aspectAndRuleHaveDifferentExecutionPlatforms_buildSucceeds(
+      @TestParameter boolean autoExecGroups) throws Exception {
+    scratch.file(
+        "test/defs.bzl",
+        """
+        def _impl(target, ctx):
+          return []
+
+        toolchain_aspect = aspect(
+          implementation = _impl,
+          exec_groups = {"gp": exec_group(toolchains = ['//rule:toolchain_type_1'])},
+          attrs = {
+            "_tool": attr.label(default='//test:aspect_tool', cfg=config.exec(exec_group = 'gp')),
+          },
+          toolchains_aspects = ['//rule:toolchain_type_1'],
+        )
+
+        def _rule_impl(ctx):
+          pass
+
+        r1 = rule(
+          implementation = _rule_impl,
+          exec_groups = {
+            "gp": exec_group(
+              toolchains = ['//rule:toolchain_type_1'],
+              exec_compatible_with = ['//platforms:constraint_2']
+            )
+          },
+          attrs = {
+            "_tool": attr.label(default='//test:rule_tool', cfg = config.exec(exec_group = 'gp')),
+          },
+        )
+        """);
+    scratch.file(
+        "test/BUILD",
+        """
+        load('//test:defs.bzl', 'r1')
+        r1(name = 't1')
+        sh_binary(name = 'rule_tool', srcs = ['test.sh'])
+        sh_binary(name = 'aspect_tool', srcs = ['test.sh'])
+        """);
+    useConfiguration(
+        "--extra_toolchains=//toolchain:foo_toolchain",
+        "--extra_execution_platforms=//platforms:platform_1,//platforms:platform_2",
+        "--incompatible_auto_exec_groups=" + autoExecGroups,
+        "--incompatible_enable_cc_toolchain_resolution");
+
+    var unused = update(ImmutableList.of("//test:defs.bzl%toolchain_aspect"), "//test:t1");
+
+    // //test:rule_tool uses //platforms:platform_2
+    ConfiguredTargetKey ruleTool =
+        Iterables.getOnlyElement(getConfiguredTargetKey("//test:rule_tool"));
+    assertThat(ruleTool.getConfigurationKey().getOptions().get(PlatformOptions.class).platforms)
+        .containsExactly(Label.parseCanonicalUnchecked("//platforms:platform_2"));
+
+    // //test:aspect_tool uses //platforms:platform_1
+    ConfiguredTargetKey aspectTool =
+        Iterables.getOnlyElement(getConfiguredTargetKey("//test:aspect_tool"));
+    assertThat(aspectTool.getConfigurationKey().getOptions().get(PlatformOptions.class).platforms)
+        .containsExactly(Label.parseCanonicalUnchecked("//platforms:platform_1"));
+
+    // aspect propagates to the rule's toolchain (with //platforms:platform_2 execution platform)
+    // not to its own toolchain
+    var aspectOnTarget =
+        Iterables.getOnlyElement(getAspectKeys("//test:t1", "//test:defs.bzl%toolchain_aspect"));
+    var aspectOnTargetDeps = getDirectDeps(aspectOnTarget);
+
+    var aspectsOnToolchain =
+        Iterables.transform(
+            Iterables.filter(aspectOnTargetDeps, AspectKey.class),
+            k ->
+                k.getAspectName()
+                    + " on "
+                    + k.getLabel()
+                    + ", exec_platform: "
+                    + k.getBaseConfiguredTargetKey().getExecutionPlatformLabel());
+    assertThat(aspectsOnToolchain)
+        .containsExactly(
+            "//test:defs.bzl%toolchain_aspect on //toolchain:foo,"
+                + " exec_platform: //platforms:platform_2");
+  }
+
+  private ImmutableList<ConfiguredTargetKey> getConfiguredTargetKey(String targetLabel) {
+    return skyframeExecutor.getEvaluator().getInMemoryGraph().getAllNodeEntries().stream()
+        .filter(n -> isConfiguredTarget(n.getKey(), targetLabel))
+        .map(n -> (ConfiguredTargetKey) n.getKey())
+        .collect(toImmutableList());
+  }
+
+  private Iterable<SkyKey> getDirectDeps(SkyKey key) throws Exception {
+    return skyframeExecutor
+        .getEvaluator()
+        .getExistingEntryAtCurrentlyEvaluatingVersion(key)
+        .getDirectDeps();
+  }
+
+  private static boolean isConfiguredTarget(SkyKey key, String label) {
+    return key instanceof ConfiguredTargetKey ctKey && ctKey.getLabel().toString().equals(label);
+  }
+
   private ImmutableList<AspectKey> getAspectKeys(String targetLabel, String aspectLabel) {
     return skyframeExecutor.getEvaluator().getDoneValues().entrySet().stream()
         .filter(