| // 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); | |
| } | |
| } | |
| } |