blob: 9f37b1858de3ca93cb2fc98eae9768f6e14ce66c [file] [log] [blame]
// 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.cache.Cache;
import com.google.common.cache.CacheBuilder;
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;
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.PathFragment;
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;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
/**
* 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 {
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<Key> interner = BlazeInterners.newWeakInterner();
/**
* 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.
*
* <p>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(PathFragment path, boolean wasExplicitlySetByUser) {
this.path = path;
this.wasExplicitlySetByUser = wasExplicitlySetByUser;
}
/** 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
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) && wasExplicitlySetByUser == key.wasExplicitlySetByUser;
}
@Override
public int hashCode() {
return Objects.hash(path, wasExplicitlySetByUser);
}
@Override
public String toString() {
return "PlatformMappingValue.Key{path="
+ path
+ ", wasExplicitlySetByUser="
+ wasExplicitlySetByUser
+ "}";
}
}
private final Map<Label, Collection<String>> platformsToFlags;
private final Map<Collection<String>, Label> flagsToPlatforms;
private final transient Cache<Collection<String>, OptionsParsingResult> parserCache;
/**
* 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;
this.parserCache =
CacheBuilder.newBuilder()
.initialCapacity(platformsToFlags.size() + flagsToPlatforms.size())
.build();
}
/**
* 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;
}
modifiedOptions =
originalOptions.applyParsingResult(
parseWithCache(platformsToFlags.get(targetPlatform), defaultBuildOptions));
} else {
boolean mappingFound = false;
for (Map.Entry<Collection<String>, Label> flagsToPlatform : flagsToPlatforms.entrySet()) {
if (originalOptions.matches(
parseWithCache(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.keyWithoutPlatformMapping(
original.getFragments(),
BuildOptions.diffForReconstruction(defaultBuildOptions, modifiedOptions));
}
private OptionsParsingResult parseWithCache(
Collection<String> args, BuildOptions defaultBuildOptions) throws OptionsParsingException {
try {
return parserCache.get(args, () -> parse(args, defaultBuildOptions));
} catch (ExecutionException e) {
throw (OptionsParsingException) e.getCause();
}
}
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;
}
}