Add a "canExec" method to SpawnActionContext and SpawnRunner.

This is used by the ProxySpawnActionContext to determine at runtime
whether a spawn strategy can execute a given spawn.

Adds the flag --incompatible_list_based_execution_strategy_selection,
which is used to make this an opt-in change that will be managed by the
incompatible flags process.

The flag's GitHub issue is here:
https://github.com/bazelbuild/bazel/issues/7480

RELNOTES[INC]: The flag --incompatible_list_based_execution_strategy_selection
was added and is used to ease the migration to the new style of specifying
execution strategy selection and fallback behavior. The documentation for
this flag is here: https://github.com/bazelbuild/bazel/issues/7480

PiperOrigin-RevId: 234877574
diff --git a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
index e95413b..9759999 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java
@@ -77,6 +77,11 @@
   }
 
   @Override
+  public boolean canExec(Spawn spawn) {
+    return spawnRunner.canExec(spawn);
+  }
+
+  @Override
   public List<SpawnResult> exec(
       Spawn spawn,
       ActionExecutionContext actionExecutionContext,
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
index 88e36d5..e3099fc 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
@@ -26,10 +26,10 @@
 import com.google.devtools.common.options.BoolOrEnumConverter;
 import com.google.devtools.common.options.Converters.AssignmentToListOfValuesConverter;
 import com.google.devtools.common.options.Converters.CommaSeparatedNonEmptyOptionListConverter;
-import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionDocumentationCategory;
 import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionMetadataTag;
 import com.google.devtools.common.options.Options;
 import com.google.devtools.common.options.OptionsBase;
 import com.google.devtools.common.options.OptionsParsingException;
@@ -58,6 +58,18 @@
   public static final ExecutionOptions DEFAULTS = Options.getDefaults(ExecutionOptions.class);
 
   @Option(
+      name = "incompatible_list_based_execution_strategy_selection",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
+      effectTags = {OptionEffectTag.EXECUTION},
+      metadataTags = {
+        OptionMetadataTag.INCOMPATIBLE_CHANGE,
+        OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
+      },
+      help = "See https://github.com/bazelbuild/bazel/issues/7480")
+  public boolean incompatibleListBasedExecutionStrategySelection;
+
+  @Option(
       name = "spawn_strategy",
       defaultValue = "",
       converter = CommaSeparatedNonEmptyOptionListConverter.class,
@@ -73,7 +85,7 @@
   @Option(
       name = "genrule_strategy",
       defaultValue = "",
-      converter = CommaSeparatedOptionListConverter.class,
+      converter = CommaSeparatedNonEmptyOptionListConverter.class,
       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
       effectTags = {OptionEffectTag.UNKNOWN},
       help =
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ProxySpawnActionContext.java b/src/main/java/com/google/devtools/build/lib/exec/ProxySpawnActionContext.java
index 7464e6e..f337b12 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ProxySpawnActionContext.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ProxySpawnActionContext.java
@@ -22,21 +22,27 @@
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.NullEventHandler;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /** Proxy that looks up the right SpawnActionContext for a spawn during {@link #exec}. */
 public final class ProxySpawnActionContext implements SpawnActionContext {
 
   private final SpawnActionContextMaps spawnActionContextMaps;
+  private final boolean listBasedExecutionStrategySelection;
 
   /**
    * Creates a new {@link ProxySpawnActionContext}.
    *
    * @param spawnActionContextMaps The {@link SpawnActionContextMaps} to use to decide which {@link
    *     SpawnActionContext} should execute a given {@link Spawn} during {@link #exec}.
+   * @param listBasedExecutionStrategySelection
    */
-  public ProxySpawnActionContext(SpawnActionContextMaps spawnActionContextMaps) {
+  public ProxySpawnActionContext(
+      SpawnActionContextMaps spawnActionContextMaps, boolean listBasedExecutionStrategySelection) {
     this.spawnActionContextMaps = spawnActionContextMaps;
+    this.listBasedExecutionStrategySelection = listBasedExecutionStrategySelection;
   }
 
   @Override
@@ -44,8 +50,9 @@
       throws ExecException, InterruptedException {
     List<SpawnActionContext> strategies = resolve(spawn, actionExecutionContext.getEventHandler());
 
-    // For now, we only support executing with the first strategy in the list. Later versions of
-    // this code will add some smartness to pick the best out of the list.
+    // Because the strategies are ordered by preference, we can execute the spawn with the best
+    // possible one by simply filtering out the ones that can't execute it and then picking the
+    // first one from the remaining strategies in the list.
     return strategies.get(0).exec(spawn, actionExecutionContext);
   }
 
@@ -54,8 +61,9 @@
       throws ExecException, InterruptedException {
     List<SpawnActionContext> strategies = resolve(spawn, actionExecutionContext.getEventHandler());
 
-    // For now, we only support executing with the first strategy in the list. Later versions of
-    // this code will add some smartness to pick the best out of the list.
+    // Because the strategies are ordered by preference, we can execute the spawn with the best
+    // possible one by simply filtering out the ones that can't execute it and then picking the
+    // first one from the remaining strategies in the list.
     return strategies.get(0).execMaybeAsync(spawn, actionExecutionContext);
   }
 
@@ -72,6 +80,13 @@
     List<SpawnActionContext> strategies =
         spawnActionContextMaps.getSpawnActionContexts(spawn, eventHandler);
 
+    if (listBasedExecutionStrategySelection) {
+      strategies =
+          strategies.stream()
+              .filter(spawnActionContext -> spawnActionContext.canExec(spawn))
+              .collect(Collectors.toList());
+    }
+
     if (strategies.isEmpty()) {
       throw new UserExecException(
           String.format(
@@ -82,4 +97,10 @@
 
     return strategies;
   }
+
+  @Override
+  public boolean canExec(Spawn spawn) {
+    return spawnActionContextMaps.getSpawnActionContexts(spawn, NullEventHandler.INSTANCE).stream()
+        .anyMatch(spawnActionContext -> spawnActionContext.canExec(spawn));
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/RegexFilterAssignmentConverter.java b/src/main/java/com/google/devtools/build/lib/exec/RegexFilterAssignmentConverter.java
index 96610bc..2824104 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/RegexFilterAssignmentConverter.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/RegexFilterAssignmentConverter.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.exec;
 
 import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.util.RegexFilter;
 import com.google.devtools.build.lib.util.RegexFilter.RegexFilterConverter;
@@ -36,6 +37,16 @@
           "Must be in the form of a 'regex=value[,value]' assignment");
     }
     List<String> value = splitter.splitToList(input.substring(pos + 1));
+    if (value.contains("")) {
+      // If the list contains exactly the empty string, it means an empty value was passed and we
+      // should instead return an empty list.
+      if (value.size() == 1) {
+        value = ImmutableList.of();
+      } else {
+        throw new OptionsParsingException(
+            "Values must not contain empty strings or leading / trailing commas");
+      }
+    }
     RegexFilter filter = new RegexFilterConverter().convert(input.substring(0, pos));
     return Maps.immutableEntry(filter, value);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnActionContextMaps.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnActionContextMaps.java
index 2d7918c..b0fd288 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnActionContextMaps.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnActionContextMaps.java
@@ -73,14 +73,17 @@
   private final ImmutableSortedMap<String, List<SpawnActionContext>> mnemonicToSpawnStrategiesMap;
   private final ImmutableList<ActionContext> strategies;
   private final ImmutableList<RegexFilterSpawnActionContext> spawnStrategyRegexList;
+  private final boolean listBasedExecutionStrategySelection;
 
   private SpawnActionContextMaps(
       ImmutableSortedMap<String, List<SpawnActionContext>> mnemonicToSpawnStrategiesMap,
       ImmutableList<ActionContext> strategies,
-      ImmutableList<RegexFilterSpawnActionContext> spawnStrategyRegexList) {
+      ImmutableList<RegexFilterSpawnActionContext> spawnStrategyRegexList,
+      boolean listBasedExecutionStrategySelection) {
     this.mnemonicToSpawnStrategiesMap = mnemonicToSpawnStrategiesMap;
     this.strategies = strategies;
     this.spawnStrategyRegexList = spawnStrategyRegexList;
+    this.listBasedExecutionStrategySelection = listBasedExecutionStrategySelection;
   }
 
   /**
@@ -121,7 +124,9 @@
       }
       contextMap.put(context.getClass(), context);
     }
-    contextMap.put(SpawnActionContext.class, new ProxySpawnActionContext(this));
+    contextMap.put(
+        SpawnActionContext.class,
+        new ProxySpawnActionContext(this, listBasedExecutionStrategySelection));
     return ImmutableMap.copyOf(contextMap);
   }
 
@@ -185,7 +190,8 @@
     return new SpawnActionContextMaps(
         ImmutableSortedMap.copyOf(spawnStrategyMnemonicMap, String.CASE_INSENSITIVE_ORDER),
         ImmutableList.copyOf(strategies),
-        ImmutableList.of());
+        ImmutableList.of(),
+        false);
   }
 
   /** A stored entry for a {@link RegexFilter} to {@code strategy} mapping. */
@@ -238,7 +244,9 @@
 
     /** Builds a {@link SpawnActionContextMaps} instance. */
     public SpawnActionContextMaps build(
-        ImmutableList<ActionContextProvider> actionContextProviders, String testStrategyValue)
+        ImmutableList<ActionContextProvider> actionContextProviders,
+        String testStrategyValue,
+        boolean listBasedExecutionStrategySelection)
         throws ExecutorInitException {
       StrategyConverter strategyConverter = new StrategyConverter(actionContextProviders);
 
@@ -250,7 +258,19 @@
 
       for (String mnemonic : strategyByMnemonicMap.keySet()) {
         ImmutableList.Builder<SpawnActionContext> contexts = ImmutableList.builder();
-        for (String strategy : strategyByMnemonicMap.get(mnemonic)) {
+        Set<String> strategiesForMnemonic = strategyByMnemonicMap.get(mnemonic);
+        if (strategiesForMnemonic.size() > 1 && !listBasedExecutionStrategySelection) {
+          String flagName =
+              mnemonic.isEmpty() ? "--spawn_strategy=" : ("--strategy=" + mnemonic + "=");
+          throw new ExecutorInitException(
+              "Flag "
+                  + flagName
+                  + Joiner.on(',').join(strategiesForMnemonic)
+                  + " contains a list of strategies to use, but "
+                  + "--incompatible_list_based_execution_strategy_selection was not enabled.",
+              ExitCode.COMMAND_LINE_ERROR);
+        }
+        for (String strategy : strategiesForMnemonic) {
           SpawnActionContext context =
               strategyConverter.getStrategy(SpawnActionContext.class, strategy);
           if (context == null) {
@@ -284,7 +304,18 @@
 
       for (RegexFilterStrategy entry : strategyByRegexpBuilder.build()) {
         ImmutableList.Builder<SpawnActionContext> contexts = ImmutableList.builder();
-        for (String strategy : entry.strategy()) {
+        List<String> strategiesForRegex = entry.strategy();
+        if (strategiesForRegex.size() > 1 && !listBasedExecutionStrategySelection) {
+          throw new ExecutorInitException(
+              "Flag --strategy_regexp="
+                  + entry.regexFilter().toString()
+                  + "="
+                  + Joiner.on(',').join(strategiesForRegex)
+                  + " contains a list of strategies to use, but "
+                  + "--incompatible_list_based_execution_strategy_selection was not enabled.",
+              ExitCode.COMMAND_LINE_ERROR);
+        }
+        for (String strategy : strategiesForRegex) {
           SpawnActionContext context =
               strategyConverter.getStrategy(SpawnActionContext.class, strategy);
           if (context == null) {
@@ -310,7 +341,10 @@
       strategies.add(context);
 
       return new SpawnActionContextMaps(
-          spawnStrategyMap.build(), strategies.build(), spawnStrategyRegexList.build());
+          spawnStrategyMap.build(),
+          strategies.build(),
+          spawnStrategyRegexList.build(),
+          listBasedExecutionStrategySelection);
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
index 0d70563..131915e 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnRunner.java
@@ -232,6 +232,9 @@
   SpawnResult exec(Spawn spawn, SpawnExecutionContext context)
       throws InterruptedException, IOException, ExecException;
 
+  /** Returns whether this SpawnRunner supports executing the given Spawn. */
+  boolean canExec(Spawn spawn);
+
   /* Name of the SpawnRunner. */
   String getName();
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
index 80770b2..04f975d 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
@@ -154,6 +154,11 @@
     }
   }
 
+  @Override
+  public boolean canExec(Spawn spawn) {
+    return true;
+  }
+
   protected Path createActionTemp(Path execRoot) throws IOException {
     return execRoot.getRelative(
         java.nio.file.Files.createTempDirectory(