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/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 =