| // 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.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<PackageValue.Key, 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<PackageValue.Key, PackageValue> getBuildSettingPackages( |
| SkyFunction.Environment env, ImmutableSet<Label> buildSettings) |
| throws InterruptedException, TransitionException { |
| HashMap<PackageValue.Key, 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<PackageValue.Key> packageKeys = |
| getPackageKeysFromLabels(unverifiedBuildSettings); |
| SkyframeLookupResult newlyLoaded = env.getValuesAndExceptions(packageKeys); |
| if (env.valuesMissing()) { |
| return null; |
| } |
| for (PackageValue.Key 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 PackageValue.Key}s. */ |
| private static ImmutableSet<PackageValue.Key> getPackageKeysFromLabels(Set<Label> buildSettings) { |
| ImmutableSet.Builder<PackageValue.Key> keyBuilder = new ImmutableSet.Builder<>(); |
| for (Label setting : buildSettings) { |
| keyBuilder.add(PackageValue.key(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<PackageValue.Key, PackageValue> buildSettingPackages, Set<Label> buildSettingsToVerify) |
| throws TransitionException { |
| ImmutableSet.Builder<Label> actualSettingBuilder = new ImmutableSet.Builder<>(); |
| for (Label allegedBuildSetting : buildSettingsToVerify) { |
| Package buildSettingPackage = |
| buildSettingPackages |
| .get(PackageValue.key(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) { |
| actualSettingBuilder.add((Label) actualValue); |
| 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<PackageValue.Key, 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<PackageValue.Key, PackageValue> buildSettingPackages, Label setting) { |
| Package buildSettingPackage = |
| buildSettingPackages.get(PackageValue.key(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); |
| } |
| } |
| } |