Add validation that all targets used as platforms actually advertise

they provide PlatformInfo.

Fixes #10282.

Closes #10307.

PiperOrigin-RevId: 282766536
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformLookupUtil.java b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformLookupUtil.java
index 6c96440..c60fb08 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformLookupUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformLookupUtil.java
@@ -16,18 +16,28 @@
 
 import static com.google.common.base.Predicates.equalTo;
 import static com.google.common.base.Predicates.not;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.devtools.build.lib.skyframe.RegisteredExecutionPlatformsFunction.hasPlatformInfo;
 import static java.util.stream.Collectors.joining;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.analysis.PlatformConfiguration;
 import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
 import com.google.devtools.build.lib.analysis.platform.PlatformProviderUtils;
 import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
 import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetFunction.ConfiguredValueCreationException;
 import com.google.devtools.build.skyframe.SkyFunction.Environment;
 import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.ValueOrException;
 import com.google.devtools.build.skyframe.ValueOrException3;
 import java.util.HashMap;
 import java.util.Map;
@@ -38,9 +48,16 @@
 
   @Nullable
   public static Map<ConfiguredTargetKey, PlatformInfo> getPlatformInfo(
-      Iterable<ConfiguredTargetKey> platformKeys, Environment env, boolean sanityCheckConfiguration)
+      ImmutableList<ConfiguredTargetKey> platformKeys,
+      Environment env,
+      boolean sanityCheckConfiguration)
       throws InterruptedException, InvalidPlatformException {
 
+    validatePlatformKeys(platformKeys, env);
+    if (env.valuesMissing()) {
+      return null;
+    }
+
     Map<
             SkyKey,
             ValueOrException3<
@@ -66,6 +83,53 @@
     return platforms;
   }
 
+  /** Validate that all keys are for actual platform targets. */
+  private static void validatePlatformKeys(
+      ImmutableList<ConfiguredTargetKey> platformKeys, Environment env)
+      throws InterruptedException, InvalidPlatformException {
+    // Load the packages. This should already be in Skyframe and thus not require a restart.
+    ImmutableSet<PackageValue.Key> packageKeys =
+        platformKeys.stream()
+            .map(ConfiguredTargetKey::getLabel)
+            .map(Label::getPackageIdentifier)
+            .distinct()
+            .map(PackageValue::key)
+            .collect(toImmutableSet());
+
+    Map<SkyKey, ValueOrException<NoSuchPackageException>> values =
+        env.getValuesOrThrow(packageKeys, NoSuchPackageException.class);
+    boolean valuesMissing = env.valuesMissing();
+    Map<PackageIdentifier, Package> packages = valuesMissing ? null : new HashMap<>();
+    for (Map.Entry<SkyKey, ValueOrException<NoSuchPackageException>> value : values.entrySet()) {
+      try {
+        PackageValue packageValue = (PackageValue) value.getValue().get();
+        if (!valuesMissing && packageValue != null) {
+          packages.put(packageValue.getPackage().getPackageIdentifier(), packageValue.getPackage());
+        }
+      } catch (NoSuchPackageException e) {
+        throw new InvalidPlatformException(e);
+      }
+    }
+    if (valuesMissing) {
+      return;
+    }
+
+    // Now check each platform.
+    for (ConfiguredTargetKey platformKey : platformKeys) {
+      try {
+        Label platformLabel = platformKey.getLabel();
+        Target target =
+            packages.get(platformLabel.getPackageIdentifier()).getTarget(platformLabel.getName());
+        if (!hasPlatformInfo(target)) {
+          // validation failure
+          throw new InvalidPlatformException(platformLabel);
+        }
+      } catch (NoSuchTargetException e) {
+        throw new InvalidPlatformException(e);
+      }
+    }
+  }
+
   /**
    * Returns the {@link PlatformInfo} provider from the {@link ConfiguredTarget} in the {@link
    * ValueOrException3}, or {@code null} if the {@link ConfiguredTarget} is not present. If the
@@ -120,7 +184,7 @@
     } catch (ConfiguredValueCreationException e) {
       throw new InvalidPlatformException(key.getLabel(), e);
     } catch (NoSuchThingException e) {
-      throw new InvalidPlatformException(key.getLabel(), e);
+      throw new InvalidPlatformException(e);
     } catch (ActionConflictException e) {
       throw new InvalidPlatformException(key.getLabel(), e);
     }
@@ -138,7 +202,7 @@
       super(formatError(label, DEFAULT_ERROR), e);
     }
 
-    public InvalidPlatformException(Label label, NoSuchThingException e) {
+    public InvalidPlatformException(NoSuchThingException e) {
       // Just propagate the inner exception, because it's directly actionable.
       super(e);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunction.java
index 26a1dd4..e154ee6 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunction.java
@@ -225,6 +225,18 @@
     }
   }
 
+  static boolean hasPlatformInfo(Target target) {
+    // If the rule uses toolchain resolution, it can't be used as a target or exec platform.
+    RuleClass ruleClass = target.getAssociatedRule().getRuleClassObject();
+    if (ruleClass == null || ruleClass.useToolchainResolution()) {
+      return false;
+    }
+
+    return ruleClass.getAdvertisedProviders().advertises(PlatformInfo.class);
+  }
+
+  // This class uses AutoValue solely to get default equals/hashCode behavior, which is needed to
+  // make skyframe serialization work properly.
   @AutoValue
   @AutoCodec
   abstract static class HasPlatformInfo extends FilteringPolicy {
@@ -234,14 +246,7 @@
       if (explicit) {
         return true;
       }
-
-      // If the rule requires platforms or toolchain resolution, it can't be used as a platform.
-      RuleClass ruleClass = target.getAssociatedRule().getRuleClassObject();
-      if (ruleClass == null || ruleClass.useToolchainResolution()) {
-        return false;
-      }
-
-      return ruleClass.getAdvertisedProviders().advertises(PlatformInfo.class);
+      return hasPlatformInfo(target);
     }
 
     @AutoCodec.Instantiator
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformLookupUtilTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformLookupUtilTest.java
index 1ed9020..3e638be 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformLookupUtilTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformLookupUtilTest.java
@@ -138,9 +138,9 @@
       return GET_PLATFORM_INFO_FUNCTION;
     }
 
-    abstract Iterable<ConfiguredTargetKey> platformKeys();
+    abstract ImmutableList<ConfiguredTargetKey> platformKeys();
 
-    public static GetPlatformInfoKey create(Iterable<ConfiguredTargetKey> platformKeys) {
+    public static GetPlatformInfoKey create(ImmutableList<ConfiguredTargetKey> platformKeys) {
       return new AutoValue_PlatformLookupUtilTest_GetPlatformInfoKey(platformKeys);
     }
   }
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/SingleToolchainResolutionFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/SingleToolchainResolutionFunctionTest.java
index ee4b18e..f2a5b62 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/SingleToolchainResolutionFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/SingleToolchainResolutionFunctionTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.devtools.build.skyframe.EvaluationResultSubjectFactory.assertThatEvaluationResult;
+import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
@@ -40,6 +41,7 @@
 import com.google.devtools.build.skyframe.EvaluationResult;
 import com.google.devtools.build.skyframe.SkyKey;
 import javax.annotation.Nullable;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -56,11 +58,12 @@
   @AutoCodec @AutoCodec.VisibleForSerialization
   static final ConfiguredTargetKey MAC_CTKEY = Mockito.mock(ConfiguredTargetKey.class);
 
-  static {
-    Mockito.when(LINUX_CTKEY.functionName())
-        .thenReturn(InjectedActionLookupKey.INJECTED_ACTION_LOOKUP);
-    Mockito.when(MAC_CTKEY.functionName())
-        .thenReturn(InjectedActionLookupKey.INJECTED_ACTION_LOOKUP);
+  @Before
+  public void setUpKeys() {
+    when(LINUX_CTKEY.functionName()).thenReturn(InjectedActionLookupKey.INJECTED_ACTION_LOOKUP);
+    when(LINUX_CTKEY.getLabel()).thenReturn(Label.parseAbsoluteUnchecked("//platforms:linux"));
+    when(MAC_CTKEY.functionName()).thenReturn(InjectedActionLookupKey.INJECTED_ACTION_LOOKUP);
+    when(MAC_CTKEY.getLabel()).thenReturn(Label.parseAbsoluteUnchecked("//platforms:mac"));
   }
 
   private static ConfiguredTargetValue createConfiguredTargetValue(
diff --git a/src/test/shell/bazel/toolchain_test.sh b/src/test/shell/bazel/toolchain_test.sh
index e29a454..4df2123 100755
--- a/src/test/shell/bazel/toolchain_test.sh
+++ b/src/test/shell/bazel/toolchain_test.sh
@@ -1375,6 +1375,36 @@
   expect_log "Misconfigured toolchains: //:upper_toolchain is declared as a toolchain but has inappropriate dependencies"
 }
 
+
+# Catch the error when a target platform requires a configuration which contains the same target platform.
+# This can only happen when the target platform is not actually a platform.
+function test_target_platform_cycle() {
+  cat >> hello.sh <<EOF
+  #!/bin/sh
+  echo "Hello world"
+EOF
+  cat >> target.sh <<EOF
+  #!/bin/sh
+  echo "Hello target"
+EOF
+  chmod +x hello.sh
+  chmod +x target.sh
+  cat >> BUILD <<EOF
+sh_binary(
+  name = "hello",
+  srcs = ["hello.sh"],
+)
+sh_binary(
+  name = "target",
+  srcs = ["target.sh"],
+)
+EOF
+
+  bazel build --platforms=//:hello //:target &> $TEST_log && fail "Build succeeded unexpectedly"
+  expect_log "While resolving toolchains for target //:target: Target //:hello was referenced as a platform, but does not provide PlatformInfo"
+}
+
+
 function test_platform_duplicate_constraint_error() {
   # Write a platform with duplicate constraint values for the same setting.
   mkdir -p platform