blob: 209996262ed7401f343bc53bdc55778a6b4d63a3 [file] [log] [blame]
// Copyright 2014 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.devtools.build.lib.analysis.DependencyKind.OUTPUT_FILE_RULE_DEPENDENCY;
import static com.google.devtools.build.lib.analysis.DependencyKind.VISIBILITY_DEPENDENCY;
import com.google.auto.value.AutoOneOf;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.DependencyKind.AttributeDependencyKind;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
import com.google.devtools.build.lib.analysis.config.ExecutionTransitionFactory;
import com.google.devtools.build.lib.analysis.config.Fragment;
import com.google.devtools.build.lib.analysis.config.transitions.TransitionFactory;
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.Aspect;
import com.google.devtools.build.lib.packages.AspectClass;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Attribute.ComputedDefault;
import com.google.devtools.build.lib.packages.Attribute.LateBoundDefault;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.AttributeTransitionData;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.ConfiguredAttributeMapper;
import com.google.devtools.build.lib.packages.EnvironmentGroup;
import com.google.devtools.build.lib.packages.ExecGroup;
import com.google.devtools.build.lib.packages.InputFile;
import com.google.devtools.build.lib.packages.OutputFile;
import com.google.devtools.build.lib.packages.PackageGroup;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.skyframe.toolchains.UnloadedToolchainContext;
import com.google.devtools.build.lib.util.OrderedSetMultimap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.syntax.Location;
/**
* Helpers for resolution for dependencies between configured targets.
*
* <p>Includes logic to determine all attribute dependencies and their associated labels.
*/
public final class DependencyResolutionHelpers {
private DependencyResolutionHelpers() {}
/** The tuple {@link #computeDependencyLabels} outputs. */
public static final class DependencyLabels {
private final OrderedSetMultimap<DependencyKind, Label> labels;
@Nullable private final ConfiguredAttributeMapper attributeMap;
private DependencyLabels(
OrderedSetMultimap<DependencyKind, Label> labels,
@Nullable ConfiguredAttributeMapper attributeMap) {
this.labels = labels;
this.attributeMap = attributeMap;
}
public OrderedSetMultimap<DependencyKind, Label> labels() {
return labels;
}
@Nullable // Non-null for rules and output files when there are aspects that apply to files.
public ConfiguredAttributeMapper attributeMap() {
return attributeMap;
}
}
public static DependencyLabels computeDependencyLabels(
TargetAndConfiguration node,
ImmutableList<Aspect> aspects,
ImmutableMap<Label, ConfigMatchingProvider> configConditions,
@Nullable ToolchainCollection<ToolchainContext> toolchainContexts,
@Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts)
throws Failure, InterruptedException {
Target target = node.getTarget();
BuildConfigurationValue config = node.getConfiguration();
OrderedSetMultimap<DependencyKind, Label> outgoingLabels = OrderedSetMultimap.create();
// TODO(bazel-team): Figure out a way to implement the below using LabelVisitationUtils.
Rule fromRule;
ConfiguredAttributeMapper attributeMap = null;
if (target instanceof OutputFile) {
Preconditions.checkNotNull(config);
addVisibilityDepLabels(target.getVisibilityDependencyLabels(), outgoingLabels);
Rule rule = ((OutputFile) target).getGeneratingRule();
outgoingLabels.put(OUTPUT_FILE_RULE_DEPENDENCY, rule.getLabel());
if (Iterables.any(aspects, a -> a.getDefinition().applyToFiles())) {
attributeMap = ConfiguredAttributeMapper.of(rule, configConditions, config);
resolveAttributes(getAspectAttributes(aspects), outgoingLabels, rule, attributeMap, config);
}
addToolchainDeps(toolchainContexts, outgoingLabels);
} else if (target instanceof InputFile || target instanceof EnvironmentGroup) {
addVisibilityDepLabels(target.getVisibilityDependencyLabels(), outgoingLabels);
} else if (target instanceof Rule rule) {
fromRule = rule;
attributeMap = ConfiguredAttributeMapper.of(fromRule, configConditions, config);
visitRule(
node,
aspects,
attributeMap,
toolchainContexts,
baseTargetUnloadedToolchainContexts,
outgoingLabels);
} else if (target instanceof PackageGroup packageGroup) {
outgoingLabels.putAll(VISIBILITY_DEPENDENCY, packageGroup.getIncludes());
} else {
throw new IllegalStateException(target.getLabel().toString());
}
return new DependencyLabels(outgoingLabels, attributeMap);
}
/** The results of {@link #getExecutionPlatformLabel} as a tagged union. */
@AutoOneOf(DependencyResolutionHelpers.ExecutionPlatformResult.Kind.class)
public abstract static class ExecutionPlatformResult {
/** Tags for the possible results. */
public enum Kind {
/** A label was successfully determined. */
LABEL,
/**
* A label was successfully determined to be null.
*
* <p>{@link AutoOneOf} does not permit {@code @Nullable} so this is distinct from {@link
* #LABEL}.
*/
NULL_LABEL,
/**
* The dependency should be skipped.
*
* <p>See comments in {@link #getExecutionPlatformLabel} for details.
*/
SKIP,
/** An error message. */
ERROR
}
public abstract Kind kind();
public abstract Label label();
abstract void nullLabel();
abstract void skip();
public abstract String error();
private static ExecutionPlatformResult ofLabel(Label label) {
return AutoOneOf_DependencyResolutionHelpers_ExecutionPlatformResult.label(label);
}
private static ExecutionPlatformResult ofNullLabel() {
return AutoOneOf_DependencyResolutionHelpers_ExecutionPlatformResult.nullLabel();
}
private static ExecutionPlatformResult ofSkip() {
return AutoOneOf_DependencyResolutionHelpers_ExecutionPlatformResult.skip();
}
private static ExecutionPlatformResult ofError(String message) {
return AutoOneOf_DependencyResolutionHelpers_ExecutionPlatformResult.error(message);
}
}
public static ExecutionPlatformResult getExecutionPlatformLabel(
AttributeDependencyKind kind,
@Nullable ToolchainCollection<ToolchainContext> toolchainContexts,
@Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts,
ImmutableList<Aspect> aspectsList) {
if (aspectsList.isEmpty() || isMainAspect(aspectsList, kind.getOwningAspect())) {
return getExecutionPlatformLabel(kind, toolchainContexts);
} else if (kind.getOwningAspect() == null) {
// During aspect evaluation, use {@code baseTargetUnloadedToolchainContexts} for the base
// target's dependencies.
return getExecutionPlatformLabel(kind, baseTargetUnloadedToolchainContexts);
} else {
ExecutionPlatformResult executionPlatformResult =
getExecutionPlatformLabel(kind, toolchainContexts);
if (executionPlatformResult.kind() == ExecutionPlatformResult.Kind.ERROR) {
// TODO(b/373963347): Make the toolchain contexts of base aspects available to be used with
// their corresponding dependencies.
// Currently dependencies of the base aspects are resolved with the toolchain context of the
// main aspect, skip errors as actual errors would be reported during the base aspect
// evaluation.
return ExecutionPlatformResult.ofSkip();
} else {
return executionPlatformResult;
}
}
}
private static ExecutionPlatformResult getExecutionPlatformLabel(
AttributeDependencyKind kind,
@Nullable ToolchainCollection<? extends ToolchainContext> toolchainContexts) {
if (toolchainContexts == null) {
return ExecutionPlatformResult.ofNullLabel();
}
TransitionFactory<AttributeTransitionData> transitionFactory =
kind.getAttribute().getTransitionFactory();
if (!(transitionFactory instanceof ExecutionTransitionFactory)) {
return ExecutionPlatformResult.ofLabel(
toolchainContexts
.getToolchainContext(ExecGroup.DEFAULT_EXEC_GROUP_NAME)
.executionPlatform()
.label());
}
String execGroup = ((ExecutionTransitionFactory) transitionFactory).getExecGroup();
if (toolchainContexts.hasToolchainContext(execGroup)) {
PlatformInfo platform = toolchainContexts.getToolchainContext(execGroup).executionPlatform();
return platform == null
? ExecutionPlatformResult.ofNullLabel()
: ExecutionPlatformResult.ofLabel(platform.label());
}
return ExecutionPlatformResult.ofError(
String.format(
"Attr '%s' declares a transition for non-existent exec group '%s'",
kind.getAttribute().getName(), execGroup));
}
/** True if {@code owningAspect} is the main aspect, the last one in {@code aspectsList}. */
private static boolean isMainAspect(
ImmutableList<Aspect> aspectsList, @Nullable AspectClass owningAspect) {
return Iterables.getLast(aspectsList).getAspectClass().equals(owningAspect);
}
/** Indicates a failure during dependency resolution. */
public static class Failure extends Exception {
@Nullable private final Location location;
private Failure(Location location, String message) {
super(message);
this.location = location;
}
/** Returns the location of the error, if known. */
@Nullable
public Location getLocation() {
return location;
}
}
private static void visitRule(
TargetAndConfiguration node,
ImmutableList<Aspect> aspects,
ConfiguredAttributeMapper attributeMap,
@Nullable ToolchainCollection<ToolchainContext> toolchainContexts,
@Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts,
OrderedSetMultimap<DependencyKind, Label> outgoingLabels)
throws Failure, InterruptedException {
Preconditions.checkArgument(node.getTarget() instanceof Rule, node);
BuildConfigurationValue ruleConfig = Preconditions.checkNotNull(node.getConfiguration(), node);
Rule rule = (Rule) node.getTarget();
try {
attributeMap.validateAttributes();
} catch (ConfiguredAttributeMapper.ValidationException ex) {
throw new Failure(rule.getLocation(), ex.getMessage());
}
Iterable<Label> visibilityDepLabels = rule.getVisibilityDependencyLabels();
addVisibilityDepLabels(visibilityDepLabels, outgoingLabels);
resolveAttributes(getAttributes(rule, aspects), outgoingLabels, rule, attributeMap, ruleConfig);
// Add the rule's visibility labels (which may come from the rule or from package defaults).
addExplicitDeps(outgoingLabels, rule, "visibility", visibilityDepLabels);
// Add package default constraints when the rule doesn't explicitly declare them.
//
// Note that this can have subtle implications for constraint semantics. For example: say that
// package defaults declare compatibility with ':foo' and rule R declares compatibility with
// ':bar'. Does that mean that R is compatible with [':foo', ':bar'] or just [':bar']? In other
// words, did R's author intend to add additional compatibility to the package defaults or to
// override them? More severely, what if package defaults "restrict" support to just [':baz']?
// Should R's declaration signify [':baz'] + ['bar'], [ORIGINAL_DEFAULTS] + ['bar'], or
// something else?
//
// Rather than try to answer these questions with possibly confusing logic, we take the
// simple approach of assigning the rule's "restriction" attribute to the rule-declared value if
// it exists, else the package defaults value (and likewise for "compatibility"). This may not
// always provide what users want, but it makes it easy for them to understand how rule
// declarations and package defaults intermix (and how to refactor them to get what they want).
//
// An alternative model would be to apply the "rule declaration" / "rule class defaults"
// relationship, i.e. the rule class' "compatibility" and "restriction" declarations are merged
// to generate a set of default environments, then the rule's declarations are independently
// processed on top of that. This protects against obscure coupling behavior between
// declarations from wildly different places (e.g. it offers clear answers to the examples posed
// above). But within the scope of a single package it seems better to keep the model simple and
// make the user responsible for resolving ambiguities.
if (!rule.isAttributeValueExplicitlySpecified(RuleClass.COMPATIBLE_ENVIRONMENT_ATTR)) {
addExplicitDeps(
outgoingLabels,
rule,
RuleClass.COMPATIBLE_ENVIRONMENT_ATTR,
rule.getPackage().getPackageArgs().defaultCompatibleWith());
}
if (!rule.isAttributeValueExplicitlySpecified(RuleClass.RESTRICTED_ENVIRONMENT_ATTR)) {
addExplicitDeps(
outgoingLabels,
rule,
RuleClass.RESTRICTED_ENVIRONMENT_ATTR,
rule.getPackage().getPackageArgs().defaultRestrictedTo());
}
addToolchainDeps(toolchainContexts, outgoingLabels);
addBaseTargetToolchainDeps(baseTargetUnloadedToolchainContexts, outgoingLabels);
}
private static void addToolchainDeps(
ToolchainCollection<ToolchainContext> toolchainContexts,
OrderedSetMultimap<DependencyKind, Label> outgoingLabels) {
if (toolchainContexts != null) {
for (Map.Entry<String, ToolchainContext> entry :
toolchainContexts.getContextMap().entrySet()) {
outgoingLabels.putAll(
DependencyKind.forExecGroup(entry.getKey()),
entry.getValue().resolvedToolchainLabels());
}
}
}
private static void addBaseTargetToolchainDeps(
@Nullable ToolchainCollection<UnloadedToolchainContext> toolchainContexts,
OrderedSetMultimap<DependencyKind, Label> outgoingLabels) {
if (toolchainContexts == null) {
return;
}
for (Map.Entry<String, UnloadedToolchainContext> execGroup :
toolchainContexts.getContextMap().entrySet()) {
for (var toolchainTypeToResolved :
execGroup.getValue().toolchainTypeToResolved().asMap().entrySet()) {
// map entries from (exec group, toolchain type) to resolved toolchain labels. We need to
// distinguish the resolved toolchains per type because aspects propagate on toolchains
// based on the types specified in `toolchains_aspects`. So even if 2 types resolved to the
// same toolchain target, their CT will be different if an aspect propagates to one type but
// not the other.
outgoingLabels.putAll(
DependencyKind.forBaseTargetExecGroup(
execGroup.getKey(), toolchainTypeToResolved.getKey().typeLabel()),
toolchainTypeToResolved.getValue());
}
}
}
private static void resolveAttributes(
Iterable<AttributeDependencyKind> attributeDependencyKinds,
OrderedSetMultimap<DependencyKind, Label> outgoingLabels,
Rule rule,
ConfiguredAttributeMapper attributeMap,
BuildConfigurationValue ruleConfig)
throws InterruptedException {
for (AttributeDependencyKind dependencyKind : attributeDependencyKinds) {
Attribute attribute = dependencyKind.getAttribute();
// Not only is resolving CONFIG_SETTING_DEPS_ATTRIBUTE deps here wasteful, since the only
// place they're used is in ConfiguredTargetFunction.getConfigConditions, but it actually
// breaks trimming as shown by
// FeatureFlagManualTrimmingTest#featureFlagInUnusedSelectBranchButNotInTransitiveConfigs_DoesNotError
// because it resolves a dep that trimming (correctly) doesn't account for because it's part
// of an unchosen select() branch.
if (attribute.getName().equals(RuleClass.CONFIG_SETTING_DEPS_ATTRIBUTE)) {
continue;
}
Type<?> type = attribute.getType();
if (type == BuildType.OUTPUT
|| type == BuildType.OUTPUT_LIST
|| type == BuildType.NODEP_LABEL
|| type == BuildType.NODEP_LABEL_LIST
|| type == BuildType.DORMANT_LABEL
|| type == BuildType.DORMANT_LABEL_LIST
|| type == BuildType.GENQUERY_SCOPE_TYPE
|| type == BuildType.GENQUERY_SCOPE_TYPE_LIST) {
// These types invoke visitLabels() so that they are reported in "bazel query" but do not
// create a dependency. Maybe it's better to remove that, but then the labels() query
// function would need to be rethought.
continue;
}
resolveAttribute(
attribute, type, dependencyKind, outgoingLabels, rule, attributeMap, ruleConfig);
}
}
private static <T> void resolveAttribute(
Attribute attribute,
Type<T> type,
AttributeDependencyKind dependencyKind,
OrderedSetMultimap<DependencyKind, Label> outgoingLabels,
Rule rule,
ConfiguredAttributeMapper attributeMap,
BuildConfigurationValue ruleConfig)
throws InterruptedException {
T attributeValue = null;
if (attribute.isImplicit()) {
// Since the attributes that come from aspects do not appear in attributeMap, we have to get
// their values from somewhere else. This incidentally means that aspects attributes are not
// configurable. It would be nice if that wasn't the case, but we'd have to revamp how
// attribute mapping works, which is a large chunk of work.
if (dependencyKind.getOwningAspect() == null) {
attributeValue = attributeMap.get(attribute.getName(), type);
} else {
Object defaultValue = attribute.getDefaultValue(rule);
attributeValue =
type.cast(
defaultValue instanceof ComputedDefault computedDefault
? computedDefault.getDefault(attributeMap)
: defaultValue);
}
} else if (attribute.isMaterializing()) {
// These attributes are resolved by calling the materializer function in
// DependencyMapProducer. The reason is that they need the analyzed versions some direct
// dependencies and we can't do that here.
outgoingLabels.put(dependencyKind, null);
} else if (attribute.isLateBound()) {
attributeValue =
type.cast(resolveLateBoundDefault(rule, attributeMap, attribute, ruleConfig));
} else if (dependencyKind.getOwningAspect() == null && attributeMap.has(attribute.getName())) {
// This condition is false for aspect attributes that do not give rise to dependencies because
// attributes that come from aspects do not appear in attributeMap (see the comment in the
// case that handles implicit attributes).
attributeValue = attributeMap.get(attribute.getName(), type);
}
if (attributeValue == null) {
return;
}
type.visitLabels(
(depLabel, ctx) -> outgoingLabels.put(dependencyKind, depLabel),
attributeValue,
/*context=*/ null);
}
@Nullable
@VisibleForTesting(/* used to test LateBoundDefaults' default values */ )
static <FragmentT> Object resolveLateBoundDefault(
Rule rule, AttributeMap attributeMap, Attribute attribute, BuildConfigurationValue ruleConfig)
throws InterruptedException {
Preconditions.checkState(!attribute.getTransitionFactory().isSplit());
@SuppressWarnings("unchecked")
LateBoundDefault<FragmentT, ?> lateBoundDefault =
(LateBoundDefault<FragmentT, ?>) attribute.getLateBoundDefault();
Class<FragmentT> fragmentClass = lateBoundDefault.getFragmentClass();
try {
// TODO(b/65746853): remove this when nothing uses it anymore
if (BuildConfigurationValue.class.equals(fragmentClass)
// noconfig targets can't meaningfully parse late-bound defaults. See NoConfigTransition.
&& !ruleConfig.getOptions().hasNoConfig()) {
return lateBoundDefault.resolve(rule, attributeMap, fragmentClass.cast(ruleConfig));
}
if (Void.class.equals(fragmentClass)) {
return lateBoundDefault.resolve(
rule, attributeMap, /* input= */ null
/* analysisContext= */
/* eventHandler= */ );
}
@SuppressWarnings("unchecked")
FragmentT fragment =
fragmentClass.cast(ruleConfig.getFragment((Class<? extends Fragment>) fragmentClass));
if (fragment == null) {
return null;
}
return lateBoundDefault.resolve(
rule, attributeMap, fragment
/* analysisContext= */
/* eventHandler= */ );
} catch (EvalException e) {
// Materializers should not be called here and those are the only kind of late-bound defaults
// that can throw these exceptions.
throw new IllegalStateException(e);
}
}
/**
* Adds new dependencies to the given rule under the given attribute name
*
* @param attrName the name of the attribute to add dependency labels to
* @param labels the dependencies to add
*/
private static void addExplicitDeps(
OrderedSetMultimap<DependencyKind, Label> outgoingLabels,
Rule rule,
String attrName,
Iterable<Label> labels) {
if (!rule.isAttrDefined(attrName, BuildType.LABEL_LIST)
&& !rule.isAttrDefined(attrName, BuildType.NODEP_LABEL_LIST)) {
return;
}
Attribute attribute = rule.getRuleClassObject().getAttributeByName(attrName);
outgoingLabels.putAll(AttributeDependencyKind.forRule(attribute), labels);
}
/** Returns the attributes that should be visited for this rule/aspect combination. */
private static ImmutableList<AttributeDependencyKind> getAttributes(
Rule rule, ImmutableList<Aspect> aspects) {
ImmutableList.Builder<AttributeDependencyKind> result = ImmutableList.builder();
HashSet<String> ruleAndBaseAspectsProcessedAttributes = new HashSet<>();
// For aspects evaluation, all attributes of the main aspect (last aspect in {@code aspects}
// should be added, even if they have the same name as an attribute in the rule or a base aspect
// because main aspect attributes are separated and retrieved from `ctx.attr`.
// Attributes of the underlying rule and base aspects are merged and retrieved from
// `ctx.rule.attr` with rule attributes taking precedence then aspects' attributes based on the
// aspect order in the aspects path (lowest order to highest).
List<Attribute> ruleAttributes = rule.getRuleClassObject().getAttributes();
for (Attribute attribute : ruleAttributes) {
result.add(AttributeDependencyKind.forRule(attribute));
ruleAndBaseAspectsProcessedAttributes.add(attribute.getName());
}
addAspectAttributes(aspects, ruleAndBaseAspectsProcessedAttributes, result);
return result.build();
}
private static ImmutableList<AttributeDependencyKind> getAspectAttributes(
ImmutableList<Aspect> aspects) {
ImmutableList.Builder<AttributeDependencyKind> result = ImmutableList.builder();
addAspectAttributes(aspects, new HashSet<>(), result);
return result.build();
}
private static void addAspectAttributes(
ImmutableList<Aspect> aspects,
Set<String> processedAttributes,
ImmutableList.Builder<AttributeDependencyKind> attributes) {
if (aspects.isEmpty()) {
return;
}
// Add all the main aspect's attributes
Aspect mainAspect = Iterables.getLast(aspects, null);
for (Attribute attribute : mainAspect.getDefinition().getAttributes().values()) {
attributes.add(AttributeDependencyKind.forAspect(attribute, mainAspect.getAspectClass()));
}
// For base aspects, if multiple attributes have the same name, take the first encountered in
// the aspects path.
for (Aspect aspect : aspects.subList(0, aspects.size() - 1)) {
for (Attribute attribute : aspect.getDefinition().getAttributes().values()) {
if (processedAttributes.add(attribute.getName())) {
attributes.add(AttributeDependencyKind.forAspect(attribute, aspect.getAspectClass()));
}
}
}
}
private static void addVisibilityDepLabels(
Iterable<Label> labels, OrderedSetMultimap<DependencyKind, Label> outgoingLabels) {
outgoingLabels.putAll(VISIBILITY_DEPENDENCY, labels);
}
}