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 788dc3a85956e0..391aae39f8f00a 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
@@ -20,6 +20,8 @@
import com.google.devtools.build.lib.analysis.config.BuildConfiguration.LabelListConverter;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.util.OptionsUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
@@ -39,6 +41,13 @@ public class PlatformOptions extends FragmentOptions {
public static final Label LEGACY_DEFAULT_TARGET_PLATFORM =
Label.parseAbsoluteUnchecked("@bazel_tools//platforms:target_platform");
+ /**
+ * Main workspace-relative location to use when the user does not explicitly set {@code
+ * --platform_mappings}.
+ */
+ public static final PathFragment DEFAULT_PLATFORM_MAPPINGS =
+ PathFragment.create("platform_mappings");
+
@Option(
name = "host_platform",
oldName = "experimental_host_platform",
@@ -169,6 +178,23 @@ public class PlatformOptions extends FragmentOptions {
+ " java_runtime.")
public boolean useToolchainResolutionForJavaRules;
+ @Option(
+ name = "platform_mappings",
+ converter = OptionsUtils.EmptyToNullRelativePathFragmentConverter.class,
+ defaultValue = "",
+ documentationCategory = OptionDocumentationCategory.TOOLCHAIN,
+ effectTags = {
+ OptionEffectTag.AFFECTS_OUTPUTS,
+ OptionEffectTag.CHANGES_INPUTS,
+ OptionEffectTag.LOADING_AND_ANALYSIS
+ },
+ help =
+ "The location of a mapping file that describes which platform to use if none is set or "
+ + "which flags to set when a platform already exists. Must be relative to the main "
+ + "workspace root. Defaults to 'platform_mappings' (a file directly under the "
+ + "workspace root).")
+ public PathFragment platformMappings;
+
@Override
public PlatformOptions getHost() {
PlatformOptions host = (PlatformOptions) getDefault();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java
new file mode 100644
index 00000000000000..33694f43b976ce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunction.java
@@ -0,0 +1,309 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.FileValue;
+import com.google.devtools.build.lib.actions.MissingInputFileException;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Root;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+/**
+ * Function that reads the contents of a mapping file specified in {@code --platform_mappings} and
+ * parses them for use in a {@link PlatformMappingValue}.
+ *
+ *
Note that this class only parses the mapping-file specific format, parsing (and validation) of
+ * flags contained therein is left to the invocation of {@link
+ * PlatformMappingValue#map(BuildConfigurationValue.Key, BuildOptions)}.
+ */
+public class PlatformMappingFunction implements SkyFunction {
+
+ private final BlazeDirectories blazeDirectories;
+
+ public PlatformMappingFunction(BlazeDirectories blazeDirectories) {
+ this.blazeDirectories = blazeDirectories;
+ }
+
+ @Nullable
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws PlatformMappingException, InterruptedException {
+ PlatformMappingValue.Key platformMappingKey = (PlatformMappingValue.Key) skyKey.argument();
+ PathFragment workspaceRelativeMappingPath =
+ platformMappingKey.getWorkspaceRelativeMappingPath();
+
+ Root workspaceRoot = Root.fromPath(blazeDirectories.getWorkspace());
+ RootedPath rootedMappingPath =
+ RootedPath.toRootedPath(workspaceRoot, workspaceRelativeMappingPath);
+ FileValue fileValue = (FileValue) env.getValue(FileValue.key(rootedMappingPath));
+ if (fileValue == null) {
+ return null;
+ }
+
+ if (!fileValue.exists()) {
+ if (!platformMappingKey.wasExplicitlySetByUser()) {
+ // If no flag was passed and the default mapping file does not exist treat this as if the
+ // mapping file was empty rather than an error.
+ return PlatformMappingValue.EMPTY;
+ }
+ throw new PlatformMappingException(
+ new MissingInputFileException(
+ String.format(
+ "--platform_mappings was set to '%s' but no such file exists relative to the "
+ + "top-level workspace, '%s'",
+ workspaceRelativeMappingPath, workspaceRoot),
+ Location.BUILTIN),
+ SkyFunctionException.Transience.PERSISTENT);
+ }
+ if (fileValue.isDirectory()) {
+ throw new PlatformMappingException(
+ new MissingInputFileException(
+ String.format(
+ "--platform_mappings was set to '%s' relative to the top-level workspace '%s' but"
+ + "that path refers to a directory, not a file",
+ workspaceRelativeMappingPath, workspaceRoot),
+ Location.BUILTIN),
+ SkyFunctionException.Transience.PERSISTENT);
+ }
+
+ Iterable lines;
+ try {
+ lines =
+ FileSystemUtils.readLines(fileValue.realRootedPath().asPath(), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new PlatformMappingException(e, SkyFunctionException.Transience.PERSISTENT);
+ }
+
+ return new Parser(lines.iterator()).parse().toPlatformMappingValue();
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ @VisibleForTesting
+ static class PlatformMappingException extends SkyFunctionException {
+
+ public PlatformMappingException(Exception cause, Transience transience) {
+ super(cause, transience);
+ }
+ }
+
+ @VisibleForTesting
+ static class Parser {
+
+ private final Iterator lines;
+
+ /**
+ * Using an optional to represent the next line with contents, {@link Optional#empty()} if we
+ * reached end of file.
+ *
+ * Stores the current non-comment, non-empty, non-whitespace line. Don't access the field
+ * directly, it can either be "used up" by calling {@link #consume()} or retrieved without
+ * moving on by calling {@link #peek()}.
+ */
+ private Optional line;
+
+ Parser(Iterator lines) {
+ this.lines = lines;
+ }
+
+ Mappings parse() throws PlatformMappingException {
+ goToNextContentLine();
+
+ if (!line.isPresent()) {
+ return new Mappings(ImmutableMap.of(), ImmutableMap.of());
+ }
+
+ Map> platformsToFlags = ImmutableMap.of();
+ Map, Label> flagsToPlatforms = ImmutableMap.of();
+
+ if (!peek().equalsIgnoreCase("platforms:") && !peek().equalsIgnoreCase("flags:")) {
+ throwParsingException("Expected 'platforms:' or 'flags:' but got " + peek());
+ }
+
+ if (peek().equalsIgnoreCase("platforms:")) {
+ consume();
+ platformsToFlags = platformsToFlags();
+ }
+
+ if (line.isPresent()) {
+ if (!peek().equalsIgnoreCase("flags:")) {
+ throwParsingException("Expected 'flags:' but got " + peek());
+ }
+ consume();
+ flagsToPlatforms = flagsToPlatforms();
+ }
+
+ if (line.isPresent()) {
+ throwParsingException("Expected end of file but got " + peek());
+ }
+ return new Mappings(platformsToFlags, flagsToPlatforms);
+ }
+
+ private Map> platformsToFlags() throws PlatformMappingException {
+ ImmutableMap.Builder> platformsToFlags = ImmutableMap.builder();
+ while (line.isPresent() && !peek().equalsIgnoreCase("flags:")) {
+ Label platform = platform();
+ Collection flags = flags();
+ platformsToFlags.put(platform, flags);
+ }
+
+ return platformsToFlags.build();
+ }
+
+ private Label platform() throws PlatformMappingException {
+ if (!line.isPresent()) {
+ throwParsingException("Expected platform label but got end of file");
+ }
+ String label = consume();
+
+ Label platform;
+ try {
+ ImmutableMap emptyRepositoryMapping = ImmutableMap.of();
+ // It is ok for us to use an empty repository mapping in this instance because all platform
+ // labels used in the mapping file should be relative to the root repository. Repository
+ // mappings however only apply within a repository imported by the root repository.
+ platform = Label.parseAbsolute(label, emptyRepositoryMapping);
+ } catch (LabelSyntaxException e) {
+ throw new PlatformMappingException(
+ new PlatformMappingParsingException("Expected platform label but got " + label, e),
+ SkyFunctionException.Transience.PERSISTENT);
+ }
+ return platform;
+ }
+
+ private Collection flags() throws PlatformMappingException {
+ ImmutableSet.Builder flags = ImmutableSet.builder();
+ // Note: Short form flags are not supported.
+ while (lineIsFlag()) {
+ flags.add(consume());
+ }
+ ImmutableSet parsedFlags = flags.build();
+ if (parsedFlags.isEmpty()) {
+ if (!line.isPresent()) {
+ throwParsingException("Expected a flag but got end of file");
+ }
+ throwParsingException(
+ "Expected a standard format flag (starting with --) but got " + peek());
+ }
+
+ return parsedFlags;
+ }
+
+ private Map, Label> flagsToPlatforms() throws PlatformMappingException {
+ ImmutableMap.Builder, Label> flagsToPlatforms = ImmutableMap.builder();
+ while (lineIsFlag()) {
+ Collection flags = flags();
+ Label platform = platform();
+ flagsToPlatforms.put(flags, platform);
+ }
+ return flagsToPlatforms.build();
+ }
+
+ private String consume() {
+ Preconditions.checkState(
+ line.isPresent(), "Must make sure that a line exists before consuming.");
+ String value = line.get();
+ goToNextContentLine();
+ return value;
+ }
+
+ private String peek() {
+ Preconditions.checkState(
+ line.isPresent(), "Must make sure that a line exists before peeking.");
+ return line.get();
+ }
+
+ private void throwParsingException(String message) throws PlatformMappingException {
+ throw new PlatformMappingException(
+ new PlatformMappingParsingException(message), SkyFunctionException.Transience.PERSISTENT);
+ }
+
+ private boolean lineIsFlag() {
+ return line.isPresent() && peek().startsWith("--");
+ }
+
+ private void goToNextContentLine() {
+ while (lines.hasNext()) {
+ String line = lines.next().trim();
+ if (line.isEmpty() || line.startsWith("#")) {
+ continue;
+ }
+ this.line = Optional.of(line);
+ return;
+ }
+ line = Optional.empty();
+ }
+ }
+
+ /**
+ * Simple data holder to make testing easier. Only for use internal to this file/tests thereof.
+ */
+ @VisibleForTesting
+ static class Mappings {
+ final Map> platformsToFlags;
+ final Map, Label> flagsToPlatforms;
+
+ Mappings(
+ Map> platformsToFlags,
+ Map, Label> flagsToPlatforms) {
+ this.platformsToFlags = platformsToFlags;
+ this.flagsToPlatforms = flagsToPlatforms;
+ }
+
+ PlatformMappingValue toPlatformMappingValue() {
+ return new PlatformMappingValue(platformsToFlags, flagsToPlatforms);
+ }
+ }
+
+ /**
+ * Inner wrapper exception to work around the fact that {@link SkyFunctionException} cannot carry
+ * a message of its own.
+ */
+ private static class PlatformMappingParsingException extends Exception {
+ public PlatformMappingParsingException(String message) {
+ super(message);
+ }
+
+ public PlatformMappingParsingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
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
index 4ad16cb904a48c..1c90fa6a5f89fa 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PlatformMappingValue.java
@@ -16,6 +16,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Interner;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.PlatformOptions;
@@ -24,7 +25,7 @@
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.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
@@ -35,6 +36,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import javax.annotation.Nullable;
/**
* Stores contents of a platforms/flags mapping file for transforming one {@link
@@ -46,22 +48,54 @@
*/
public final class PlatformMappingValue implements SkyValue {
+ public static final PlatformMappingValue EMPTY =
+ new PlatformMappingValue(ImmutableMap.of(), ImmutableMap.of());
+
/** 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 interner = BlazeInterners.newWeakInterner();
- private final RootedPath path;
+ /**
+ * Creates a new platform mappings key with the given, main workspace-relative path to the
+ * mappings file, typically derived from the {@code --platform_mappings} flag.
+ *
+ * If the path is {@code null} the {@link PlatformOptions#DEFAULT_PLATFORM_MAPPINGS default
+ * path} will be used and the key marked as not having been set by a user.
+ *
+ * @param workspaceRelativeMappingPath main workspace relative path to the mappings file or
+ * {@code null} if the default location should be used
+ */
+ public static Key create(@Nullable PathFragment workspaceRelativeMappingPath) {
+ if (workspaceRelativeMappingPath == null) {
+ return create(PlatformOptions.DEFAULT_PLATFORM_MAPPINGS, false);
+ } else {
+ return create(workspaceRelativeMappingPath, true);
+ }
+ }
+
+ @AutoCodec.Instantiator
+ @AutoCodec.VisibleForSerialization
+ static Key create(PathFragment workspaceRelativeMappingPath, boolean wasExplicitlySetByUser) {
+ return interner.intern(new Key(workspaceRelativeMappingPath, wasExplicitlySetByUser));
+ }
+
+ private final PathFragment path;
+ private final boolean wasExplicitlySetByUser;
- private Key(RootedPath path) {
+ private Key(PathFragment path, boolean wasExplicitlySetByUser) {
this.path = path;
+ this.wasExplicitlySetByUser = wasExplicitlySetByUser;
}
- @AutoCodec.VisibleForSerialization
- @AutoCodec.Instantiator
- static Key create(RootedPath path) {
- return interner.intern(new Key(path));
+ /** Returns the main-workspace relative path this mapping's mapping file can be found at. */
+ public PathFragment getWorkspaceRelativeMappingPath() {
+ return path;
+ }
+
+ public boolean wasExplicitlySetByUser() {
+ return wasExplicitlySetByUser;
}
@Override
@@ -78,17 +112,21 @@ public boolean equals(Object o) {
return false;
}
Key key = (Key) o;
- return Objects.equals(path, key.path);
+ return Objects.equals(path, key.path) && wasExplicitlySetByUser == key.wasExplicitlySetByUser;
}
@Override
public int hashCode() {
- return Objects.hash(path);
+ return Objects.hash(path, wasExplicitlySetByUser);
}
@Override
public String toString() {
- return "PlatformMappingValue.Key{" + "path=" + path + '}';
+ return "PlatformMappingValue.Key{path="
+ + path
+ + ", wasExplicitlySetByUser="
+ + wasExplicitlySetByUser
+ + "}";
}
}
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 8bfa5dfd312b51..21f5ad10d6b238 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
@@ -118,7 +118,7 @@ public final class SkyFunctions {
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 =
+ public 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");
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index 2e84d5e4ce7e85..eb44df0887411c 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -587,6 +587,7 @@ private ImmutableMap skyFunctions(
map.put(SkyFunctions.REPOSITORY_MAPPING, new RepositoryMappingFunction());
map.put(SkyFunctions.RESOLVED_HASH_VALUES, new ResolvedHashesFunction());
map.put(SkyFunctions.RESOLVED_FILE, new ResolvedFileFunction());
+ map.put(SkyFunctions.PLATFORM_MAPPING, new PlatformMappingFunction(directories));
map.putAll(extraSkyFunctions);
return map.build();
}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionParserTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionParserTest.java
new file mode 100644
index 00000000000000..6d7a9fa5225710
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionParserTest.java
@@ -0,0 +1,354 @@
+// 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.testutil.MoreAsserts.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PlatformMappingFunction}. */
+@RunWith(JUnit4.class)
+public class PlatformMappingFunctionParserTest {
+
+ private static final Label PLATFORM1 = Label.parseAbsoluteUnchecked("//platforms:one");
+ private static final Label PLATFORM2 = Label.parseAbsoluteUnchecked("//platforms:two");
+
+ @Test
+ public void testParse() throws Exception {
+ PlatformMappingFunction.Mappings mappings =
+ parse(
+ "platforms:",
+ " //platforms:one",
+ " --cpu=one",
+ " //platforms:two",
+ " --cpu=two",
+ "flags:",
+ " --cpu=one",
+ " //platforms:one",
+ " --cpu=two",
+ " //platforms:two");
+
+ assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1, PLATFORM2);
+ assertThat(mappings.platformsToFlags.get(PLATFORM1)).containsExactly("--cpu=one");
+ assertThat(mappings.platformsToFlags.get(PLATFORM2)).containsExactly("--cpu=two");
+
+ assertThat(mappings.flagsToPlatforms.keySet())
+ .containsExactly(ImmutableSet.of("--cpu=one"), ImmutableSet.of("--cpu=two"));
+ assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one"))).isEqualTo(PLATFORM1);
+ assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=two"))).isEqualTo(PLATFORM2);
+ }
+
+ @Test
+ public void testParseComment() throws Exception {
+ PlatformMappingFunction.Mappings mappings =
+ parse(
+ "# A mapping file!",
+ "platforms:",
+ " # comment1",
+ " //platforms:one",
+ "# comment2",
+ " --cpu=one",
+ " //platforms:two",
+ " --cpu=two",
+ "flags:",
+ "# another comment",
+ " --cpu=one",
+ " //platforms:one",
+ " --cpu=two",
+ " //platforms:two");
+
+ assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1, PLATFORM2);
+ assertThat(mappings.platformsToFlags.get(PLATFORM1)).containsExactly("--cpu=one");
+ assertThat(mappings.platformsToFlags.get(PLATFORM2)).containsExactly("--cpu=two");
+
+ assertThat(mappings.flagsToPlatforms.keySet())
+ .containsExactly(ImmutableSet.of("--cpu=one"), ImmutableSet.of("--cpu=two"));
+ assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one"))).isEqualTo(PLATFORM1);
+ assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=two"))).isEqualTo(PLATFORM2);
+ }
+
+ @Test
+ public void testParseWhitespace() throws Exception {
+ PlatformMappingFunction.Mappings mappings =
+ parse(
+ "",
+ "platforms:",
+ " ",
+ " //platforms:one",
+ "",
+ " --cpu=one",
+ " //platforms:two ",
+ " --cpu=two ",
+ "flags:",
+ " ",
+ "",
+ "--cpu=one",
+ " //platforms:one",
+ " --cpu=two",
+ " //platforms:two");
+
+ assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1, PLATFORM2);
+ assertThat(mappings.platformsToFlags.get(PLATFORM1)).containsExactly("--cpu=one");
+ assertThat(mappings.platformsToFlags.get(PLATFORM2)).containsExactly("--cpu=two");
+
+ assertThat(mappings.flagsToPlatforms.keySet())
+ .containsExactly(ImmutableSet.of("--cpu=one"), ImmutableSet.of("--cpu=two"));
+ assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one"))).isEqualTo(PLATFORM1);
+ assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=two"))).isEqualTo(PLATFORM2);
+ }
+
+ @Test
+ public void testParseMultipleFlagsInPlatform() throws Exception {
+ PlatformMappingFunction.Mappings mappings =
+ parse(
+ "platforms:",
+ " //platforms:one",
+ " --cpu=one",
+ " --compilation_mode=dbg",
+ " //platforms:two",
+ " --cpu=two");
+
+ assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1, PLATFORM2);
+ assertThat(mappings.platformsToFlags.get(PLATFORM1))
+ .containsExactly("--cpu=one", "--compilation_mode=dbg");
+ }
+
+ @Test
+ public void testParseMultipleFlagsInFlags() throws Exception {
+ PlatformMappingFunction.Mappings mappings =
+ parse(
+ "flags:",
+ " --compilation_mode=dbg",
+ " --cpu=one",
+ " //platforms:one",
+ " --cpu=two",
+ " //platforms:two");
+
+ assertThat(mappings.flagsToPlatforms.keySet())
+ .containsExactly(
+ ImmutableSet.of("--cpu=one", "--compilation_mode=dbg"), ImmutableSet.of("--cpu=two"));
+ assertThat(
+ mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one", "--compilation_mode=dbg")))
+ .isEqualTo(PLATFORM1);
+ }
+
+ @Test
+ public void testParseOnlyPlatforms() throws Exception {
+ PlatformMappingFunction.Mappings mappings =
+ parse(
+ "platforms:", // Force line break
+ " //platforms:one", // Force line break
+ " --cpu=one" // Force line break
+ );
+
+ assertThat(mappings.platformsToFlags.keySet()).containsExactly(PLATFORM1);
+ assertThat(mappings.platformsToFlags.get(PLATFORM1)).containsExactly("--cpu=one");
+ assertThat(mappings.flagsToPlatforms).isEmpty();
+ }
+
+ @Test
+ public void testParseOnlyFlags() throws Exception {
+ PlatformMappingFunction.Mappings mappings =
+ parse(
+ "flags:", // Force line break
+ " --cpu=one", // Force line break
+ " //platforms:one" // Force line break
+ );
+
+ assertThat(mappings.flagsToPlatforms.keySet()).containsExactly(ImmutableSet.of("--cpu=one"));
+ assertThat(mappings.flagsToPlatforms.get(ImmutableSet.of("--cpu=one"))).isEqualTo(PLATFORM1);
+ assertThat(mappings.platformsToFlags).isEmpty();
+ }
+
+ @Test
+ public void testParseEmpty() throws Exception {
+ PlatformMappingFunction.Mappings mappings = parse();
+
+ assertThat(mappings.flagsToPlatforms).isEmpty();
+ assertThat(mappings.platformsToFlags).isEmpty();
+ }
+
+ @Test
+ public void testParseEmptySections() throws Exception {
+ PlatformMappingFunction.Mappings mappings = parse("platforms:", "flags:");
+
+ assertThat(mappings.flagsToPlatforms).isEmpty();
+ assertThat(mappings.platformsToFlags).isEmpty();
+ }
+
+ @Test
+ public void testParseCommentOnly() throws Exception {
+ PlatformMappingFunction.Mappings mappings = parse("#No mappings");
+
+ assertThat(mappings.flagsToPlatforms).isEmpty();
+ assertThat(mappings.platformsToFlags).isEmpty();
+ }
+
+ @Test
+ public void testParseExtraPlatformInFlags() throws Exception {
+ PlatformMappingFunction.PlatformMappingException exception =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "flags:", // Force line break
+ " --cpu=one", // Force line break
+ " //platforms:one", // Force line break
+ " //platforms:two" // Force line break
+ ));
+
+ assertThat(exception).hasMessageThat().contains("//platforms:two");
+ }
+
+ @Test
+ public void testParsePlatformWithoutFlags() throws Exception {
+ PlatformMappingFunction.PlatformMappingException exception =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "platforms:", // Force line break
+ " //platforms:one" // Force line break
+ ));
+
+ assertThat(exception).hasMessageThat().contains("end of file");
+ }
+
+ @Test
+ public void testParseFlagsWithoutPlatform() throws Exception {
+ PlatformMappingFunction.PlatformMappingException exception =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "flags:", // Force line break
+ " --cpu=one" // Force line break
+ ));
+
+ assertThat(exception).hasMessageThat().contains("end of file");
+ }
+
+ @Test
+ public void testParseCommentEndOfFile() throws Exception {
+ PlatformMappingFunction.Mappings mappings =
+ parse(
+ "platforms:", // Force line break
+ " //platforms:one", // Force line break
+ " --cpu=one", // Force line break
+ "# No more mappings" // Force line break
+ );
+
+ assertThat(mappings.platformsToFlags).isNotEmpty();
+ }
+
+ @Test
+ public void testParseUnknownSection() throws Exception {
+ PlatformMappingFunction.PlatformMappingException exception =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "platform:", // Force line break
+ " //platforms:one", // Force line break
+ " --cpu=one" // Force line break
+ ));
+
+ assertThat(exception).hasMessageThat().contains("platform:");
+
+ PlatformMappingFunction.PlatformMappingException exception2 =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "platforms:",
+ " //platforms:one",
+ " --cpu=one",
+ "flag:",
+ " --cpu=one",
+ " //platforms:one"));
+
+ assertThat(exception).hasMessageThat().contains("platform");
+ }
+
+ @Test
+ public void testParsePlatformsInvalidPlatformLabel() throws Exception {
+ PlatformMappingFunction.PlatformMappingException exception =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "platforms:", // Force line break
+ " @@@", // Force line break
+ " --cpu=one"));
+
+ assertThat(exception).hasMessageThat().contains("@@@");
+ assertThat(exception).hasCauseThat().hasCauseThat().isInstanceOf(LabelSyntaxException.class);
+ }
+
+ @Test
+ public void testParseFlagsInvalidPlatformLabel() throws Exception {
+ PlatformMappingFunction.PlatformMappingException exception =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "flags:", // Force line break
+ " --cpu=one", // Force line break
+ " @@@"));
+
+ assertThat(exception).hasMessageThat().contains("@@@");
+ assertThat(exception).hasCauseThat().hasCauseThat().isInstanceOf(LabelSyntaxException.class);
+ }
+
+ @Test
+ public void testParsePlatformsInvalidFlag() throws Exception {
+ PlatformMappingFunction.PlatformMappingException exception =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "platforms:", // Force line break
+ " //platforms:one", // Force line break
+ " -cpu=one"));
+
+ assertThat(exception).hasMessageThat().contains("-cpu");
+ }
+
+ @Test
+ public void testParseFlagsInvalidFlag() throws Exception {
+ PlatformMappingFunction.PlatformMappingException exception =
+ assertThrows(
+ PlatformMappingFunction.PlatformMappingException.class,
+ () ->
+ parse(
+ "flags:", // Force line break
+ " -cpu=one", // Force line break
+ " //platforms:one"));
+
+ assertThat(exception).hasMessageThat().contains("-cpu");
+ }
+
+ private PlatformMappingFunction.Mappings parse(String... lines)
+ throws PlatformMappingFunction.PlatformMappingException {
+ return new PlatformMappingFunction.Parser(ImmutableList.copyOf(lines).iterator()).parse();
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionTest.java
new file mode 100644
index 00000000000000..194fa03a2a0345
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingFunctionTest.java
@@ -0,0 +1,159 @@
+// 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.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.MissingInputFileException;
+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.FragmentOptions;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
+import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.common.options.OptionsParsingException;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link PlatformMappingFunction}.
+ *
+ * Note that all parsing tests are located in {@link PlatformMappingFunctionParserTest}.
+ */
+@RunWith(JUnit4.class)
+public class PlatformMappingFunctionTest extends BuildViewTestCase {
+
+ // 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> PLATFORM_FRAGMENT_CLASS =
+ ImmutableSet.of(PlatformConfiguration.class);
+
+ private static final ImmutableList>
+ BUILD_CONFIG_PLATFORM_OPTIONS =
+ ImmutableList.of(BuildConfiguration.Options.class, PlatformOptions.class);
+
+ private static final Label PLATFORM1 = Label.parseAbsoluteUnchecked("//platforms:one");
+
+ 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 testMappingFileDoesNotExist() throws Exception {
+ MissingInputFileException exception =
+ assertThrows(
+ MissingInputFileException.class,
+ () ->
+ executeFunction(
+ PlatformMappingValue.Key.create(PathFragment.create("random_location"))));
+ assertThat(exception).hasMessageThat().contains("random_location");
+ }
+
+ @Test
+ public void testMappingFileDoesNotExistDefaultLocation() throws Exception {
+ PlatformMappingValue platformMappingValue =
+ executeFunction(PlatformMappingValue.Key.create(null));
+
+ BuildConfigurationValue.Key key =
+ BuildConfigurationValue.key(PLATFORM_FRAGMENT_CLASS, EMPTY_DIFF);
+
+ BuildConfigurationValue.Key mapped =
+ platformMappingValue.map(key, DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+ assertThat(toMappedOptions(mapped).get(PlatformOptions.class).platforms)
+ .containsExactly(LEGACY_DEFAULT_TARGET_PLATFORM);
+ }
+
+ @Test
+ public void testMappingFileIsDirectory() throws Exception {
+ scratch.dir("somedir");
+
+ MissingInputFileException exception =
+ assertThrows(
+ MissingInputFileException.class,
+ () -> executeFunction(PlatformMappingValue.Key.create(PathFragment.create("somedir"))));
+ assertThat(exception).hasMessageThat().contains("somedir");
+ }
+
+ @Test
+ public void testMappingFileIsRead() throws Exception {
+ scratch.file(
+ "my_mapping_file",
+ "platforms:", // Force line break
+ " //platforms:one", // Force line break
+ " --cpu=one");
+
+ PlatformMappingValue platformMappingValue =
+ executeFunction(PlatformMappingValue.Key.create(PathFragment.create("my_mapping_file")));
+
+ BuildOptions modifiedOptions = DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.clone();
+ modifiedOptions.get(PlatformOptions.class).platforms = ImmutableList.of(PLATFORM1);
+
+ BuildConfigurationValue.Key mapped =
+ platformMappingValue.map(
+ keyForOptions(modifiedOptions), DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS);
+
+ assertThat(toMappedOptions(mapped).get(BuildConfiguration.Options.class).cpu).isEqualTo("one");
+ }
+
+ private PlatformMappingValue executeFunction(PlatformMappingValue.Key key) throws Exception {
+ SkyframeExecutor skyframeExecutor = getSkyframeExecutor();
+ skyframeExecutor.injectExtraPrecomputedValues(
+ ImmutableList.of(
+ PrecomputedValue.injected(
+ RepositoryDelegatorFunction.RESOLVED_FILE_INSTEAD_OF_WORKSPACE,
+ Optional.absent())));
+ EvaluationResult result =
+ SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, /*keepGoing=*/ false, reporter);
+ if (result.hasError()) {
+ throw result.getError(key).getException();
+ }
+ return result.get(key);
+ }
+
+ 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);
+ }
+}
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
index af504702078190..88cdf7be4093e1 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PlatformMappingValueTest.java
@@ -27,6 +27,7 @@
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.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.OptionsParsingException;
import java.util.Collection;
import java.util.Set;
@@ -203,6 +204,23 @@ public void testMapNoMappingIfPlatformIsSetAndNoPlatformMapping() throws Excepti
assertThat(keyForOptions(modifiedOptions)).isEqualTo(mapped);
}
+ @Test
+ public void testDefaultKey() {
+ PlatformMappingValue.Key key = PlatformMappingValue.Key.create(null);
+
+ assertThat(key.getWorkspaceRelativeMappingPath())
+ .isEqualTo(PlatformOptions.DEFAULT_PLATFORM_MAPPINGS);
+ assertThat(key.wasExplicitlySetByUser()).isFalse();
+ }
+
+ @Test
+ public void testCustomKey() {
+ PlatformMappingValue.Key key = PlatformMappingValue.Key.create(PathFragment.create("my/path"));
+
+ assertThat(key.getWorkspaceRelativeMappingPath()).isEqualTo(PathFragment.create("my/path"));
+ assertThat(key.wasExplicitlySetByUser()).isTrue();
+ }
+
private BuildOptions toMappedOptions(BuildConfigurationValue.Key mapped) {
return DEFAULT_BUILD_CONFIG_PLATFORM_OPTIONS.applyDiff(mapped.getOptionsDiff());
}