| // 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.StarlarkBuildSettingsDetailsValue.CustomExecScopeValue; |
| 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.RawAttributeMapper; |
| 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() && key.hostFlags().isEmpty()) { |
| return StarlarkBuildSettingsDetailsValue.EMPTY; |
| } |
| |
| // for each --flag_alias to --host_{flag} alias, we load the starlarkified host flag and get its |
| // default value. |
| // and scopeType. These information are only needed for flags that declare a scope of |
| // exec:--{some_other_flag}. This is necessary because Bazel doesn't know how to find Starlark |
| // host flags if they're not set at the command line, and we need to ensure the exec transition |
| // correctly sets --foo=--host_foo even for builds that don't reference either flag |
| ImmutableMap.Builder<Label, CustomExecScopeValue> customExecScopeValuesBuilder = |
| ImmutableMap.builder(); |
| for (Label hostFlag : key.hostFlags()) { |
| Label flag = |
| Label.createUnvalidated( |
| hostFlag.getPackageIdentifier(), hostFlag.getName().replaceFirst("host_", "")); |
| |
| try { |
| ImmutableMap<PackageIdentifier, PackageValue> buildSettingPackage = |
| getBuildSettingPackages(env, ImmutableSet.of(flag)); |
| if (buildSettingPackage == null) { |
| return null; |
| } |
| |
| // if --host_foo exists, then look up for --foo. |
| Target flagTarget; |
| try { |
| flagTarget = getTarget(buildSettingPackage, flag); |
| } catch (NoSuchTargetException e) { |
| // while we might not expect --host_foo to exist when --foo doesn't exist, |
| // if that happens the outcome is there's no scoped flag we have to account for. |
| // In that case rather than error, we simply break out of this logic since --host_foo |
| // doesn't matter here now. |
| continue; |
| } |
| |
| if (flagTarget == null) { |
| return null; |
| } |
| var attrMap = RawAttributeMapper.of(flagTarget.getAssociatedRule()); |
| Object flagDefaultValue = |
| flagTarget.getAssociatedRule().getAttr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME); |
| if (attrMap.isAttributeValueExplicitlySpecified("scope")) { |
| String scopeType = attrMap.get("scope", Type.STRING); |
| if (scopeType.startsWith("exec:--")) { |
| // load the starlarkified host flag |
| Target starlarkifiedHostFlag; |
| try { |
| starlarkifiedHostFlag = getTarget(buildSettingPackage, hostFlag); |
| } catch (NoSuchTargetException e) { |
| throw new StarlarkBuildSettingsDetailsException(e); |
| } |
| |
| if (starlarkifiedHostFlag == null) { |
| return null; |
| } |
| var hostAttrMap = RawAttributeMapper.of(starlarkifiedHostFlag.getAssociatedRule()); |
| String hostScopeType = |
| attrMap.isAttributeValueExplicitlySpecified("scope") |
| ? hostAttrMap.get("scope", Type.STRING) |
| : "default"; |
| Object hostFlagDefaultValue = |
| starlarkifiedHostFlag |
| .getAssociatedRule() |
| .getAttr(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME); |
| if (starlarkifiedHostFlag |
| .getAssociatedRule() |
| .getRuleClassObject() |
| .getBuildSetting() |
| .allowsMultiple()) { |
| // allowed multiple |
| customExecScopeValuesBuilder.put( |
| flagTarget.getLabel(), |
| new CustomExecScopeValue( |
| flagTarget.getLabel(), |
| flagDefaultValue, |
| hostFlag, |
| ImmutableList.of(hostFlagDefaultValue), |
| scopeType, |
| hostScopeType)); |
| } else { |
| // not allowed multiple |
| customExecScopeValuesBuilder.put( |
| flagTarget.getLabel(), |
| new CustomExecScopeValue( |
| flagTarget.getLabel(), |
| flagDefaultValue, |
| hostFlag, |
| hostFlagDefaultValue, |
| scopeType, |
| hostScopeType)); |
| } |
| } |
| } |
| } catch (TransitionException e) { |
| throw new StarlarkBuildSettingsDetailsException(e); |
| } |
| } |
| |
| ImmutableSet<Label> effectiveBuildSettings = |
| ImmutableSet.<Label>builder() |
| .addAll(key.buildSettings()) |
| .addAll(customExecScopeValuesBuilder.buildOrThrow().keySet()) |
| .build(); |
| |
| try { |
| ImmutableMap<PackageIdentifier, PackageValue> buildSettingPackages = |
| getBuildSettingPackages(env, effectiveBuildSettings); |
| if (buildSettingPackages == null) { |
| return null; |
| } |
| |
| // Each setting is unique so don't need a merge function. |
| ImmutableMap<Label, Rule> rawSettingToActualRule = |
| effectiveBuildSettings.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, |
| customExecScopeValuesBuilder.buildOrThrow()); |
| |
| } 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); |
| if (skyValue == null) { |
| return null; |
| } |
| 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; |
| try { |
| target = getTarget(buildSettingPackages, setting); |
| } catch (NoSuchTargetException e) { |
| throw new IllegalStateException(e); |
| } |
| |
| while (target.getAssociatedRule().getRuleClass().equals(ALIAS_RULE_NAME)) { |
| try { |
| target = |
| getTarget( |
| buildSettingPackages, |
| (Label) target.getAssociatedRule().getAttr(ALIAS_ACTUAL_ATTRIBUTE_NAME)); |
| } catch (NoSuchTargetException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| 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) |
| throws NoSuchTargetException { |
| Package buildSettingPackage = |
| buildSettingPackages.get(setting.getPackageIdentifier()).getPackage(); |
| Preconditions.checkNotNull( |
| buildSettingPackage, "Reading build setting for which we don't have a package"); |
| Target target; |
| target = buildSettingPackage.getTarget(setting.getName()); |
| return target; |
| } |
| |
| private static final class StarlarkBuildSettingsDetailsException extends SkyFunctionException { |
| StarlarkBuildSettingsDetailsException(Exception e) { |
| super(e, Transience.PERSISTENT); |
| } |
| |
| StarlarkBuildSettingsDetailsException(NoSuchTargetException message) { |
| super(message, Transience.PERSISTENT); |
| } |
| } |
| } |