| // 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.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.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.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.analysis.config.FragmentOptions; |
| 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.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 |
| * BuildConfigurationKey} 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 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; |
| } |
| |
| 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 ImmutableMap<Label, ImmutableSet<String>> platformsToFlags; |
| private final ImmutableMap<ImmutableSet<String>, Label> flagsToPlatforms; |
| private final ImmutableSet<Class<? extends FragmentOptions>> optionsClasses; |
| private final LoadingCache<ImmutableSet<String>, OptionsParsingResult> parserCache; |
| private final LoadingCache<BuildConfigurationKey, BuildConfigurationKey> mappingCache; |
| |
| /** |
| * 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 |
| */ |
| PlatformMappingValue( |
| ImmutableMap<Label, ImmutableSet<String>> platformsToFlags, |
| ImmutableMap<ImmutableSet<String>, Label> flagsToPlatforms, |
| ImmutableSet<Class<? extends FragmentOptions>> optionsClasses) { |
| this.platformsToFlags = checkNotNull(platformsToFlags); |
| this.flagsToPlatforms = checkNotNull(flagsToPlatforms); |
| this.optionsClasses = checkNotNull(optionsClasses); |
| this.parserCache = |
| Caffeine.newBuilder() |
| .initialCapacity(platformsToFlags.size() + flagsToPlatforms.size()) |
| .build(this::parse); |
| this.mappingCache = Caffeine.newBuilder().weakKeys().build(this::computeMapping); |
| } |
| |
| /** |
| * Maps one {@link BuildConfigurationKey} 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 BuildConfigurationKey map(BuildConfigurationKey original) throws OptionsParsingException { |
| try { |
| return mappingCache.get(original); |
| } catch (CompletionException e) { |
| Throwables.propagateIfPossible(e.getCause(), OptionsParsingException.class); |
| throw e; |
| } |
| } |
| |
| private BuildConfigurationKey computeMapping(BuildConfigurationKey original) |
| throws OptionsParsingException { |
| BuildOptions originalOptions = original.getOptions(); |
| |
| 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 original; |
| } |
| |
| modifiedOptions = |
| originalOptions.applyParsingResult(parseWithCache(platformsToFlags.get(targetPlatform))); |
| } else { |
| boolean mappingFound = false; |
| for (Map.Entry<ImmutableSet<String>, Label> flagsToPlatform : flagsToPlatforms.entrySet()) { |
| if (originalOptions.matches(parseWithCache(flagsToPlatform.getKey()))) { |
| 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 BuildConfigurationKey.withoutPlatformMapping(modifiedOptions); |
| } |
| |
| private OptionsParsingResult parseWithCache(ImmutableSet<String> args) |
| throws OptionsParsingException { |
| try { |
| return parserCache.get(args); |
| } catch (CompletionException e) { |
| Throwables.propagateIfPossible(e.getCause(), OptionsParsingException.class); |
| throw e; |
| } |
| } |
| |
| private OptionsParsingResult parse(Iterable<String> args) throws OptionsParsingException { |
| OptionsParser parser = |
| OptionsParser.builder() |
| .optionsClasses(optionsClasses) |
| // We need the ability to re-map internal options in the mappings file. |
| .ignoreInternalOptions(false) |
| .build(); |
| parser.parse(ImmutableList.copyOf(args)); |
| // TODO(schmitt): Parse starlark options as well. |
| 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(); |
| } |
| } |