blob: ea902c5c2a54c164a63ccf1b259f34ba46c1f16c [file] [log] [blame]
// Copyright 2022 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.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.devtools.build.lib.packages.RuleClass.Builder.STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.starlark.StarlarkBuildSettingsDetailsValue;
import com.google.devtools.build.lib.analysis.starlark.StarlarkTransition.TransitionException;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.packages.BuildType.SelectorList;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.Type;
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 com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/** A builder for {@link StarlarkBuildSettingsDetailsValue} instances. */
final class StarlarkBuildSettingsDetailsFunction implements SkyFunction {
// Use the plain strings rather than reaching into the Alias class and adding a dependency edge.
// TODO(blaze-configurability-team): We can probably afford the edge now that this is
// inside of skyframe_cluster.
private static final String ALIAS_RULE_NAME = "alias";
private static final String ALIAS_ACTUAL_ATTRIBUTE_NAME = "actual";
StarlarkBuildSettingsDetailsFunction() {}
@Override
@Nullable
public SkyValue compute(SkyKey skyKey, Environment env)
throws InterruptedException, StarlarkBuildSettingsDetailsException {
StarlarkBuildSettingsDetailsValue.Key key = (StarlarkBuildSettingsDetailsValue.Key) skyKey;
// Ideally, callers would bypass StarlarkBuildSettingsDetailsFunction entirely when the
// key is empty but provide a fast escape here just in case.
if (key.buildSettings().isEmpty()) {
return StarlarkBuildSettingsDetailsValue.EMPTY;
}
try {
ImmutableMap<PackageIdentifier, PackageValue> buildSettingPackages =
getBuildSettingPackages(env, key.buildSettings());
if (buildSettingPackages == null) {
return null;
}
// Each setting is unique so don't need a merge function.
ImmutableMap<Label, Rule> rawSettingToActualRule =
key.buildSettings().stream()
.collect(
toImmutableMap(
setting -> setting,
setting -> getActual(buildSettingPackages, setting).getAssociatedRule()));
ImmutableSet<Rule> actualRules = ImmutableSet.copyOf(rawSettingToActualRule.values());
// Calculate info based on the actual rules
// Different rules have different labels so don't need a merge function
ImmutableMap<Label, Object> buildSettingToDefault =
actualRules.stream()
.collect(
toImmutableMap(
Rule::getLabel,
rule -> {
if (rule.getRuleClassObject().getBuildSetting().allowsMultiple()) {
return ImmutableList.of(
rule.getAttr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME));
}
return rule.getAttr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME);
}));
ImmutableMap<Label, Type<?>> buildSettingToType =
actualRules.stream()
.collect(
toImmutableMap(
Rule::getLabel,
rule -> rule.getRuleClassObject().getBuildSetting().getType()));
ImmutableSet<Label> buildSettingIsAllowsMultiple =
actualRules.stream()
.filter(rule -> rule.getRuleClassObject().getBuildSetting().allowsMultiple())
.map(Rule::getLabel)
.collect(toImmutableSet());
// Calculate the alias table (filtering out non-aliases!)
ImmutableMap<Label, Label> aliasToActual =
rawSettingToActualRule.entrySet().stream()
.filter(entry -> !entry.getKey().equals(entry.getValue().getLabel()))
.collect(toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().getLabel()));
return StarlarkBuildSettingsDetailsValue.create(
buildSettingToDefault, buildSettingToType, buildSettingIsAllowsMultiple, aliasToActual);
} catch (TransitionException e) {
throw new StarlarkBuildSettingsDetailsException(e);
}
}
/**
* Given a {@link ConfigurationTransition} find all build settings read or set by the transition
* and load their packages.
*
* <p>In the case that build settings are referred to by aliases, we do a couple loops of package
* loading. We generally don't expect build settings to be aliased multiple times so we don't
* expect this while loop (and relevant null return) to happen more than two or three times (and
* usually only once).
*
* @return the package keys and values of build settings or null if not all packages are
* available. if not null, and some build settings are referenced by alias, the returned map
* will include both alias and actual packages to allow for alias chain following at a later
* state.
*/
@Nullable
private static ImmutableMap<PackageIdentifier, PackageValue> getBuildSettingPackages(
SkyFunction.Environment env, ImmutableSet<Label> buildSettings)
throws InterruptedException, TransitionException {
HashMap<PackageIdentifier, PackageValue> buildSettingPackages = new HashMap<>();
// This happens before cycle detection so keep track of all seen build settings to ensure
// we don't get stuck in endless loops (e.g. //alias1->//alias2 && //alias2->alias1)
Set<Label> allSeenBuildSettings = new HashSet<>();
ImmutableSet<Label> unverifiedBuildSettings = buildSettings;
while (!unverifiedBuildSettings.isEmpty()) {
for (Label buildSetting : unverifiedBuildSettings) {
if (!allSeenBuildSettings.add(buildSetting)) {
throw new TransitionException(
String.format(
"Dependency cycle involving '%s' detected in aliased build settings",
buildSetting));
}
}
ImmutableSet<PackageIdentifier> packageKeys =
getPackageKeysFromLabels(unverifiedBuildSettings);
SkyframeLookupResult newlyLoaded = env.getValuesAndExceptions(packageKeys);
if (env.valuesMissing()) {
return null;
}
for (PackageIdentifier packageKey : packageKeys) {
try {
SkyValue skyValue = newlyLoaded.getOrThrow(packageKey, NoSuchPackageException.class);
buildSettingPackages.put(packageKey, (PackageValue) skyValue);
} catch (NoSuchPackageException e) {
throw new TransitionException(e);
}
}
unverifiedBuildSettings =
verifyBuildSettingsAndGetAliases(buildSettingPackages, unverifiedBuildSettings);
}
return ImmutableMap.copyOf(buildSettingPackages);
}
/** Given a set of labels, return a set of their package {@link PackageIdentifier} keys. */
private static ImmutableSet<PackageIdentifier> getPackageKeysFromLabels(
Set<Label> buildSettings) {
ImmutableSet.Builder<PackageIdentifier> keyBuilder = new ImmutableSet.Builder<>();
for (Label setting : buildSettings) {
keyBuilder.add(setting.getPackageIdentifier());
}
return keyBuilder.build();
}
/**
* Given a preliminary set of alleged build setting labels and relevant packages, verify that the
* given {@link Label}s actually correspond to build setting targets.
*
* <p>This method is meant to be run in a loop to handle aliased build settings. It also
* explicitly bans configured 'actual' values for aliased build settings. Since build settings are
* used to define configuration, there should be better ways to accomplish disparate
* configurations than configured aliases. Also from a technical standpoint, it's unclear what
* configuration is correct to use to resolve configured attributes.
*
* @param buildSettingPackages packages that include {@code buildSettingsToVerify}'s packages
* @param buildSettingsToVerify alleged build setting labels
* @return a set of "actual" labels of any build settings that are referenced by aliases (note -
* if the "actual" value of aliasA is aliasB, this method returns aliasB AKA we only follow
* one link in the alias chain per call of this method)
*/
private static ImmutableSet<Label> verifyBuildSettingsAndGetAliases(
Map<PackageIdentifier, PackageValue> buildSettingPackages, Set<Label> buildSettingsToVerify)
throws TransitionException {
ImmutableSet.Builder<Label> actualSettingBuilder = new ImmutableSet.Builder<>();
for (Label allegedBuildSetting : buildSettingsToVerify) {
Package buildSettingPackage =
buildSettingPackages.get(allegedBuildSetting.getPackageIdentifier()).getPackage();
Preconditions.checkNotNull(
buildSettingPackage, "Reading build setting for which we don't have a package");
Target buildSettingTarget;
try {
buildSettingTarget = buildSettingPackage.getTarget(allegedBuildSetting.getName());
} catch (NoSuchTargetException e) {
throw new TransitionException(e);
}
if (buildSettingTarget.getAssociatedRule() == null) {
throw new TransitionException(
String.format(
"attempting to transition on '%s' which is not a build setting",
allegedBuildSetting));
}
if (buildSettingTarget.getAssociatedRule().getRuleClass().equals(ALIAS_RULE_NAME)) {
Object actualValue =
buildSettingTarget.getAssociatedRule().getAttr(ALIAS_ACTUAL_ATTRIBUTE_NAME);
if (actualValue instanceof Label label) {
actualSettingBuilder.add(label);
continue;
} else if (actualValue instanceof SelectorList) {
// configured "actual" value
throw new TransitionException(
String.format(
"attempting to transition on aliased build setting '%s', the actual value of"
+ " which uses select(). Aliased build settings with configured actual values"
+ " is not supported.",
allegedBuildSetting));
} else {
throw new IllegalStateException(
String.format(
"Alias target '%s' with 'actual' attr value not equals to "
+ "a label or a selectorlist",
allegedBuildSetting));
}
}
if (!buildSettingTarget.getAssociatedRule().isBuildSetting()) {
throw new TransitionException(
String.format(
"attempting to transition on '%s' which is not a build setting",
allegedBuildSetting));
}
}
return actualSettingBuilder.build();
}
/**
* Given a {@link Label} that could be an {@link com.google.devtools.build.lib.rules.Alias} and a
* set of packages, find the actual target that {@link Label} ultimately points to.
*
* <p>This method assumes that the packages of the entire {@link
* com.google.devtools.build.lib.rules.Alias} chain (if {@code setting} is indeed an alias) are
* included in {@code buildSettingPackages}
*
* <p>This checking is likely done in {@link #verifyBuildSettingsAndGetAliases}.
*/
private static Target getActual(
Map<PackageIdentifier, PackageValue> buildSettingPackages, Label setting) {
Target target = getTarget(buildSettingPackages, setting);
while (target.getAssociatedRule().getRuleClass().equals(ALIAS_RULE_NAME)) {
target =
getTarget(
buildSettingPackages,
(Label) target.getAssociatedRule().getAttr(ALIAS_ACTUAL_ATTRIBUTE_NAME));
}
return target;
}
/**
* Return a target given its label and a set of package values we know to contain the target.
*
* <p>This method is essentially a wrapper around PackageValue.getTarget.
*
* @param buildSettingPackages packages that include {@code setting}'s package
*/
private static Target getTarget(
Map<PackageIdentifier, PackageValue> buildSettingPackages, Label setting) {
Package buildSettingPackage =
buildSettingPackages.get(setting.getPackageIdentifier()).getPackage();
Preconditions.checkNotNull(
buildSettingPackage, "Reading build setting for which we don't have a package");
Target target;
try {
target = buildSettingPackage.getTarget(setting.getName());
} catch (NoSuchTargetException e) {
// This should never happen, see javadoc.
throw new IllegalStateException(e);
}
return target;
}
private static final class StarlarkBuildSettingsDetailsException extends SkyFunctionException {
StarlarkBuildSettingsDetailsException(Exception e) {
super(e, Transience.PERSISTENT);
}
}
}