Changes SplitTransition#split's return type to Map and updates the current native implementations to return CPU-based keys. Lists returned by Starlark transitions are automatically keyed by indexes.

This change relands https://github.com/bazelbuild/bazel/commit/528cb62caa4a742c9918d984d929fe8662a539df with a fix to the duplicate map key issue in the new objc handling code. Other parts of the change are free of the problem because they already deduplicate CPU flag values with ImmutableSortedSet. Added a test case to ObjcSkylarkTest.java and ran TGP with no suspicious results found.

RELNOTES: None
PiperOrigin-RevId: 293656453
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
index d2d4b80..b1b13d0 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
@@ -909,16 +909,16 @@
                         .executionPlatform(getToolchainContext().executionPlatform().label())
                         .build());
     BuildOptions fromOptions = getConfiguration().getOptions();
-    List<BuildOptions> splitOptions = transition.split(fromOptions);
+    Map<String, BuildOptions> splitOptions = transition.split(fromOptions);
     List<ConfiguredTargetAndData> deps = getConfiguredTargetAndTargetDeps(attributeName);
 
-    if (SplitTransition.equals(fromOptions, splitOptions)) {
+    if (SplitTransition.equals(fromOptions, splitOptions.values())) {
       // The split transition is not active. Defer the decision on which CPU to use.
       return ImmutableMap.of(Optional.<String>absent(), deps);
     }
 
     Set<String> cpus = new HashSet<>();
-    for (BuildOptions options : splitOptions) {
+    for (BuildOptions options : splitOptions.values()) {
       // This method should only be called when the split config is enabled on the command line, in
       // which case this cpu can't be null.
       cpus.add(options.get(CoreOptions.class).cpu);
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/StarlarkDefinedConfigTransition.java b/src/main/java/com/google/devtools/build/lib/analysis/config/StarlarkDefinedConfigTransition.java
index e59a539..817e73e 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/StarlarkDefinedConfigTransition.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/StarlarkDefinedConfigTransition.java
@@ -101,7 +101,7 @@
    * @throws EvalException if there is an error evaluating the transition
    * @throws InterruptedException if evaluating the transition is interrupted
    */
-  public abstract ImmutableList<Map<String, Object>> evaluate(
+  public abstract ImmutableMap<String, Map<String, Object>> evaluate(
       Map<String, Object> previousSettings, StructImpl attributeMap)
       throws EvalException, InterruptedException;
 
@@ -134,9 +134,9 @@
     }
 
     @Override
-    public ImmutableList<Map<String, Object>> evaluate(
+    public ImmutableMap<String, Map<String, Object>> evaluate(
         Map<String, Object> previousSettings, StructImpl attributeMapper) {
-      return ImmutableList.of(changedSettings);
+      return ImmutableMap.of("analysis_test", changedSettings);
     }
 
     @Override
@@ -206,7 +206,7 @@
     // TODO(bazel-team): integrate dict-of-dicts return type with ctx.split_attr
     @Override
     @SuppressWarnings("rawtypes")
-    public ImmutableList<Map<String, Object>> evaluate(
+    public ImmutableMap<String, Map<String, Object>> evaluate(
         Map<String, Object> previousSettings, StructImpl attributeMapper)
         throws EvalException, InterruptedException {
       Object result;
@@ -222,7 +222,7 @@
         // settings. Return early for now since better error reporting will happen in
         // {@link FunctionTransitionUtil#validateFunctionOutputsMatchesDeclaredOutputs}
         if (((Dict) result).isEmpty()) {
-          return ImmutableList.of(ImmutableMap.of());
+          return ImmutableMap.of("error", ImmutableMap.of());
         }
         // TODO(bazel-team): integrate keys with ctx.split_attr. Currently ctx.split_attr always
         // keys on cpu value - we should be able to key on the keys returned here.
@@ -231,31 +231,38 @@
           Map<String, Dict> dictOfDict =
               ((Dict<?, ?>) result)
                   .getContents(String.class, Dict.class, "dictionary of options dictionaries");
-          ImmutableList.Builder<Map<String, Object>> builder = ImmutableList.builder();
+          ImmutableMap.Builder<String, Map<String, Object>> builder = ImmutableMap.builder();
           for (Map.Entry<String, Dict> entry : dictOfDict.entrySet()) { // rawtypes error
             Map<String, Object> dict =
                 ((Dict<?, ?>) entry.getValue())
                     .getContents(String.class, Object.class, "an option dictionary");
-            builder.add(dict);
+            builder.put(entry.getKey(), dict);
           }
           return builder.build();
         } catch (EvalException e) {
           // fall through
         }
         try {
-          return ImmutableList.of(
+          // Try if this is a non-split, i.e. 1:1, transition. In which case use the impl function
+          // name as the transition key though it would not be used anywhere.
+          return ImmutableMap.of(
+              impl.getName(),
               ((Dict<?, ?>) result)
                   .getContents(String.class, Object.class, "dictionary of options"));
         } catch (EvalException e) {
           throw new EvalException(impl.getLocation(), e.getMessage());
         }
       } else if (result instanceof Sequence) {
-        ImmutableList.Builder<Map<String, Object>> builder = ImmutableList.builder();
+        ImmutableMap.Builder<String, Map<String, Object>> builder = ImmutableMap.builder();
         try {
+          int i = 0;
           for (Dict<?, ?> toOptions :
               ((Sequence<?>) result)
                   .getContents(Dict.class, "dictionary of options dictionaries")) {
-            builder.add(toOptions.getContents(String.class, Object.class, "dictionary of options"));
+            // TODO(b/146347033): Document this behavior.
+            builder.put(
+                Integer.toString(i++),
+                toOptions.getContents(String.class, Object.class, "dictionary of options"));
           }
         } catch (EvalException e) {
           throw new EvalException(impl.getLocation(), e.getMessage());
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/transitions/SplitTransition.java b/src/main/java/com/google/devtools/build/lib/analysis/config/transitions/SplitTransition.java
index 525269c..625e625 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/transitions/SplitTransition.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/transitions/SplitTransition.java
@@ -15,10 +15,13 @@
 package com.google.devtools.build.lib.analysis.config.transitions;
 
 import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A configuration transition that maps a single input {@link BuildOptions} to possibly multiple
@@ -36,25 +39,26 @@
 @FunctionalInterface
 public interface SplitTransition extends ConfigurationTransition {
   /**
-   * Returns the list of {@code BuildOptions} after splitting, or the original options if this
-   * split is a noop.
+   * Returns the map of {@code BuildOptions} after splitting, or the original options if this split
+   * is a noop. The key values are used as dict keys in ctx.split_attr, so human-readable strings
+   * are recommended.
    *
    * <p>Returning an empty or null list triggers a {@link RuntimeException}.
    */
-  List<BuildOptions> split(BuildOptions buildOptions);
+  Map<String, BuildOptions> split(BuildOptions buildOptions);
 
   /**
    * Returns true iff {@code option} and {@splitOptions} are equal.
    *
    * <p>This can be used to determine if a split is a noop.
    */
-  static boolean equals(BuildOptions options, List<BuildOptions> splitOptions) {
+  static boolean equals(BuildOptions options, Collection<BuildOptions> splitOptions) {
     return splitOptions.size() == 1 && Iterables.getOnlyElement(splitOptions).equals(options);
   }
 
   @Override
   default List<BuildOptions> apply(BuildOptions buildOptions) {
-    List<BuildOptions> splitOptions = split(buildOptions);
+    List<BuildOptions> splitOptions = ImmutableList.copyOf(split(buildOptions).values());
     Verify.verifyNotNull(splitOptions, "Split transition output may not be null");
     Verify.verify(!splitOptions.isEmpty(), "Split transition output may not be empty");
     return splitOptions;
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
index 4441efd..58297d6 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
@@ -36,9 +36,9 @@
 import com.google.devtools.common.options.OptionsParser;
 import com.google.devtools.common.options.OptionsParsingException;
 import java.lang.reflect.Field;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedHashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
@@ -63,7 +63,7 @@
    * @param attrObject the attributes of the rule to which this transition is attached
    * @return the post-transition build options.
    */
-  static List<BuildOptions> applyAndValidate(
+  static Map<String, BuildOptions> applyAndValidate(
       BuildOptions buildOptions,
       StarlarkDefinedConfigTransition starlarkTransition,
       StructImpl attrObject)
@@ -73,16 +73,16 @@
     Map<String, OptionInfo> optionInfoMap = buildOptionInfo(buildOptions);
     Dict<String, Object> settings = buildSettings(buildOptions, optionInfoMap, starlarkTransition);
 
-    ImmutableList.Builder<BuildOptions> splitBuildOptions = ImmutableList.builder();
+    ImmutableMap.Builder<String, BuildOptions> splitBuildOptions = ImmutableMap.builder();
 
-    ImmutableList<Map<String, Object>> transitions =
+    ImmutableMap<String, Map<String, Object>> transitions =
         starlarkTransition.evaluate(settings, attrObject);
-    validateFunctionOutputsMatchesDeclaredOutputs(transitions, starlarkTransition);
+    validateFunctionOutputsMatchesDeclaredOutputs(transitions.values(), starlarkTransition);
 
-    for (Map<String, Object> transition : transitions) {
+    for (Map.Entry<String, Map<String, Object>> entry : transitions.entrySet()) {
       BuildOptions transitionedOptions =
-          applyTransition(buildOptions, transition, optionInfoMap, starlarkTransition);
-      splitBuildOptions.add(transitionedOptions);
+          applyTransition(buildOptions, entry.getValue(), optionInfoMap, starlarkTransition);
+      splitBuildOptions.put(entry.getKey(), transitionedOptions);
     }
     return splitBuildOptions.build();
   }
@@ -93,7 +93,7 @@
    * StarlarkTransition#validate}
    */
   private static void validateFunctionOutputsMatchesDeclaredOutputs(
-      ImmutableList<Map<String, Object>> transitions,
+      Collection<Map<String, Object>> transitions,
       StarlarkDefinedConfigTransition starlarkTransition)
       throws EvalException {
     for (Map<String, Object> transition : transitions) {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkAttributeTransitionProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkAttributeTransitionProvider.java
index e0549aa..2767b44 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkAttributeTransitionProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkAttributeTransitionProvider.java
@@ -19,7 +19,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.StarlarkDefinedConfigTransition;
 import com.google.devtools.build.lib.analysis.config.transitions.SplitTransition;
@@ -36,7 +36,7 @@
 import com.google.devtools.build.lib.syntax.Printer;
 import com.google.devtools.build.lib.syntax.Starlark;
 import java.util.LinkedHashMap;
-import java.util.List;
+import java.util.Map;
 
 /**
  * This class implements {@link TransitionFactory} to provide a starlark-defined transition that
@@ -102,10 +102,9 @@
      *     error was encountered during transition application/validation.
      */
     @Override
-    public final List<BuildOptions> split(BuildOptions buildOptions) {
-      List<BuildOptions> toReturn;
+    public final Map<String, BuildOptions> split(BuildOptions buildOptions) {
       try {
-        toReturn = applyAndValidate(buildOptions, starlarkDefinedConfigTransition, attrObject);
+        return applyAndValidate(buildOptions, starlarkDefinedConfigTransition, attrObject);
       } catch (InterruptedException | EvalException e) {
         starlarkDefinedConfigTransition
             .getEventHandler()
@@ -113,9 +112,8 @@
                 Event.error(
                     starlarkDefinedConfigTransition.getLocationForErrorReporting(),
                     e.getMessage()));
-        return ImmutableList.of(buildOptions.clone());
+        return ImmutableMap.of("error", buildOptions.clone());
       }
-      return toReturn;
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleTransitionProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleTransitionProvider.java
index 84bf516..9024366 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleTransitionProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleTransitionProvider.java
@@ -32,7 +32,7 @@
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Starlark;
 import java.util.LinkedHashMap;
-import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -106,7 +106,7 @@
     // attributes that are then configured by the outputs of these transitions.
     @Override
     public BuildOptions patch(BuildOptions buildOptions) {
-      List<BuildOptions> result;
+      Map<String, BuildOptions> result;
       try {
         result = applyAndValidate(buildOptions, starlarkDefinedConfigTransition, attrObject);
       } catch (EvalException | InterruptedException e) {
@@ -127,7 +127,7 @@
                     "Rule transition only allowed to return a single transitioned configuration."));
         return buildOptions.clone();
       }
-      return Iterables.getOnlyElement(result);
+      return Iterables.getOnlyElement(result.values());
     }
 
     @Override
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
index 2ef4ecf..d022627 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java
@@ -27,6 +27,7 @@
 import static com.google.devtools.build.lib.util.FileTypeSet.NO_FILE;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Lists;
 import com.google.devtools.build.lib.analysis.BaseRuleClasses;
@@ -66,6 +67,7 @@
 import com.google.devtools.build.lib.util.FileType;
 import com.google.devtools.build.lib.util.FileTypeSet;
 import java.util.List;
+import java.util.Map;
 
 /** Rule definitions for Android rules. */
 public final class AndroidRuleClasses {
@@ -235,7 +237,7 @@
     }
 
     @Override
-    public List<BuildOptions> split(BuildOptions buildOptions) {
+    public Map<String, BuildOptions> split(BuildOptions buildOptions) {
 
       AndroidConfiguration.Options androidOptions =
           buildOptions.get(AndroidConfiguration.Options.class);
@@ -247,19 +249,19 @@
         if (androidOptions.cpu.isEmpty()
             || androidCrosstoolTop == null
             || androidCrosstoolTop.equals(cppOptions.crosstoolTop)) {
-          return ImmutableList.of(buildOptions);
+          return ImmutableMap.of(buildOptions.get(CoreOptions.class).cpu, buildOptions);
 
         } else {
 
           BuildOptions splitOptions = buildOptions.clone();
           splitOptions.get(CoreOptions.class).cpu = androidOptions.cpu;
           setCommonAndroidOptions(androidOptions, splitOptions);
-          return ImmutableList.of(splitOptions);
+          return ImmutableMap.of(androidOptions.cpu, splitOptions);
         }
 
       } else {
 
-        ImmutableList.Builder<BuildOptions> result = ImmutableList.builder();
+        ImmutableMap.Builder<String, BuildOptions> result = ImmutableMap.builder();
         for (String cpu : ImmutableSortedSet.copyOf(androidOptions.fatApkCpus)) {
           BuildOptions splitOptions = buildOptions.clone();
           // Disable fat APKs for the child configurations.
@@ -270,7 +272,7 @@
           splitOptions.get(AndroidConfiguration.Options.class).cpu = cpu;
           splitOptions.get(CoreOptions.class).cpu = cpu;
           setCommonAndroidOptions(androidOptions, splitOptions);
-          result.add(splitOptions);
+          result.put(cpu, splitOptions);
         }
         return result.build();
       }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/MultiArchSplitTransitionProvider.java b/src/main/java/com/google/devtools/build/lib/rules/objc/MultiArchSplitTransitionProvider.java
index eda568f..fc0e372 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/MultiArchSplitTransitionProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/MultiArchSplitTransitionProvider.java
@@ -20,7 +20,9 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.devtools.build.lib.analysis.RuleContext;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.CoreOptions;
@@ -40,6 +42,7 @@
 import com.google.devtools.build.lib.syntax.Printer;
 import com.google.devtools.build.lib.syntax.StarlarkValue;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -193,7 +196,7 @@
     }
 
     @Override
-    public final List<BuildOptions> split(BuildOptions buildOptions) {
+    public final Map<String, BuildOptions> split(BuildOptions buildOptions) {
       List<String> cpus;
       DottedVersion actualMinimumOsVersion;
       ConfigurationDistinguisher configurationDistinguisher;
@@ -259,7 +262,9 @@
           throw new IllegalArgumentException("Unsupported platform type " + platformType);
       }
 
-      ImmutableList.Builder<BuildOptions> splitBuildOptions = ImmutableList.builder();
+      // There may be some duplicate flag values.
+      cpus = ImmutableSortedSet.copyOf(cpus).asList();
+      ImmutableMap.Builder<String, BuildOptions> splitBuildOptions = ImmutableMap.builder();
       for (String cpu : cpus) {
         BuildOptions splitOptions = buildOptions.clone();
 
@@ -298,7 +303,7 @@
         }
 
         appleCommandLineOptions.configurationDistinguisher = configurationDistinguisher;
-        splitBuildOptions.add(splitOptions);
+        splitBuildOptions.put(cpu, splitOptions);
       }
       return splitBuildOptions.build();
     }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/CircularDependencyTest.java b/src/test/java/com/google/devtools/build/lib/analysis/CircularDependencyTest.java
index 3548847..89fca0e 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/CircularDependencyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/CircularDependencyTest.java
@@ -22,7 +22,7 @@
 import static com.google.devtools.build.lib.packages.Type.STRING;
 import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.CoreOptions;
@@ -273,7 +273,7 @@
                                 optionsFragment.commandLineBuildVariables.stream()
                                     .filter((pair) -> !pair.getKey().equals(define))
                                     .collect(toImmutableList());
-                            return ImmutableList.of(newOptions);
+                            return ImmutableMap.of("define_cleaner", newOptions);
                           };
                         }
 
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/config/TransitionFactoriesTest.java b/src/test/java/com/google/devtools/build/lib/analysis/config/TransitionFactoriesTest.java
index 564dbcb..542504f 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/config/TransitionFactoriesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/config/TransitionFactoriesTest.java
@@ -15,7 +15,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.analysis.config.transitions.NoTransition;
 import com.google.devtools.build.lib.analysis.config.transitions.NullTransition;
 import com.google.devtools.build.lib.analysis.config.transitions.SplitTransition;
@@ -59,7 +59,7 @@
   public void splitTransition() {
     TransitionFactory<Object> factory =
         TransitionFactories.of(
-            (SplitTransition) buildOptions -> ImmutableList.of(buildOptions.clone()));
+            (SplitTransition) buildOptions -> ImmutableMap.of("test0", buildOptions.clone()));
     assertThat(factory).isNotNull();
     assertThat(factory.isHost()).isFalse();
     assertThat(factory.isSplit()).isTrue();
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionFactoryTest.java b/src/test/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionFactoryTest.java
index 492ae8a..c2ae62c 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionFactoryTest.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.analysis.config.transitions;
 
-import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
@@ -23,6 +23,8 @@
 import com.google.devtools.build.lib.analysis.config.TransitionFactories;
 import com.google.devtools.build.lib.cmdline.Label;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -237,10 +239,13 @@
     }
 
     @Override
-    public List<BuildOptions> split(BuildOptions options) {
-      return flagValues.stream()
-          .map(value -> updateOptions(options, flagLabel, value))
-          .collect(toImmutableList());
+    public Map<String, BuildOptions> split(BuildOptions options) {
+      return IntStream.range(0, flagValues.size())
+          .boxed()
+          .collect(
+              toImmutableMap(
+                  i -> "stub_split" + i,
+                  i -> updateOptions(options, flagLabel, flagValues.get(i))));
     }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionTest.java
index ed0e879..c621133 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/config/transitions/ComposingTransitionTest.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.analysis.config.transitions;
 
-import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
@@ -22,6 +22,8 @@
 import com.google.devtools.build.lib.analysis.config.HostTransition;
 import com.google.devtools.build.lib.cmdline.Label;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -194,10 +196,13 @@
     }
 
     @Override
-    public List<BuildOptions> split(BuildOptions options) {
-      return flagValues.stream()
-          .map(value -> updateOptions(options, flagLabel, value))
-          .collect(toImmutableList());
+    public Map<String, BuildOptions> split(BuildOptions options) {
+      return IntStream.range(0, flagValues.size())
+          .boxed()
+          .collect(
+              toImmutableMap(
+                  i -> "stub_split" + i,
+                  i -> updateOptions(options, flagLabel, flagValues.get(i))));
     }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java b/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java
index b805d85..6e903e0 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/AttributeTest.java
@@ -23,7 +23,6 @@
 import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.HostTransition;
@@ -40,6 +39,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -286,8 +286,8 @@
 
   private static class TestSplitTransition implements SplitTransition {
     @Override
-    public List<BuildOptions> split(BuildOptions buildOptions) {
-      return ImmutableList.of(buildOptions.clone(), buildOptions.clone());
+    public Map<String, BuildOptions> split(BuildOptions buildOptions) {
+      return ImmutableMap.of("test0", buildOptions.clone(), "test1", buildOptions.clone());
     }
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetQueryTest.java b/src/test/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetQueryTest.java
index 7c196eb..ae83400 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetQueryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetQueryTest.java
@@ -21,6 +21,7 @@
 import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
@@ -45,7 +46,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.junit.Test;
@@ -271,12 +272,12 @@
     }
 
     @Override
-    public List<BuildOptions> split(BuildOptions options) {
+    public Map<String, BuildOptions> split(BuildOptions options) {
       BuildOptions result1 = options.clone();
       BuildOptions result2 = options.clone();
       result1.get(TestOptions.class).testArguments = Collections.singletonList(toOption1);
       result2.get(TestOptions.class).testArguments = Collections.singletonList(toOption2);
-      return ImmutableList.of(result1, result2);
+      return ImmutableMap.of("result1", result1, "result2", result2);
     }
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java
index b6691b7..a32aeea 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java
@@ -316,7 +316,7 @@
       throws InterruptedException, OptionsParsingException, InvalidConfigurationException {
     ImmutableList.Builder<BuildConfiguration> splitConfigs = ImmutableList.builder();
 
-    for (BuildOptions splitOptions : splitTransition.split(configuration.getOptions())) {
+    for (BuildOptions splitOptions : splitTransition.split(configuration.getOptions()).values()) {
       splitConfigs.add(getSkyframeExecutor().getConfigurationForTesting(
           reporter, configuration.fragmentClasses(), splitOptions));
     }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcSkylarkTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcSkylarkTest.java
index 3043343..ece89ea 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcSkylarkTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcSkylarkTest.java
@@ -1413,6 +1413,26 @@
   }
 
   @Test
+  public void testMultiArchSplitTransitionWithDuplicateFlagValues() throws Exception {
+    scratch.file("examples/rule/BUILD");
+    writeObjcSplitTransitionTestFiles();
+
+    useConfiguration("--ios_multi_cpus=armv7,arm64,armv7");
+    ConfiguredTarget skylarkTarget = getConfiguredTarget("//examples/apple_skylark:my_target");
+    StructImpl myInfo = getMyInfoFromTarget(skylarkTarget);
+    ObjcProvider armv7Objc =
+        ((SkylarkInfo) myInfo.getValue("ios_armv7")).getValue("objc", ObjcProvider.class);
+    ObjcProvider arm64Objc =
+        ((SkylarkInfo) myInfo.getValue("ios_arm64")).getValue("objc", ObjcProvider.class);
+    assertThat(armv7Objc).isNotNull();
+    assertThat(arm64Objc).isNotNull();
+    assertThat(Iterables.getOnlyElement(armv7Objc.getObjcLibraries()).getExecPathString())
+        .contains("ios_armv7");
+    assertThat(Iterables.getOnlyElement(arm64Objc.getObjcLibraries()).getExecPathString())
+        .contains("ios_arm64");
+  }
+
+  @Test
   public void testNoSplitTransitionUsesCpuFlagValue() throws Exception {
     scratch.file("examples/rule/BUILD");
     writeObjcSplitTransitionTestFiles();
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithTrimmedConfigurationsTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithTrimmedConfigurationsTest.java
index 7e0fa75..94a99e3 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithTrimmedConfigurationsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithTrimmedConfigurationsTest.java
@@ -52,6 +52,7 @@
 import com.google.devtools.build.lib.testutil.TestSpec;
 import com.google.devtools.build.lib.util.FileTypeSet;
 import java.util.List;
+import java.util.Map;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -68,27 +69,27 @@
 
   private static class NoopSplitTransition implements SplitTransition {
     @Override
-    public List<BuildOptions> split(BuildOptions buildOptions) {
-      return ImmutableList.of(buildOptions);
+    public Map<String, BuildOptions> split(BuildOptions buildOptions) {
+      return ImmutableMap.of("noop", buildOptions);
     }
   }
 
   private static class SetsHostCpuSplitTransition implements SplitTransition {
     @Override
-    public List<BuildOptions> split(BuildOptions buildOptions) {
+    public Map<String, BuildOptions> split(BuildOptions buildOptions) {
       BuildOptions result = buildOptions.clone();
       result.get(CoreOptions.class).hostCpu = "SET BY SPLIT";
-      return ImmutableList.of(result);
+      return ImmutableMap.of("hostCpu", result);
     }
   }
 
   private static class SetsCpuSplitTransition implements SplitTransition {
 
     @Override
-    public List<BuildOptions> split(BuildOptions buildOptions) {
+    public Map<String, BuildOptions> split(BuildOptions buildOptions) {
       BuildOptions result = buildOptions.clone();
       result.get(CoreOptions.class).cpu = "SET BY SPLIT";
-      return ImmutableList.of(result);
+      return ImmutableMap.of("cpu", result);
     }
   }
 
@@ -565,14 +566,14 @@
    */
   private static SplitTransition newSplitTransition(final String prefix) {
     return buildOptions -> {
-      ImmutableList.Builder<BuildOptions> result = ImmutableList.builder();
+      ImmutableMap.Builder<String, BuildOptions> result = ImmutableMap.builder();
       for (int index = 1; index <= 2; index++) {
         BuildOptions toOptions = buildOptions.clone();
         TestConfiguration.TestOptions baseOptions =
             toOptions.get(TestConfiguration.TestOptions.class);
         baseOptions.testFilter =
             (baseOptions.testFilter == null ? "" : baseOptions.testFilter) + prefix + index;
-        result.add(toOptions);
+        result.put(prefix + index, toOptions);
       }
       return result.build();
     };