Transformation for build configurations based on a platform/flags mapping.

Introduces a new SkyValue which stores the information obtained from a mapping file (parser yet to be written) and provides logic to transform a build configuration (key) based on that.

Step 3/N towards the platforms mapping functionality for https://github.com/bazelbuild/bazel/issues/6426

RELNOTES: None.
PiperOrigin-RevId: 238298127
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/PlatformConfigurationLoader.java b/src/main/java/com/google/devtools/build/lib/analysis/PlatformConfigurationLoader.java
index ce34005..adad85a 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/PlatformConfigurationLoader.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/PlatformConfigurationLoader.java
@@ -16,13 +16,11 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
 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.ConfigurationFragmentFactory;
 import com.google.devtools.build.lib.analysis.config.FragmentOptions;
 import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
-import com.google.devtools.build.lib.cmdline.Label;
 
 /** A loader that creates {@link PlatformConfiguration} instances based on command-line options. */
 public class PlatformConfigurationLoader implements ConfigurationFragmentFactory {
@@ -35,36 +33,10 @@
   public PlatformConfiguration create(BuildOptions buildOptions)
       throws InvalidConfigurationException {
     PlatformOptions platformOptions = buildOptions.get(PlatformOptions.class);
-
-    // Handle default values for the host and target platform.
-    // TODO(https://github.com/bazelbuild/bazel/issues/6849): After migration, set the defaults
-    // directly.
-    Label hostPlatform;
-    if (platformOptions.hostPlatform != null) {
-      hostPlatform = platformOptions.hostPlatform;
-    } else if (platformOptions.autoConfigureHostPlatform) {
-      // Use the auto-configured host platform.
-      hostPlatform = PlatformOptions.DEFAULT_HOST_PLATFORM;
-    } else {
-      // Use the legacy host platform.
-      hostPlatform = PlatformOptions.LEGACY_DEFAULT_HOST_PLATFORM;
-    }
-
-    Label targetPlatform;
-    if (!platformOptions.platforms.isEmpty()) {
-      targetPlatform = Iterables.getFirst(platformOptions.platforms, null);
-    } else if (platformOptions.autoConfigureHostPlatform) {
-      // Default to the host platform, whatever it is.
-      targetPlatform = hostPlatform;
-    } else {
-      // Use the legacy target platform
-      targetPlatform = PlatformOptions.LEGACY_DEFAULT_TARGET_PLATFORM;
-    }
-
     return new PlatformConfiguration(
-        hostPlatform,
+        platformOptions.computeHostPlatform(),
         ImmutableList.copyOf(platformOptions.extraExecutionPlatforms),
-        targetPlatform,
+        platformOptions.computeTargetPlatform(),
         ImmutableList.copyOf(platformOptions.extraToolchains),
         ImmutableList.copyOf(platformOptions.enabledToolchainTypes));
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/PlatformOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/PlatformOptions.java
index 7681871..788dc3a 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/PlatformOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/PlatformOptions.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.lib.analysis;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration.LabelListConverter;
 import com.google.devtools.build.lib.analysis.config.FragmentOptions;
@@ -101,23 +102,22 @@
   public List<String> extraToolchains;
 
   @Option(
-    name = "toolchain_resolution_override",
-    allowMultiple = true,
-    defaultValue = "",
-    documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
-    effectTags = {
-      OptionEffectTag.AFFECTS_OUTPUTS,
-      OptionEffectTag.CHANGES_INPUTS,
-      OptionEffectTag.LOADING_AND_ANALYSIS
-    },
-    deprecationWarning =
-        "toolchain_resolution_override is now a no-op and will be removed in"
-            + " an upcoming release",
-    help =
-        "Override toolchain resolution for a toolchain type with a specific toolchain. "
-            + "Example: --toolchain_resolution_override=@io_bazel_rules_go//:toolchain="
-            + "@io_bazel_rules_go//:linux-arm64-toolchain"
-  )
+      name = "toolchain_resolution_override",
+      allowMultiple = true,
+      defaultValue = "",
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {
+        OptionEffectTag.AFFECTS_OUTPUTS,
+        OptionEffectTag.CHANGES_INPUTS,
+        OptionEffectTag.LOADING_AND_ANALYSIS
+      },
+      deprecationWarning =
+          "toolchain_resolution_override is now a no-op and will be removed in"
+              + " an upcoming release",
+      help =
+          "Override toolchain resolution for a toolchain type with a specific toolchain. "
+              + "Example: --toolchain_resolution_override=@io_bazel_rules_go//:toolchain="
+              + "@io_bazel_rules_go//:linux-arm64-toolchain")
   public List<String> toolchainResolutionOverrides;
 
   @Option(
@@ -184,4 +184,39 @@
     host.useToolchainResolutionForJavaRules = this.useToolchainResolutionForJavaRules;
     return host;
   }
+
+  /** Returns the intended target platform value based on options defined in this fragment. */
+  public Label computeTargetPlatform() {
+    // Handle default values for the host and target platform.
+    // TODO(https://github.com/bazelbuild/bazel/issues/6849): After migration, set the defaults
+    // directly.
+
+    if (!platforms.isEmpty()) {
+      return Iterables.getFirst(platforms, null);
+    } else if (autoConfigureHostPlatform) {
+      // Default to the host platform, whatever it is.
+      return computeHostPlatform();
+    } else {
+      // Use the legacy target platform
+      return LEGACY_DEFAULT_TARGET_PLATFORM;
+    }
+  }
+
+  /** Returns the intended host platform value based on options defined in this fragment. */
+  public Label computeHostPlatform() {
+    // Handle default values for the host and target platform.
+    // TODO(https://github.com/bazelbuild/bazel/issues/6849): After migration, set the defaults
+    // directly.
+
+    Label hostPlatform;
+    if (this.hostPlatform != null) {
+      return this.hostPlatform;
+    } else if (autoConfigureHostPlatform) {
+      // Use the auto-configured host platform.
+      return DEFAULT_HOST_PLATFORM;
+    } else {
+      // Use the legacy host platform.
+      return LEGACY_DEFAULT_HOST_PLATFORM;
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
index 7775d33..a47b2d6 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
@@ -192,6 +192,11 @@
     return fragmentOptionsMap.values();
   }
 
+  /** Returns the set of fragment classes contained in these options. */
+  public Set<Class<? extends FragmentOptions>> getFragmentClasses() {
+    return fragmentOptionsMap.keySet();
+  }
+
   public ImmutableMap<Label, Object> getStarlarkOptions() {
     return skylarkOptionsMap;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java
new file mode 100644
index 0000000..4ad16cb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java
@@ -0,0 +1,196 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Interner;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.PlatformOptions;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.concurrent.BlazeInterners;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsParsingResult;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Stores contents of a platforms/flags mapping file for transforming one {@link
+ * BuildConfigurationValue.Key} into another.
+ *
+ * <p>See <a href=https://docs.google.com/document/d/1Vg_tPgiZbSrvXcJ403vZVAGlsWhH9BUDrAxMOYnO0Ls>
+ * the design</a> for more details on how the mapping can be defined and the desired logic on how it
+ * is applied to configuration keys.
+ */
+public final class PlatformMappingValue implements SkyValue {
+
+  /** Key for {@link PlatformMappingValue} based on the location of the mapping file. */
+  @ThreadSafety.Immutable
+  @AutoCodec
+  public static final class Key implements SkyKey {
+    private static final Interner<Key> interner = BlazeInterners.newWeakInterner();
+
+    private final RootedPath path;
+
+    private Key(RootedPath path) {
+      this.path = path;
+    }
+
+    @AutoCodec.VisibleForSerialization
+    @AutoCodec.Instantiator
+    static Key create(RootedPath path) {
+      return interner.intern(new Key(path));
+    }
+
+    @Override
+    public SkyFunctionName functionName() {
+      return SkyFunctions.PLATFORM_MAPPING;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      Key key = (Key) o;
+      return Objects.equals(path, key.path);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(path);
+    }
+
+    @Override
+    public String toString() {
+      return "PlatformMappingValue.Key{" + "path=" + path + '}';
+    }
+  }
+
+  private final Map<Label, Collection<String>> platformsToFlags;
+  private final Map<Collection<String>, Label> flagsToPlatforms;
+
+  /**
+   * Creates a new mapping value which will match on the given platforms (if a target platform is
+   * set on the key to be mapped), otherwise on the set of flags.
+   *
+   * @param platformsToFlags mapping from target platform label to the command line style flags that
+   *     should be parsed & modified if that platform is set
+   * @param flagsToPlatforms mapping from a collection of command line style flags to a target
+   *     platform that should be set if the flags match the mapped options
+   */
+  PlatformMappingValue(
+      Map<Label, Collection<String>> platformsToFlags,
+      Map<Collection<String>, Label> flagsToPlatforms) {
+    this.platformsToFlags = platformsToFlags;
+    this.flagsToPlatforms = flagsToPlatforms;
+  }
+
+  /**
+   * Maps one {@link BuildConfigurationValue.Key} to another by way of mappings provided in a file.
+   *
+   * <p>The <a href=https://docs.google.com/document/d/1Vg_tPgiZbSrvXcJ403vZVAGlsWhH9BUDrAxMOYnO0Ls>
+   * full design</a> contains the details for the mapping logic but in short:
+   *
+   * <ol>
+   *   <li>If a target platform is set on the original then mappings from platform to flags will be
+   *       applied.
+   *   <li>If no target platform is set then mappings from flags to platforms will be applied.
+   *   <li>If no matching flags to platforms mapping was found, the default target platform will be
+   *       used.
+   * </ol>
+   *
+   * @param original the key representing the configuration to be mapped
+   * @param defaultBuildOptions build options as set by default in this server
+   * @return the mapped key if any mapping matched the original or else the original
+   * @throws OptionsParsingException if any of the user configured flags cannot be parsed
+   * @throws IllegalArgumentException if the original does not contain a {@link PlatformOptions}
+   *     fragment
+   */
+  public BuildConfigurationValue.Key map(
+      BuildConfigurationValue.Key original, BuildOptions defaultBuildOptions)
+      throws OptionsParsingException {
+    BuildOptions.OptionsDiffForReconstruction originalDiff = original.getOptionsDiff();
+    BuildOptions originalOptions = defaultBuildOptions.applyDiff(originalDiff);
+
+    Preconditions.checkArgument(
+        originalOptions.contains(PlatformOptions.class),
+        "When using platform mappings, all configurations must contain platform options");
+
+    BuildOptions modifiedOptions = null;
+
+    if (!originalOptions.get(PlatformOptions.class).platforms.isEmpty()) {
+      List<Label> platforms = originalOptions.get(PlatformOptions.class).platforms;
+
+      Preconditions.checkArgument(
+          platforms.size() == 1,
+          "Platform mapping only supports a single target platform but found %s",
+          platforms);
+
+      Label targetPlatform = Iterables.getOnlyElement(platforms);
+      if (!platformsToFlags.containsKey(targetPlatform)) {
+        // This can happen if the user has set the platform and any other flags that would normally
+        // be mapped from it on the command line instead of relying on the mapping.
+        return original;
+      }
+
+      OptionsParsingResult parsingResult =
+          parse(platformsToFlags.get(targetPlatform), defaultBuildOptions);
+      modifiedOptions = originalOptions.applyParsingResult(parsingResult);
+    } else {
+      boolean mappingFound = false;
+      for (Map.Entry<Collection<String>, Label> flagsToPlatform : flagsToPlatforms.entrySet()) {
+        if (originalOptions.matches(parse(flagsToPlatform.getKey(), defaultBuildOptions))) {
+          modifiedOptions = originalOptions.clone();
+          modifiedOptions.get(PlatformOptions.class).platforms =
+              ImmutableList.of(flagsToPlatform.getValue());
+          mappingFound = true;
+          break;
+        }
+      }
+
+      if (!mappingFound) {
+        Label targetPlatform = originalOptions.get(PlatformOptions.class).computeTargetPlatform();
+        modifiedOptions = originalOptions.clone();
+        modifiedOptions.get(PlatformOptions.class).platforms = ImmutableList.of(targetPlatform);
+      }
+    }
+
+    return BuildConfigurationValue.key(
+        original.getFragments(),
+        BuildOptions.diffForReconstruction(defaultBuildOptions, modifiedOptions));
+  }
+
+  private OptionsParsingResult parse(Iterable<String> args, BuildOptions defaultBuildOptions)
+      throws OptionsParsingException {
+    OptionsParser parser = OptionsParser.newOptionsParser(defaultBuildOptions.getFragmentClasses());
+    parser.parse(ImmutableList.copyOf(args));
+    // TODO(schmitt): Parse starlark options as well.
+    return parser;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
index ea7724a..8bfa5df 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
@@ -21,9 +21,7 @@
 import com.google.devtools.build.skyframe.SkyFunctionName;
 import com.google.devtools.build.skyframe.SkyKey;
 
-/**
- * Value types in Skyframe.
- */
+/** Value types in Skyframe. */
 public final class SkyFunctions {
   public static final SkyFunctionName PRECOMPUTED =
       SkyFunctionName.createNonHermetic("PRECOMPUTED");
@@ -120,6 +118,8 @@
   public static final SkyFunctionName BUILD_INFO = SkyFunctionName.createHermetic("BUILD_INFO");
   public static final SkyFunctionName WORKSPACE_NAME =
       SkyFunctionName.createHermetic("WORKSPACE_NAME");
+  static final SkyFunctionName PLATFORM_MAPPING =
+      SkyFunctionName.createHermetic("PLATFORM_MAPPING");
   static final SkyFunctionName COVERAGE_REPORT = SkyFunctionName.createHermetic("COVERAGE_REPORT");
   public static final SkyFunctionName REPOSITORY = SkyFunctionName.createHermetic("REPOSITORY");
   public static final SkyFunctionName REPOSITORY_DIRECTORY =
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java
new file mode 100644
index 0000000..af50470
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java
@@ -0,0 +1,224 @@
+// Copyright 2019 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.analysis.PlatformOptions.LEGACY_DEFAULT_TARGET_PLATFORM;
+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.ImmutableSet;
+import com.google.devtools.build.lib.analysis.PlatformConfiguration;
+import com.google.devtools.build.lib.analysis.PlatformOptions;
+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.CompilationMode;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.common.options.OptionsParsingException;
+import java.util.Collection;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PlatformMappingValue}. */
+@RunWith(JUnit4.class)
+public class PlatformMappingValueTest {
+
+  // We don't actually care about the contents of this set other than that it is passed intact
+  // through the mapping logic. The platform fragment in it is purely an example, it could be any
+  // set of fragments.
+  private static final Set<Class<? extends BuildConfiguration.Fragment>> PLATFORM_FRAGMENT_CLASS =
+      ImmutableSet.of(PlatformConfiguration.class);
+
+  private static final ImmutableList<Class<? extends FragmentOptions>>
+      BUILD_CONFIG_PLATFORM_OPTIONS =
+          ImmutableList.of(BuildConfiguration.Options.class, PlatformOptions.class);
+
+  private static final Label PLATFORM1 = Label.parseAbsoluteUnchecked("//platforms:one");
+  private static final Label PLATFORM2 = Label.parseAbsoluteUnchecked("//platforms:two");
+
+  private static final BuildOptions DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS =
+      getDefaultBuildConfigPlatformOptions();
+  private static final BuildOptions.OptionsDiffForReconstruction EMPTY_DIFF =
+      BuildOptions.diffForReconstruction(
+          DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS, DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+  @Test
+  public void testMapNoMappings() throws OptionsParsingException {
+    PlatformMappingValue mappingValue =
+        new PlatformMappingValue(ImmutableMap.of(), ImmutableMap.of());
+
+    BuildConfigurationValue.Key key =
+        BuildConfigurationValue.key(PLATFORM_FRAGMENT_CLASS, EMPTY_DIFF);
+
+    BuildConfigurationValue.Key mapped =
+        mappingValue.map(key, DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+    assertThat(toMappedOptions(mapped).get(PlatformOptions.class).platforms)
+        .containsExactly(LEGACY_DEFAULT_TARGET_PLATFORM);
+  }
+
+  @Test
+  public void testMapPlatformToFlags() throws Exception {
+    ImmutableMap<Label, Collection<String>> platformsToFlags =
+        ImmutableMap.of(PLATFORM1, ImmutableList.of("--cpu=one", "--compilation_mode=dbg"));
+
+    PlatformMappingValue mappingValue =
+        new PlatformMappingValue(platformsToFlags, ImmutableMap.of());
+
+    BuildOptions modifiedOptions = DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.clone();
+    modifiedOptions.get(PlatformOptions.class).platforms = ImmutableList.of(PLATFORM1);
+
+    BuildConfigurationValue.Key mapped =
+        mappingValue.map(keyForOptions(modifiedOptions), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+    assertThat(mapped.getFragments()).isEqualTo(PLATFORM_FRAGMENT_CLASS);
+
+    assertThat(toMappedOptions(mapped).get(BuildConfiguration.Options.class).cpu).isEqualTo("one");
+  }
+
+  @Test
+  public void testMapFlagsToPlatform() throws Exception {
+    ImmutableMap<Collection<String>, Label> flagsToPlatforms =
+        ImmutableMap.of(ImmutableList.of("--cpu=one", "--compilation_mode=dbg"), PLATFORM1);
+
+    PlatformMappingValue mappingValue =
+        new PlatformMappingValue(ImmutableMap.of(), flagsToPlatforms);
+
+    BuildOptions modifiedOptions = DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.clone();
+    modifiedOptions.get(BuildConfiguration.Options.class).cpu = "one";
+    modifiedOptions.get(BuildConfiguration.Options.class).compilationMode = CompilationMode.DBG;
+
+    BuildConfigurationValue.Key mapped =
+        mappingValue.map(keyForOptions(modifiedOptions), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+    assertThat(mapped.getFragments()).isEqualTo(PLATFORM_FRAGMENT_CLASS);
+
+    assertThat(toMappedOptions(mapped).get(PlatformOptions.class).platforms)
+        .containsExactly(PLATFORM1);
+  }
+
+  @Test
+  public void testMapFlagsToPlatformPriority() throws Exception {
+    ImmutableMap<Collection<String>, Label> flagsToPlatforms =
+        ImmutableMap.of(
+            ImmutableList.of("--cpu=foo", "--compilation_mode=dbg"), PLATFORM1,
+            ImmutableList.of("--cpu=foo"), PLATFORM2);
+
+    PlatformMappingValue mappingValue =
+        new PlatformMappingValue(ImmutableMap.of(), flagsToPlatforms);
+
+    BuildOptions modifiedOptions = DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.clone();
+    modifiedOptions.get(BuildConfiguration.Options.class).cpu = "foo";
+
+    BuildConfigurationValue.Key mapped =
+        mappingValue.map(keyForOptions(modifiedOptions), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+    assertThat(toMappedOptions(mapped).get(PlatformOptions.class).platforms)
+        .containsExactly(PLATFORM2);
+  }
+
+  @Test
+  public void testMapFlagsToPlatformNoneMatching() throws Exception {
+    ImmutableMap<Collection<String>, Label> flagsToPlatforms =
+        ImmutableMap.of(ImmutableList.of("--cpu=foo", "--compilation_mode=dbg"), PLATFORM1);
+
+    PlatformMappingValue mappingValue =
+        new PlatformMappingValue(ImmutableMap.of(), flagsToPlatforms);
+
+    BuildOptions modifiedOptions = DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.clone();
+    modifiedOptions.get(BuildConfiguration.Options.class).cpu = "bar";
+
+    BuildConfigurationValue.Key mapped =
+        mappingValue.map(keyForOptions(modifiedOptions), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+    assertThat(toMappedOptions(mapped).get(PlatformOptions.class).platforms)
+        .containsExactly(LEGACY_DEFAULT_TARGET_PLATFORM);
+  }
+
+  @Test
+  public void testMapNoPlatformOptions() throws Exception {
+    ImmutableMap<Collection<String>, Label> flagsToPlatforms =
+        ImmutableMap.of(ImmutableList.of("--cpu=one"), PLATFORM1);
+
+    PlatformMappingValue mappingValue =
+        new PlatformMappingValue(ImmutableMap.of(), flagsToPlatforms);
+
+    BuildOptions options = BuildOptions.of(ImmutableList.of(BuildConfiguration.Options.class));
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> mappingValue.map(keyForOptions(options), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS));
+  }
+
+  @Test
+  public void testMapNoMappingIfPlatformIsSetButNotMatching() throws Exception {
+    ImmutableMap<Label, Collection<String>> platformsToFlags =
+        ImmutableMap.of(PLATFORM1, ImmutableList.of("--cpu=one", "--compilation_mode=dbg"));
+    ImmutableMap<Collection<String>, Label> flagsToPlatforms =
+        ImmutableMap.of(ImmutableList.of("--cpu=one"), PLATFORM1);
+
+    BuildOptions modifiedOptions = DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.clone();
+    modifiedOptions.get(BuildConfiguration.Options.class).cpu = "one";
+    modifiedOptions.get(PlatformOptions.class).platforms = ImmutableList.of(PLATFORM2);
+
+    PlatformMappingValue mappingValue =
+        new PlatformMappingValue(platformsToFlags, flagsToPlatforms);
+
+    BuildConfigurationValue.Key mapped =
+        mappingValue.map(keyForOptions(modifiedOptions), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+    assertThat(keyForOptions(modifiedOptions)).isEqualTo(mapped);
+  }
+
+  @Test
+  public void testMapNoMappingIfPlatformIsSetAndNoPlatformMapping() throws Exception {
+    ImmutableMap<Collection<String>, Label> flagsToPlatforms =
+        ImmutableMap.of(ImmutableList.of("--cpu=one"), PLATFORM1);
+
+    BuildOptions modifiedOptions = DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.clone();
+    modifiedOptions.get(BuildConfiguration.Options.class).cpu = "one";
+    modifiedOptions.get(PlatformOptions.class).platforms = ImmutableList.of(PLATFORM2);
+
+    PlatformMappingValue mappingValue =
+        new PlatformMappingValue(ImmutableMap.of(), flagsToPlatforms);
+
+    BuildConfigurationValue.Key mapped =
+        mappingValue.map(keyForOptions(modifiedOptions), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+    assertThat(keyForOptions(modifiedOptions)).isEqualTo(mapped);
+  }
+
+  private BuildOptions toMappedOptions(BuildConfigurationValue.Key mapped) {
+    return DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.applyDiff(mapped.getOptionsDiff());
+  }
+
+  private static BuildOptions getDefaultBuildConfigPlatformOptions() {
+    try {
+      return BuildOptions.of(BUILD_CONFIG_PLATFORM_OPTIONS);
+    } catch (OptionsParsingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private BuildConfigurationValue.Key keyForOptions(BuildOptions modifiedOptions) {
+    BuildOptions.OptionsDiffForReconstruction diff =
+        BuildOptions.diffForReconstruction(DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS, modifiedOptions);
+
+    return BuildConfigurationValue.key(PLATFORM_FRAGMENT_CLASS, diff);
+  }
+}