blob: 04e548a82b5f67640135da6b0d4be712c1479f6f [file] [log] [blame]
// Copyright 2018 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.analysis;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.stream.Collectors.joining;
import com.google.auto.value.AutoValue;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.platform.ConstraintValueInfo;
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
import com.google.devtools.build.lib.analysis.platform.ToolchainTypeInfo;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.skyframe.BuildConfigurationValue;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
import com.google.devtools.build.lib.skyframe.ConstraintValueLookupUtil;
import com.google.devtools.build.lib.skyframe.ConstraintValueLookupUtil.InvalidConstraintValueException;
import com.google.devtools.build.lib.skyframe.PlatformLookupUtil;
import com.google.devtools.build.lib.skyframe.PlatformLookupUtil.InvalidPlatformException;
import com.google.devtools.build.lib.skyframe.RegisteredExecutionPlatformsValue;
import com.google.devtools.build.lib.skyframe.RegisteredToolchainsFunction.InvalidToolchainLabelException;
import com.google.devtools.build.lib.skyframe.SingleToolchainResolutionFunction;
import com.google.devtools.build.lib.skyframe.SingleToolchainResolutionFunction.NoToolchainFoundException;
import com.google.devtools.build.lib.skyframe.SingleToolchainResolutionValue;
import com.google.devtools.build.lib.skyframe.ToolchainException;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.ValueOrException2;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* Performs the toolchain resolution process to determine the correct toolchain target dependencies
* for a target being configured, based on the required toolchain types, target platform, and
* available execution platforms.
*/
public class ToolchainResolver {
// Required data.
private final Environment environment;
private final BuildConfigurationValue.Key configurationKey;
// Optional data.
private ImmutableSet<Label> requiredToolchainTypeLabels = ImmutableSet.of();
private ImmutableSet<Label> execConstraintLabels = ImmutableSet.of();
private boolean shouldSanityCheckConfiguration = false;
// Determined during execution.
private boolean debug = false;
/**
* Creates a new {@link ToolchainResolver} to help find the required toolchains for a configured
* target.
*
* @param env the environment to use to request dependent Skyframe nodes
* @param configurationKey The build configuration to use for dependent targets
*/
public ToolchainResolver(Environment env, BuildConfigurationValue.Key configurationKey) {
this.environment = checkNotNull(env);
this.configurationKey = checkNotNull(configurationKey);
}
/**
* Sets the labels of the required toolchain types that this resolver needs to find toolchains
* for.
*/
public ToolchainResolver setRequiredToolchainTypes(Set<Label> requiredToolchainTypeLabels) {
this.requiredToolchainTypeLabels = ImmutableSet.copyOf(requiredToolchainTypeLabels);
return this;
}
/**
* Sets extra constraints on the execution platform. Targets can use this to ensure that the
* execution platform has some desired characteristics, such as having enough memory to run tests.
*/
public ToolchainResolver setExecConstraintLabels(Set<Label> execConstraintLabels) {
this.execConstraintLabels = ImmutableSet.copyOf(execConstraintLabels);
return this;
}
/**
* Sets whether the experimental retroactive trimming mode is in use. This determines whether
* sanity checks regarding the fragments in use for the configurations of platforms and toolchains
* are used - specifically, whether platforms use only the PlatformConfiguration, and toolchains
* do not use any configuration at all.
*/
public ToolchainResolver setShouldSanityCheckConfiguration(boolean useSanityChecks) {
this.shouldSanityCheckConfiguration = useSanityChecks;
return this;
}
/**
* Determines the specific toolchains that are required, given the requested toolchain types,
* target platform, and configuration.
*
* <p>In order to resolve toolchains, first the {@link ToolchainResolver} must be created, and
* then an {@link UnloadedToolchainContext} generated. The {@link UnloadedToolchainContext} will
* report the specific toolchain targets to depend on, and those can be found using the typical
* dependency machinery. Once dependencies, including toolchains, have been loaded, the {@link
* ResolvedToolchainContext#load} method can be called to generate the final {@link
* ResolvedToolchainContext} to be used by the target.
*
* <p>This makes several SkyFrame calls, particularly to {@link
* com.google.devtools.build.lib.skyframe.ConfiguredTargetFunction} (to load platforms and
* toolchains), to {@link
* com.google.devtools.build.lib.skyframe.RegisteredExecutionPlatformsFunction}, and to {@link
* SingleToolchainResolutionFunction}. This method returns {@code null} to signal a SkyFrame
* restart is needed to resolve dependencies.
*/
@Nullable
public UnloadedToolchainContext resolve() throws InterruptedException, ToolchainException {
try {
UnloadedToolchainContext.Builder unloadedToolchainContext =
UnloadedToolchainContext.builder();
// Determine the configuration being used.
BuildConfigurationValue value =
(BuildConfigurationValue) environment.getValue(configurationKey);
if (value == null) {
throw new ValueMissingException();
}
BuildConfiguration configuration = value.getConfiguration();
PlatformConfiguration platformConfiguration =
configuration.getFragment(PlatformConfiguration.class);
if (platformConfiguration == null) {
throw new ValueMissingException();
}
// Check if debug output should be generated.
this.debug = configuration.getOptions().get(PlatformOptions.class).toolchainResolutionDebug;
// Create keys for all platforms that will be used, and validate them early.
PlatformKeys platformKeys = loadPlatformKeys(configuration, platformConfiguration);
if (environment.valuesMissing()) {
return null;
}
// Determine the actual toolchain implementations to use.
determineToolchainImplementations(unloadedToolchainContext, platformKeys);
return unloadedToolchainContext.build();
} catch (ValueMissingException e) {
return null;
}
}
@AutoValue
abstract static class PlatformKeys {
abstract ConfiguredTargetKey hostPlatformKey();
abstract ConfiguredTargetKey targetPlatformKey();
abstract ImmutableList<ConfiguredTargetKey> executionPlatformKeys();
static PlatformKeys create(
ConfiguredTargetKey hostPlatformKey,
ConfiguredTargetKey targetPlatformKey,
List<ConfiguredTargetKey> executionPlatformKeys) {
return new AutoValue_ToolchainResolver_PlatformKeys(
hostPlatformKey, targetPlatformKey, ImmutableList.copyOf(executionPlatformKeys));
}
}
private PlatformKeys loadPlatformKeys(
BuildConfiguration configuration, PlatformConfiguration platformConfiguration)
throws InterruptedException, InvalidPlatformException, ValueMissingException,
InvalidConstraintValueException {
// Determine the target and host platform keys.
Label hostPlatformLabel = platformConfiguration.getHostPlatform();
Label targetPlatformLabel = platformConfiguration.getTargetPlatform();
ConfiguredTargetKey hostPlatformKey = ConfiguredTargetKey.of(hostPlatformLabel, configuration);
ConfiguredTargetKey targetPlatformKey =
ConfiguredTargetKey.of(targetPlatformLabel, configuration);
// Load the host and target platforms early, to check for errors.
PlatformLookupUtil.getPlatformInfo(
ImmutableList.of(hostPlatformKey, targetPlatformKey),
environment,
shouldSanityCheckConfiguration);
if (environment.valuesMissing()) {
throw new ValueMissingException();
}
ImmutableList<ConfiguredTargetKey> executionPlatformKeys =
loadExecutionPlatformKeys(configuration, hostPlatformKey);
return PlatformKeys.create(hostPlatformKey, targetPlatformKey, executionPlatformKeys);
}
private ImmutableList<ConfiguredTargetKey> loadExecutionPlatformKeys(
BuildConfiguration configuration, ConfiguredTargetKey defaultPlatformKey)
throws InvalidPlatformException, InterruptedException, InvalidConstraintValueException,
ValueMissingException {
RegisteredExecutionPlatformsValue registeredExecutionPlatforms =
(RegisteredExecutionPlatformsValue)
environment.getValueOrThrow(
RegisteredExecutionPlatformsValue.key(configurationKey),
InvalidPlatformException.class);
if (registeredExecutionPlatforms == null) {
throw new ValueMissingException();
}
ImmutableList<ConfiguredTargetKey> availableExecutionPlatformKeys =
new ImmutableList.Builder<ConfiguredTargetKey>()
.addAll(registeredExecutionPlatforms.registeredExecutionPlatformKeys())
.add(defaultPlatformKey)
.build();
// Filter out execution platforms that don't satisfy the extra constraints.
ImmutableList<ConfiguredTargetKey> execConstraintKeys =
execConstraintLabels.stream()
.map(label -> ConfiguredTargetKey.of(label, configuration))
.collect(toImmutableList());
return filterAvailablePlatforms(availableExecutionPlatformKeys, execConstraintKeys);
}
/** Returns only the platform keys that match the given constraints. */
private ImmutableList<ConfiguredTargetKey> filterAvailablePlatforms(
ImmutableList<ConfiguredTargetKey> platformKeys,
ImmutableList<ConfiguredTargetKey> constraintKeys)
throws InterruptedException, InvalidPlatformException, InvalidConstraintValueException,
ValueMissingException {
// Short circuit if not needed.
if (constraintKeys.isEmpty()) {
return platformKeys;
}
// At this point the host and target platforms have been loaded, but not necessarily the chosen
// execution platform (it might be the same as the host platform, and might not).
//
// It's not worth trying to optimize away this call, since in the optimizable case (the exec
// platform is the host platform), Skyframe will return the correct results immediately without
// need of a restart.
Map<ConfiguredTargetKey, PlatformInfo> platformInfoMap =
PlatformLookupUtil.getPlatformInfo(
platformKeys, environment, shouldSanityCheckConfiguration);
if (platformInfoMap == null) {
throw new ValueMissingException();
}
List<ConstraintValueInfo> constraints =
ConstraintValueLookupUtil.getConstraintValueInfo(constraintKeys, environment);
if (constraints == null) {
throw new ValueMissingException();
}
return platformKeys.stream()
.filter(key -> filterPlatform(platformInfoMap.get(key), constraints))
.collect(toImmutableList());
}
/** Returns {@code true} if the given platform has all of the constraints. */
private boolean filterPlatform(PlatformInfo platformInfo, List<ConstraintValueInfo> constraints) {
ImmutableList<ConstraintValueInfo> missingConstraints =
platformInfo.constraints().findMissing(constraints);
if (debug) {
for (ConstraintValueInfo constraint : missingConstraints) {
// The value for this setting is not present in the platform, or doesn't match the expected
// value.
environment
.getListener()
.handle(
Event.info(
String.format(
"ToolchainResolver: Removed execution platform %s from"
+ " available execution platforms, it is missing constraint %s",
platformInfo.label(), constraint.label())));
}
}
return missingConstraints.isEmpty();
}
private void determineToolchainImplementations(
UnloadedToolchainContext.Builder unloadedToolchainContext, PlatformKeys platformKeys)
throws InterruptedException, ToolchainException, ValueMissingException {
// Find the toolchains for the required toolchain types.
List<SingleToolchainResolutionValue.Key> registeredToolchainKeys = new ArrayList<>();
for (Label toolchainTypeLabel : requiredToolchainTypeLabels) {
registeredToolchainKeys.add(
SingleToolchainResolutionValue.key(
configurationKey,
toolchainTypeLabel,
platformKeys.targetPlatformKey(),
platformKeys.executionPlatformKeys()));
}
Map<SkyKey, ValueOrException2<NoToolchainFoundException, InvalidToolchainLabelException>>
results =
environment.getValuesOrThrow(
registeredToolchainKeys,
NoToolchainFoundException.class,
InvalidToolchainLabelException.class);
boolean valuesMissing = false;
// Determine the potential set of toolchains.
Table<ConfiguredTargetKey, ToolchainTypeInfo, Label> resolvedToolchains =
HashBasedTable.create();
ImmutableSet.Builder<ToolchainTypeInfo> requiredToolchainTypesBuilder = ImmutableSet.builder();
List<Label> missingToolchains = new ArrayList<>();
for (Map.Entry<
SkyKey, ValueOrException2<NoToolchainFoundException, InvalidToolchainLabelException>>
entry : results.entrySet()) {
try {
ValueOrException2<NoToolchainFoundException, InvalidToolchainLabelException>
valueOrException = entry.getValue();
SingleToolchainResolutionValue singleToolchainResolutionValue =
(SingleToolchainResolutionValue) valueOrException.get();
if (singleToolchainResolutionValue == null) {
valuesMissing = true;
continue;
}
ToolchainTypeInfo requiredToolchainType = singleToolchainResolutionValue.toolchainType();
requiredToolchainTypesBuilder.add(requiredToolchainType);
resolvedToolchains.putAll(
findPlatformsAndLabels(requiredToolchainType, singleToolchainResolutionValue));
} catch (NoToolchainFoundException e) {
// Save the missing type and continue looping to check for more.
missingToolchains.add(e.missingToolchainTypeLabel());
}
}
if (!missingToolchains.isEmpty()) {
throw new UnresolvedToolchainsException(missingToolchains);
}
if (valuesMissing) {
throw new ValueMissingException();
}
ImmutableSet<ToolchainTypeInfo> requiredToolchainTypes = requiredToolchainTypesBuilder.build();
// Find and return the first execution platform which has all required toolchains.
Optional<ConfiguredTargetKey> selectedExecutionPlatformKey;
if (requiredToolchainTypeLabels.isEmpty()
&& platformKeys.executionPlatformKeys().contains(platformKeys.hostPlatformKey())) {
// Fall back to the legacy behavior: use the host platform if it's available, otherwise the
// first execution platform.
selectedExecutionPlatformKey = Optional.of(platformKeys.hostPlatformKey());
} else {
// If there are no toolchains, this will return the first execution platform.
selectedExecutionPlatformKey =
findExecutionPlatformForToolchains(
requiredToolchainTypes, platformKeys.executionPlatformKeys(), resolvedToolchains);
}
if (!selectedExecutionPlatformKey.isPresent()) {
throw new NoMatchingPlatformException(
requiredToolchainTypeLabels,
platformKeys.executionPlatformKeys(),
platformKeys.targetPlatformKey());
}
Map<ConfiguredTargetKey, PlatformInfo> platforms =
PlatformLookupUtil.getPlatformInfo(
ImmutableList.of(selectedExecutionPlatformKey.get(), platformKeys.targetPlatformKey()),
environment,
shouldSanityCheckConfiguration);
if (platforms == null) {
throw new ValueMissingException();
}
unloadedToolchainContext.setRequiredToolchainTypes(requiredToolchainTypes);
unloadedToolchainContext.setExecutionPlatform(
platforms.get(selectedExecutionPlatformKey.get()));
unloadedToolchainContext.setTargetPlatform(platforms.get(platformKeys.targetPlatformKey()));
Map<ToolchainTypeInfo, Label> toolchains =
resolvedToolchains.row(selectedExecutionPlatformKey.get());
unloadedToolchainContext.setToolchainTypeToResolved(ImmutableBiMap.copyOf(toolchains));
}
/**
* Adds all of toolchain labels from{@code toolchainResolutionValue} to {@code
* resolvedToolchains}.
*/
private static Table<ConfiguredTargetKey, ToolchainTypeInfo, Label> findPlatformsAndLabels(
ToolchainTypeInfo requiredToolchainType,
SingleToolchainResolutionValue singleToolchainResolutionValue) {
Table<ConfiguredTargetKey, ToolchainTypeInfo, Label> resolvedToolchains =
HashBasedTable.create();
for (Map.Entry<ConfiguredTargetKey, Label> entry :
singleToolchainResolutionValue.availableToolchainLabels().entrySet()) {
resolvedToolchains.put(entry.getKey(), requiredToolchainType, entry.getValue());
}
return resolvedToolchains;
}
/**
* Finds the first platform from {@code availableExecutionPlatformKeys} that is present in {@code
* resolvedToolchains} and has all required toolchain types.
*/
private Optional<ConfiguredTargetKey> findExecutionPlatformForToolchains(
ImmutableSet<ToolchainTypeInfo> requiredToolchainTypes,
ImmutableList<ConfiguredTargetKey> availableExecutionPlatformKeys,
Table<ConfiguredTargetKey, ToolchainTypeInfo, Label> resolvedToolchains) {
for (ConfiguredTargetKey executionPlatformKey : availableExecutionPlatformKeys) {
Map<ToolchainTypeInfo, Label> toolchains = resolvedToolchains.row(executionPlatformKey);
if (!toolchains.keySet().containsAll(requiredToolchainTypes)) {
// Not all toolchains are present, keep going
continue;
}
if (debug) {
String selectedToolchains =
toolchains.entrySet().stream()
.map(
e ->
String.format(
"type %s -> toolchain %s", e.getKey().typeLabel(), e.getValue()))
.collect(joining(", "));
environment
.getListener()
.handle(
Event.info(
String.format(
"ToolchainResolver: Selected execution platform %s, %s",
executionPlatformKey.getLabel(), selectedToolchains)));
}
return Optional.of(executionPlatformKey);
}
return Optional.empty();
}
private static final class ValueMissingException extends Exception {
private ValueMissingException() {
super();
}
}
/** Exception used when no execution platform can be found. */
static final class NoMatchingPlatformException extends ToolchainException {
NoMatchingPlatformException(
Set<Label> requiredToolchainTypeLabels,
ImmutableList<ConfiguredTargetKey> availableExecutionPlatformKeys,
ConfiguredTargetKey targetPlatformKey) {
super(
formatError(
requiredToolchainTypeLabels, availableExecutionPlatformKeys, targetPlatformKey));
}
private static String formatError(
Set<Label> requiredToolchainTypeLabels,
ImmutableList<ConfiguredTargetKey> availableExecutionPlatformKeys,
ConfiguredTargetKey targetPlatformKey) {
if (requiredToolchainTypeLabels.isEmpty()) {
return String.format(
"Unable to find an execution platform for target platform %s"
+ " from available execution platforms [%s]",
targetPlatformKey.getLabel(),
availableExecutionPlatformKeys.stream()
.map(key -> key.getLabel().toString())
.collect(Collectors.joining(", ")));
}
return String.format(
"Unable to find an execution platform for toolchains [%s] and target platform %s"
+ " from available execution platforms [%s]",
requiredToolchainTypeLabels.stream().map(Label::toString).collect(joining(", ")),
targetPlatformKey.getLabel(),
availableExecutionPlatformKeys.stream()
.map(key -> key.getLabel().toString())
.collect(Collectors.joining(", ")));
}
}
/** Exception used when a toolchain type is required but no matching toolchain is found. */
static final class UnresolvedToolchainsException extends ToolchainException {
UnresolvedToolchainsException(List<Label> missingToolchainTypes) {
super(
String.format(
"no matching toolchains found for types %s",
missingToolchainTypes.stream().map(Label::toString).collect(joining(", "))));
}
}
}