Allow passing a comma-separated list of strategies to the strategy flags.

Any strategy except the first one in a list is currently ignored. This
feature will be used in follow up changes to implement a new way of
specifying execution strategies and to improve the configurability of
execution fallback (e.g. remote to local, sandboxed to non-sandboxed).

RELNOTES: None.
PiperOrigin-RevId: 234849292
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelStrategyModule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelStrategyModule.java
index b5236a2..6b3712c 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelStrategyModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelStrategyModule.java
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.RegexFilter;
 import com.google.devtools.common.options.OptionsBase;
+import java.util.List;
 import java.util.Map;
 
 /** Module which registers the strategy options for Bazel. */
@@ -47,27 +48,26 @@
     ExecutionOptions options = env.getOptions().getOptions(ExecutionOptions.class);
 
     // Default strategies for certain mnemonics - they can be overridden by --strategy= flags.
-    builder.addStrategyByMnemonic("Javac", "worker");
-    builder.addStrategyByMnemonic("Closure", "worker");
-    builder.addStrategyByMnemonic("DexBuilder", "worker");
+    builder.addStrategyByMnemonic("Javac", ImmutableList.of("worker"));
+    builder.addStrategyByMnemonic("Closure", ImmutableList.of("worker"));
+    builder.addStrategyByMnemonic("DexBuilder", ImmutableList.of("worker"));
 
     // Allow genrule_strategy to also be overridden by --strategy= flags.
     builder.addStrategyByMnemonic("Genrule", options.genruleStrategy);
 
-    for (Map.Entry<String, String> strategy : options.strategy) {
-      String strategyName = strategy.getValue();
-      // TODO(philwo) - remove this when the standalone / local mess is cleaned up.
-      // Some flag expansions use "local" as the strategy name, but the strategy is now called
-      // "standalone", so we'll translate it here.
-      if (strategyName.equals("local")) {
-        strategyName = "standalone";
-      }
-      builder.addStrategyByMnemonic(strategy.getKey(), strategyName);
+    for (Map.Entry<String, List<String>> strategy : options.strategy) {
+      builder.addStrategyByMnemonic(strategy.getKey(), strategy.getValue());
     }
 
-    builder.addStrategyByMnemonic("", options.spawnStrategy);
+    // The --spawn_strategy= flag is a bit special: If it's set to the empty string, we actually
+    // have to pass a literal empty string to the builder to trigger the "use the strategy that was
+    // registered last" behavior. Otherwise we would have no default strategy at all and Bazel would
+    // crash.
+    List<String> spawnStrategies =
+        options.spawnStrategy.isEmpty() ? ImmutableList.of("") : options.spawnStrategy;
+    builder.addStrategyByMnemonic("", spawnStrategies);
 
-    for (Map.Entry<RegexFilter, String> entry : options.strategyByRegexp) {
+    for (Map.Entry<RegexFilter, List<String>> entry : options.strategyByRegexp) {
       builder.addStrategyByRegexp(entry.getKey(), entry.getValue());
     }
 
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 c8444fd..88e36d5 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
@@ -24,7 +24,9 @@
 import com.google.devtools.build.lib.util.ResourceConverter;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.common.options.BoolOrEnumConverter;
-import com.google.devtools.common.options.Converters.AssignmentConverter;
+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;
@@ -58,6 +60,7 @@
   @Option(
       name = "spawn_strategy",
       defaultValue = "",
+      converter = CommaSeparatedNonEmptyOptionListConverter.class,
       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
       effectTags = {OptionEffectTag.UNKNOWN},
       help =
@@ -65,23 +68,24 @@
               + "'standalone' means run all of them locally without any kind of sandboxing. "
               + "'sandboxed' means to run them in a sandboxed environment with limited privileges "
               + "(details depend on platform support).")
-  public String spawnStrategy;
+  public List<String> spawnStrategy;
 
   @Option(
       name = "genrule_strategy",
       defaultValue = "",
+      converter = CommaSeparatedOptionListConverter.class,
       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
       effectTags = {OptionEffectTag.UNKNOWN},
       help =
           "Specify how to execute genrules. This flag will be phased out. Instead, use "
               + "--spawn_strategy=<value> to control all actions or --strategy=Genrule=<value> "
               + "to control genrules only.")
-  public String genruleStrategy;
+  public List<String> genruleStrategy;
 
   @Option(
       name = "strategy",
       allowMultiple = true,
-      converter = AssignmentConverter.class,
+      converter = AssignmentToListOfValuesConverter.class,
       defaultValue = "",
       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
       effectTags = {OptionEffectTag.UNKNOWN},
@@ -89,7 +93,7 @@
           "Specify how to distribute compilation of other spawn actions. "
               + "Example: 'Javac=local' means to spawn Java compilation locally. "
               + "'JavaIjar=sandboxed' means to spawn Java Ijar actions in a sandbox. ")
-  public List<Map.Entry<String, String>> strategy;
+  public List<Map.Entry<String, List<String>>> strategy;
 
   @Option(
       name = "strategy_regexp",
@@ -109,7 +113,7 @@
               + "Example: --strategy_regexp='Compiling.*/bar=local "
               + " --strategy_regexp=Compiling=sandboxed will run 'Compiling //foo/bar/baz' with "
               + "the 'local' strategy, but reversing the order would run it with 'sandboxed'. ")
-  public List<Map.Entry<RegexFilter, String>> strategyByRegexp;
+  public List<Map.Entry<RegexFilter, List<String>>> strategyByRegexp;
 
   @Option(
       name = "materialize_param_files",
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ExecutorBuilder.java b/src/main/java/com/google/devtools/build/lib/exec/ExecutorBuilder.java
index 73339de..e478088 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ExecutorBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ExecutorBuilder.java
@@ -63,21 +63,13 @@
   }
 
   /**
-   * Sets the strategy name for a given action mnemonic.
+   * Sets the strategy names for a given action mnemonic.
    *
-   * <p>The calling module can either decide for itself which implementation is needed and make the
-   * value associated with this key a constant or defer that decision to the user, for example, by
-   * providing a command line option and setting the value in the map based on that.
-   *
-   * <p>Setting the strategy to the empty string "" redirects it to the value for the empty
-   * mnemonic.
-   *
-   * <p>Example: a module requires {@code SpawnActionContext} to do its job, and it creates actions
-   * with the mnemonic <code>C++</code>. The the module can call
-   * <code>addStrategyByMnemonic("C++", strategy)</code>.
+   * <p>During execution, the {@link ProxySpawnActionContext} will ask each strategy whether it can
+   * execute a given Spawn. The first strategy in the list that says so will get the job.
    */
-  public ExecutorBuilder addStrategyByMnemonic(String mnemonic, String strategy) {
-    spawnActionContextMapsBuilder.strategyByMnemonicMap().put(mnemonic, strategy);
+  public ExecutorBuilder addStrategyByMnemonic(String mnemonic, List<String> strategies) {
+    spawnActionContextMapsBuilder.strategyByMnemonicMap().replaceValues(mnemonic, strategies);
     return this;
   }
 
@@ -106,7 +98,7 @@
    * Similar to {@link #addStrategyByMnemonic}, but allows specifying a regex for the set of
    * matching mnemonics, instead of an exact string.
    */
-  public ExecutorBuilder addStrategyByRegexp(RegexFilter regexFilter, String strategy) {
+  public ExecutorBuilder addStrategyByRegexp(RegexFilter regexFilter, List<String> strategy) {
     spawnActionContextMapsBuilder.addStrategyByRegexp(regexFilter, strategy);
     return this;
   }
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 143e605..7464e6e 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
@@ -20,6 +20,7 @@
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnActionContext;
 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 java.util.List;
 
@@ -41,26 +42,44 @@
   @Override
   public List<SpawnResult> exec(Spawn spawn, ActionExecutionContext actionExecutionContext)
       throws ExecException, InterruptedException {
-    return resolve(spawn, actionExecutionContext.getEventHandler())
-        .exec(spawn, actionExecutionContext);
+    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.
+    return strategies.get(0).exec(spawn, actionExecutionContext);
   }
 
   @Override
   public FutureSpawn execMaybeAsync(Spawn spawn, ActionExecutionContext actionExecutionContext)
       throws ExecException, InterruptedException {
-    return resolve(spawn, actionExecutionContext.getEventHandler())
-        .execMaybeAsync(spawn, actionExecutionContext);
+    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.
+    return strategies.get(0).execMaybeAsync(spawn, actionExecutionContext);
   }
 
   /**
-   * Returns the {@link SpawnActionContext} that should be used to execute the given spawn.
+   * Returns the list of {@link SpawnActionContext}s that should be used to execute the given spawn.
    *
    * @param spawn The spawn for which the correct {@link SpawnActionContext} should be determined.
    * @param eventHandler An event handler that can be used to print messages while resolving the
    *     correct {@link SpawnActionContext} for the given spawn.
    */
   @VisibleForTesting
-  public SpawnActionContext resolve(Spawn spawn, EventHandler eventHandler) {
-    return spawnActionContextMaps.getSpawnActionContext(spawn, eventHandler);
+  public List<SpawnActionContext> resolve(Spawn spawn, EventHandler eventHandler)
+      throws UserExecException {
+    List<SpawnActionContext> strategies =
+        spawnActionContextMaps.getSpawnActionContexts(spawn, eventHandler);
+
+    if (strategies.isEmpty()) {
+      throw new UserExecException(
+          String.format(
+              "No usable spawn strategy found for spawn with mnemonic %s. Are your --spawn_strategy"
+                  + " or --strategy flags too strict?",
+              spawn.getMnemonic()));
+    }
+
+    return strategies;
   }
 }
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 65b60b4..96610bc 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
@@ -13,29 +13,35 @@
 // limitations under the License.
 package com.google.devtools.build.lib.exec;
 
+import com.google.common.base.Splitter;
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.util.RegexFilter;
 import com.google.devtools.build.lib.util.RegexFilter.RegexFilterConverter;
 import com.google.devtools.common.options.Converter;
 import com.google.devtools.common.options.OptionsParsingException;
+import java.util.List;
 import java.util.Map;
 
 /** A converter for options of the form RegexFilter=String. */
-public class RegexFilterAssignmentConverter implements Converter<Map.Entry<RegexFilter, String>> {
+public class RegexFilterAssignmentConverter
+    implements Converter<Map.Entry<RegexFilter, List<String>>> {
+
+  private final Splitter splitter = Splitter.on(',');
 
   @Override
-  public Map.Entry<RegexFilter, String> convert(String input) throws OptionsParsingException {
+  public Map.Entry<RegexFilter, List<String>> convert(String input) throws OptionsParsingException {
     int pos = input.indexOf('=');
     if (pos <= 0) {
-      throw new OptionsParsingException("Must be in the form of a 'regex=value' assignment");
+      throw new OptionsParsingException(
+          "Must be in the form of a 'regex=value[,value]' assignment");
     }
-    String value = input.substring(pos + 1);
+    List<String> value = splitter.splitToList(input.substring(pos + 1));
     RegexFilter filter = new RegexFilterConverter().convert(input.substring(0, pos));
     return Maps.immutableEntry(filter, value);
   }
 
   @Override
   public String getTypeDescription() {
-    return "a '<RegexFilter>=value' assignment";
+    return "a '<RegexFilter>=value[,value]' assignment";
   }
 }
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 21d1215..2d7918c 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
@@ -24,7 +24,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSortedMap;
-import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Table;
 import com.google.devtools.build.lib.actions.ActionContext;
@@ -45,8 +45,10 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.stream.Collectors;
 
 /**
  * Container for looking up the {@link ActionContext} to use for a given action.
@@ -65,51 +67,52 @@
   public abstract static class RegexFilterSpawnActionContext {
     public abstract RegexFilter regexFilter();
 
-    public abstract SpawnActionContext spawnActionContext();
+    public abstract ImmutableList<SpawnActionContext> spawnActionContext();
   }
 
-  private final ImmutableSortedMap<String, SpawnActionContext> spawnStrategyMnemonicMap;
+  private final ImmutableSortedMap<String, List<SpawnActionContext>> mnemonicToSpawnStrategiesMap;
   private final ImmutableList<ActionContext> strategies;
   private final ImmutableList<RegexFilterSpawnActionContext> spawnStrategyRegexList;
 
   private SpawnActionContextMaps(
-      ImmutableSortedMap<String, SpawnActionContext> spawnStrategyMnemonicMap,
+      ImmutableSortedMap<String, List<SpawnActionContext>> mnemonicToSpawnStrategiesMap,
       ImmutableList<ActionContext> strategies,
       ImmutableList<RegexFilterSpawnActionContext> spawnStrategyRegexList) {
-    this.spawnStrategyMnemonicMap = spawnStrategyMnemonicMap;
+    this.mnemonicToSpawnStrategiesMap = mnemonicToSpawnStrategiesMap;
     this.strategies = strategies;
     this.spawnStrategyRegexList = spawnStrategyRegexList;
   }
 
   /**
-   * Returns the appropriate {@link ActionContext} to execute the given {@link Spawn} with.
+   * Returns a list of appropriate {@link ActionContext}s to execute the given {@link Spawn} with.
    *
    * <p>If the reason for selecting the context is worth mentioning to the user, logs a message
    * using the given {@link Reporter}.
    */
-  public SpawnActionContext getSpawnActionContext(Spawn spawn, EventHandler reporter) {
+  List<SpawnActionContext> getSpawnActionContexts(Spawn spawn, EventHandler reporter) {
     Preconditions.checkNotNull(spawn);
     if (!spawnStrategyRegexList.isEmpty() && spawn.getResourceOwner() != null) {
       String description = spawn.getResourceOwner().getProgressMessage();
       if (description != null) {
         for (RegexFilterSpawnActionContext entry : spawnStrategyRegexList) {
           if (entry.regexFilter().isIncluded(description) && entry.spawnActionContext() != null) {
-            reporter.handle(Event.progress(
-                description + " with context " + entry.spawnActionContext().toString()));
+            reporter.handle(
+                Event.progress(
+                    description + " with context " + entry.spawnActionContext().toString()));
             return entry.spawnActionContext();
           }
         }
       }
     }
-    SpawnActionContext context = spawnStrategyMnemonicMap.get(spawn.getMnemonic());
+    List<SpawnActionContext> context = mnemonicToSpawnStrategiesMap.get(spawn.getMnemonic());
     if (context != null) {
       return context;
     }
-    return Preconditions.checkNotNull(spawnStrategyMnemonicMap.get(""));
+    return Preconditions.checkNotNull(mnemonicToSpawnStrategiesMap.get(""));
   }
 
   /** Returns a map from action context class to its instantiated context object. */
-  public ImmutableMap<Class<? extends ActionContext>, ActionContext> contextMap() {
+  ImmutableMap<Class<? extends ActionContext>, ActionContext> contextMap() {
     Map<Class<? extends ActionContext>, ActionContext> contextMap = new HashMap<>();
     for (ActionContext context : strategies) {
       ExecutionStrategy annotation = context.getClass().getAnnotation(ExecutionStrategy.class);
@@ -123,13 +126,13 @@
   }
 
   /** Returns a list of all referenced {@link ActionContext} instances. */
-  public ImmutableList<ActionContext> allContexts() {
+  ImmutableList<ActionContext> allContexts() {
     // We need to keep only the last occurrences of the entries in contextImplementations
     // (so we respect insertion order but also instantiate them only once).
     LinkedHashSet<ActionContext> allContexts = new LinkedHashSet<>();
     allContexts.addAll(strategies);
-    allContexts.addAll(spawnStrategyMnemonicMap.values());
-    spawnStrategyRegexList.forEach(x -> allContexts.add(x.spawnActionContext()));
+    mnemonicToSpawnStrategiesMap.values().forEach(allContexts::addAll);
+    spawnStrategyRegexList.forEach(x -> allContexts.addAll(x.spawnActionContext()));
     return ImmutableList.copyOf(allContexts);
   }
 
@@ -138,13 +141,17 @@
    *
    * <p>Prints out debug information about the mappings.
    */
-  public void debugPrintSpawnActionContextMaps(Reporter reporter) {
-    for (Map.Entry<String, SpawnActionContext> entry : spawnStrategyMnemonicMap.entrySet()) {
+  void debugPrintSpawnActionContextMaps(Reporter reporter) {
+    for (Entry<String, List<SpawnActionContext>> entry : mnemonicToSpawnStrategiesMap.entrySet()) {
+      List<String> strategyNames =
+          entry.getValue().stream()
+              .map(spawnActionContext -> spawnActionContext.getClass().getSimpleName())
+              .collect(Collectors.toList());
       reporter.handle(
           Event.info(
               String.format(
-                  "SpawnActionContextMap: \"%s\" = %s",
-                  entry.getKey(), entry.getValue().getClass().getSimpleName())));
+                  "SpawnActionContextMap: \"%s\" = [%s]",
+                  entry.getKey(), Joiner.on(", ").join(strategyNames))));
     }
 
     ImmutableMap<Class<? extends ActionContext>, ActionContext> contextMap = contextMap();
@@ -173,11 +180,10 @@
 
   @VisibleForTesting
   public static SpawnActionContextMaps createStub(
-      List<ActionContext> strategies, Map<String, SpawnActionContext> spawnStrategyMnemonicMap) {
+      List<ActionContext> strategies,
+      Map<String, List<SpawnActionContext>> spawnStrategyMnemonicMap) {
     return new SpawnActionContextMaps(
-        ImmutableSortedMap.<String, SpawnActionContext>orderedBy(String.CASE_INSENSITIVE_ORDER)
-            .putAll(spawnStrategyMnemonicMap)
-            .build(),
+        ImmutableSortedMap.copyOf(spawnStrategyMnemonicMap, String.CASE_INSENSITIVE_ORDER),
         ImmutableList.copyOf(strategies),
         ImmutableList.of());
   }
@@ -187,13 +193,13 @@
   public abstract static class RegexFilterStrategy {
     public abstract RegexFilter regexFilter();
 
-    public abstract String strategy();
+    public abstract ImmutableList<String> strategy();
   }
 
   /** Builder for {@code SpawnActionContextMaps}. */
   public static final class Builder {
-    private ImmutableListMultimap.Builder<String, String> strategyByMnemonicMapBuilder =
-        ImmutableListMultimap.builder();
+    private final LinkedHashMultimap<String, String> strategyByMnemonicMap =
+        LinkedHashMultimap.create();
     private ImmutableListMultimap.Builder<Class<? extends ActionContext>, String>
         strategyByContextMapBuilder = ImmutableListMultimap.builder();
 
@@ -206,17 +212,17 @@
      * <p>If a spawn action is executed whose mnemonic maps to the empty string or is not present in
      * the map at all, the choice of the implementation is left to Blaze.
      *
-     * <p>Matching on mnemonics is done case-insensitively so it is recommended that any
-     * module makes sure that no two strategies refer to the same mnemonic.  If they do, Blaze
-     * will pick the last one added.
+     * <p>Matching on mnemonics is done case-insensitively so it is recommended that any module
+     * makes sure that no two strategies refer to the same mnemonic. If they do, Blaze will pick the
+     * last one added.
      */
-    public ImmutableMultimap.Builder<String, String> strategyByMnemonicMap() {
-      return strategyByMnemonicMapBuilder;
+    public LinkedHashMultimap<String, String> strategyByMnemonicMap() {
+      return strategyByMnemonicMap;
     }
 
     /**
-     * Returns a builder modules can use to associate {@link ActionContext} classes with
-     * strategy names.
+     * Returns a builder modules can use to associate {@link ActionContext} classes with strategy
+     * names.
      */
     public ImmutableMultimap.Builder<Class<? extends ActionContext>, String>
         strategyByContextMap() {
@@ -224,9 +230,10 @@
     }
 
     /** Adds a mapping from the given {@link RegexFilter} to a {@code strategy}. */
-    public void addStrategyByRegexp(RegexFilter regexFilter, String strategy) {
+    public void addStrategyByRegexp(RegexFilter regexFilter, List<String> strategy) {
       strategyByRegexpBuilder.add(
-          new AutoValue_SpawnActionContextMaps_RegexFilterStrategy(regexFilter, strategy));
+          new AutoValue_SpawnActionContextMaps_RegexFilterStrategy(
+              regexFilter, ImmutableList.copyOf(strategy)));
     }
 
     /** Builds a {@link SpawnActionContextMaps} instance. */
@@ -235,32 +242,27 @@
         throws ExecutorInitException {
       StrategyConverter strategyConverter = new StrategyConverter(actionContextProviders);
 
-      ImmutableSortedMap.Builder<String, SpawnActionContext> spawnStrategyMap =
+      ImmutableSortedMap.Builder<String, List<SpawnActionContext>> spawnStrategyMap =
           ImmutableSortedMap.orderedBy(String.CASE_INSENSITIVE_ORDER);
       ImmutableList.Builder<ActionContext> strategies = ImmutableList.builder();
       ImmutableList.Builder<RegexFilterSpawnActionContext> spawnStrategyRegexList =
           ImmutableList.builder();
 
-      ImmutableListMultimap<String, String> multimap = strategyByMnemonicMapBuilder.build();
-      for (String mnemonic : multimap.keySet()) {
-        String strategy = Iterables.getLast(multimap.get(mnemonic));
-        if (strategy.isEmpty() && !mnemonic.isEmpty()) {
-          // If strategy is set to the empty value, and we're not looking at the empty mnemonic,
-          // then don't create a per-mnemonic entry for this case at all. At runtime, we'll fall
-          // back to the setting for the empty mnemonic, which may or may not be the setting for
-          // the "" strategy (the default strategy), but may be overridden to a specific one.
-          continue;
+      for (String mnemonic : strategyByMnemonicMap.keySet()) {
+        ImmutableList.Builder<SpawnActionContext> contexts = ImmutableList.builder();
+        for (String strategy : strategyByMnemonicMap.get(mnemonic)) {
+          SpawnActionContext context =
+              strategyConverter.getStrategy(SpawnActionContext.class, strategy);
+          if (context == null) {
+            String strategyOrNull = Strings.emptyToNull(strategy);
+            throw makeExceptionForInvalidStrategyValue(
+                strategy,
+                Joiner.on(' ').skipNulls().join(strategyOrNull, "spawn"),
+                strategyConverter.getValidValues(SpawnActionContext.class));
+          }
+          contexts.add(context);
         }
-        SpawnActionContext context =
-            strategyConverter.getStrategy(SpawnActionContext.class, strategy);
-        if (context == null) {
-          String strategyOrNull = Strings.emptyToNull(strategy);
-          throw makeExceptionForInvalidStrategyValue(
-              strategy,
-              Joiner.on(' ').skipNulls().join(strategyOrNull, "spawn"),
-              strategyConverter.getValidValues(SpawnActionContext.class));
-        }
-        spawnStrategyMap.put(mnemonic, context);
+        spawnStrategyMap.put(mnemonic, contexts.build());
       }
 
       Set<ActionContext> seenContext = new HashSet<>();
@@ -281,19 +283,22 @@
       }
 
       for (RegexFilterStrategy entry : strategyByRegexpBuilder.build()) {
-        SpawnActionContext context =
-            strategyConverter.getStrategy(SpawnActionContext.class, entry.strategy());
-        if (context == null) {
-          String strategy = Strings.emptyToNull(entry.strategy().toString());
-          throw makeExceptionForInvalidStrategyValue(
-              entry.regexFilter().toString(),
-              Joiner.on(' ').skipNulls().join(strategy, "spawn"),
-              strategyConverter.getValidValues(SpawnActionContext.class));
+        ImmutableList.Builder<SpawnActionContext> contexts = ImmutableList.builder();
+        for (String strategy : entry.strategy()) {
+          SpawnActionContext context =
+              strategyConverter.getStrategy(SpawnActionContext.class, strategy);
+          if (context == null) {
+            strategy = Strings.emptyToNull(strategy);
+            throw makeExceptionForInvalidStrategyValue(
+                entry.regexFilter().toString(),
+                Joiner.on(' ').skipNulls().join(strategy, "spawn"),
+                strategyConverter.getValidValues(SpawnActionContext.class));
+          }
+          contexts.add(context);
         }
-
         spawnStrategyRegexList.add(
             new AutoValue_SpawnActionContextMaps_RegexFilterSpawnActionContext(
-                entry.regexFilter(), context));
+                entry.regexFilter(), contexts.build()));
       }
 
       ActionContext context =
@@ -355,5 +360,4 @@
       return marker != null ? marker.name() : context.getSimpleName();
     }
   }
-
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
index 072e821..95ca264 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
@@ -238,7 +238,7 @@
 
       // This makes the "sandboxed" strategy the default Spawn strategy, unless it is
       // overridden by a later BlazeModule.
-      builder.addStrategyByMnemonic("", "sandboxed");
+      builder.addStrategyByMnemonic("", ImmutableList.of("sandboxed"));
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/BUILD b/src/main/java/com/google/devtools/build/lib/standalone/BUILD
index 69cc4a5..c1a479d 100644
--- a/src/main/java/com/google/devtools/build/lib/standalone/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/standalone/BUILD
@@ -26,6 +26,7 @@
         "//src/main/java/com/google/devtools/build/lib/exec/local:options",
         "//src/main/java/com/google/devtools/build/lib/rules/cpp",
         "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//third_party:guava",
         "//third_party:jsr305",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneModule.java b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneModule.java
index 1393c20..1c44b3a 100644
--- a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneModule.java
+++ b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneModule.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.standalone;
 
+import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.actions.ResourceManager;
 import com.google.devtools.build.lib.actions.SpawnActionContext;
 import com.google.devtools.build.lib.analysis.actions.LocalTemplateExpansionStrategy;
@@ -69,7 +70,7 @@
 
     // This makes the "sandboxed" strategy the default Spawn strategy, unless it is overridden by a
     // later BlazeModule.
-    builder.addStrategyByMnemonic("", "standalone");
+    builder.addStrategyByMnemonic("", ImmutableList.of("standalone"));
 
     // This makes the "standalone" strategy available via --spawn_strategy=standalone, but it is not
     // necessarily the default.
diff --git a/src/main/java/com/google/devtools/common/options/Converters.java b/src/main/java/com/google/devtools/common/options/Converters.java
index fb3bbfa..a691bd4 100644
--- a/src/main/java/com/google/devtools/common/options/Converters.java
+++ b/src/main/java/com/google/devtools/common/options/Converters.java
@@ -254,15 +254,24 @@
 
     private final String separatorDescription;
     private final Splitter splitter;
+    private final boolean allowEmptyValues;
 
-    protected SeparatedOptionListConverter(char separator, String separatorDescription) {
+    protected SeparatedOptionListConverter(
+        char separator, String separatorDescription, boolean allowEmptyValues) {
       this.separatorDescription = separatorDescription;
       this.splitter = Splitter.on(separator);
+      this.allowEmptyValues = allowEmptyValues;
     }
 
     @Override
-    public List<String> convert(String input) {
-      return input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input));
+    public List<String> convert(String input) throws OptionsParsingException {
+      List<String> result =
+          input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input));
+      if (!allowEmptyValues && result.contains("")) {
+        throw new OptionsParsingException(
+            "Empty values are not allowed as part of this " + getTypeDescription());
+      }
+      return result;
     }
 
     @Override
@@ -273,13 +282,20 @@
 
   public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter {
     public CommaSeparatedOptionListConverter() {
-      super(',', "comma");
+      super(',', "comma", true);
+    }
+  }
+
+  public static class CommaSeparatedNonEmptyOptionListConverter
+      extends SeparatedOptionListConverter {
+    public CommaSeparatedNonEmptyOptionListConverter() {
+      super(',', "comma", false);
     }
   }
 
   public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter {
     public ColonSeparatedOptionListConverter() {
-      super(':', "colon");
+      super(':', "colon", true);
     }
   }
 
@@ -444,6 +460,44 @@
 
   /**
    * A converter for variable assignments from the parameter list of a blaze command invocation.
+   * Assignments are expected to have the form {@code name=value1[,..,valueN]}, where names and
+   * values are defined to be as permissive as possible.
+   */
+  public static class AssignmentToListOfValuesConverter
+      implements Converter<Map.Entry<String, List<String>>> {
+
+    private final Splitter splitter = Splitter.on(',');
+
+    @Override
+    public Map.Entry<String, List<String>> convert(String input) throws OptionsParsingException {
+      int pos = input.indexOf("=");
+      if (pos <= 0) {
+        throw new OptionsParsingException(
+            "Variable definitions must be in the form of a 'name=value1[,..,valueN]' assignment");
+      }
+      String name = input.substring(0, pos);
+      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(
+              "Variable definitions must not contain empty strings or leading / trailing commas");
+        }
+      }
+      return Maps.immutableEntry(name, value);
+    }
+
+    @Override
+    public String getTypeDescription() {
+      return "a 'name=value1[,..,valueN]' assignment";
+    }
+  }
+
+  /**
+   * A converter for variable assignments from the parameter list of a blaze command invocation.
    * Assignments are expected to have the form "name[=value]", where names and values are defined to
    * be as permissive as possible and value part can be optional (in which case it is considered to
    * be null).