blob: 6e2b173a941a890e1bb5385c23c3b6059ef02f4c [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.config;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
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.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
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.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletionException;
import javax.annotation.Nullable;
/**
* Stores contents of a platforms/flags mapping file for transforming one {@link BuildOptions} 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.
*/
@AutoCodec
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 SkyKeyInterner<Key> interner = SkyKey.newInterner();
/**
* 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;
}
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
+ "}";
}
@Override
public SkyKeyInterner<Key> getSkyKeyInterner() {
return interner;
}
}
private final ImmutableMap<Label, NativeAndStarlarkFlags> platformsToFlags;
private final ImmutableMap<ImmutableSet<String>, Label> flagsToPlatforms;
private final ImmutableSet<Class<? extends FragmentOptions>> optionsClasses;
private final LoadingCache<NativeAndStarlarkFlags, OptionsParsingResult> parserCache;
private final LoadingCache<BuildOptions, BuildOptions> mappingCache;
private final RepositoryMapping mainRepositoryMapping;
/**
* 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 set of command line style flags to a target platform
* that should be set if the flags match the mapped options
* @param optionsClasses default options classes that should be used for options parsing
* @param mainRepositoryMapping the main repo mapping used to parse label-valued options
*/
PlatformMappingValue(
ImmutableMap<Label, NativeAndStarlarkFlags> platformsToFlags,
ImmutableMap<ImmutableSet<String>, Label> flagsToPlatforms,
ImmutableSet<Class<? extends FragmentOptions>> optionsClasses,
RepositoryMapping mainRepositoryMapping) {
this.platformsToFlags = checkNotNull(platformsToFlags);
this.flagsToPlatforms = checkNotNull(flagsToPlatforms);
this.optionsClasses = checkNotNull(optionsClasses);
this.mainRepositoryMapping = checkNotNull(mainRepositoryMapping);
this.parserCache =
Caffeine.newBuilder()
.initialCapacity(platformsToFlags.size() + flagsToPlatforms.size())
.build(flags -> parse(flags, this.mainRepositoryMapping));
this.mappingCache = Caffeine.newBuilder().weakKeys().build(this::computeMapping);
}
/**
* Maps one {@link BuildOptions} 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
* @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 BuildOptions map(BuildOptions original) throws OptionsParsingException {
try {
return mappingCache.get(original);
} catch (CompletionException e) {
Throwables.propagateIfPossible(e.getCause(), OptionsParsingException.class);
throw e;
}
}
private BuildOptions computeMapping(BuildOptions originalOptions) throws OptionsParsingException {
if (originalOptions.hasNoConfig()) {
// The empty configuration (produced by NoConfigTransition) is terminal: it'll never change.
return originalOptions;
}
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;
// Platform mapping only supports a single target platform, others are ignored.
Label targetPlatform = Iterables.getFirst(platforms, null);
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 originalOptions;
}
modifiedOptions =
originalOptions.applyParsingResult(parseWithCache(platformsToFlags.get(targetPlatform)));
} else {
boolean mappingFound = false;
for (Map.Entry<ImmutableSet<String>, Label> flagsToPlatform : flagsToPlatforms.entrySet()) {
if (originalOptions.matches(
parseWithCache(
NativeAndStarlarkFlags.create(flagsToPlatform.getKey(), ImmutableMap.of())))) {
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 modifiedOptions;
}
private OptionsParsingResult parseWithCache(NativeAndStarlarkFlags args)
throws OptionsParsingException {
try {
return parserCache.get(args);
} catch (CompletionException e) {
Throwables.propagateIfPossible(e.getCause(), OptionsParsingException.class);
throw e;
}
}
private OptionsParsingResult parse(NativeAndStarlarkFlags args, RepositoryMapping mainRepoMapping)
throws OptionsParsingException {
OptionsParser parser =
OptionsParser.builder()
.optionsClasses(optionsClasses)
// We need the ability to re-map internal options in the mappings file.
.ignoreInternalOptions(false)
.withConversionContext(mainRepoMapping)
.build();
parser.parse(args.nativeFlags().asList());
parser.setStarlarkOptions(args.starlarkFlags());
return parser;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PlatformMappingValue)) {
return false;
}
PlatformMappingValue that = (PlatformMappingValue) obj;
return this.flagsToPlatforms.equals(that.flagsToPlatforms)
&& this.platformsToFlags.equals(that.platformsToFlags)
&& this.optionsClasses.equals(that.optionsClasses);
}
@Override
public int hashCode() {
return Objects.hash(flagsToPlatforms, platformsToFlags, optionsClasses);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("flagsToPlatforms", flagsToPlatforms)
.add("platformsToFlags", platformsToFlags)
.add("optionsClasses", optionsClasses)
.toString();
}
/**
* Container for storing a {@code platform -> flags} native and Starlark flag settings in separate
* buckets.
*
* <p>This is necessary because native and Starlark flags are parsed with different logic.
*/
@AutoValue
abstract static class NativeAndStarlarkFlags {
abstract ImmutableSet<String> nativeFlags();
abstract ImmutableMap<String, Object> starlarkFlags();
static NativeAndStarlarkFlags create(
ImmutableSet<String> nativeFlags, ImmutableMap<String, Object> starlarkFlags) {
return new AutoValue_PlatformMappingValue_NativeAndStarlarkFlags(nativeFlags, starlarkFlags);
}
}
}