blob: b2fcf4bb52bdb7c7ad33c2dbaccfb1828d7a13f3 [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.packages;
import static com.google.devtools.build.lib.packages.BuildType.DISTRIBUTIONS;
import static com.google.devtools.build.lib.packages.BuildType.FILESET_ENTRY_LIST;
import static com.google.devtools.build.lib.packages.BuildType.LABEL;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_DICT_UNARY;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static com.google.devtools.build.lib.packages.BuildType.LICENSE;
import static com.google.devtools.build.lib.packages.BuildType.NODEP_LABEL;
import static com.google.devtools.build.lib.packages.BuildType.NODEP_LABEL_LIST;
import static com.google.devtools.build.lib.packages.BuildType.OUTPUT;
import static com.google.devtools.build.lib.packages.BuildType.OUTPUT_LIST;
import static com.google.devtools.build.lib.packages.BuildType.TRISTATE;
import static com.google.devtools.build.lib.syntax.Type.BOOLEAN;
import static com.google.devtools.build.lib.syntax.Type.INTEGER;
import static com.google.devtools.build.lib.syntax.Type.INTEGER_LIST;
import static com.google.devtools.build.lib.syntax.Type.STRING;
import static com.google.devtools.build.lib.syntax.Type.STRING_DICT;
import static com.google.devtools.build.lib.syntax.Type.STRING_DICT_UNARY;
import static com.google.devtools.build.lib.syntax.Type.STRING_LIST;
import static com.google.devtools.build.lib.syntax.Type.STRING_LIST_DICT;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.CollectionUtils;
import com.google.devtools.build.lib.packages.Attribute.ComputationLimiter;
import com.google.devtools.build.lib.packages.BuildType.Selector;
import com.google.devtools.build.lib.packages.BuildType.SelectorList;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.Preconditions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
/**
* {@link AttributeMap} implementation that provides the ability to retrieve <i>all possible</i>
* values an attribute might take.
*/
public class AggregatingAttributeMapper extends AbstractAttributeMapper {
@SuppressWarnings("unchecked")
private static final ImmutableSet<Type<?>> scalarTypes =
ImmutableSet.of(INTEGER, STRING, LABEL, NODEP_LABEL, OUTPUT, BOOLEAN, TRISTATE, LICENSE);
private final Rule rule;
private AggregatingAttributeMapper(Rule rule) {
super(rule.getPackage(), rule.getRuleClassObject(), rule.getLabel(),
rule.getAttributeContainer());
this.rule = rule;
}
public static AggregatingAttributeMapper of(Rule rule) {
return new AggregatingAttributeMapper(rule);
}
/**
* Returns all of this rule's attributes that are non-configurable. These are unconditionally
* available to computed defaults no matter what dependencies they've declared.
*/
private List<String> getNonConfigurableAttributes() {
return rule.getRuleClassObject().getNonConfigurableAttributes();
}
/**
* Override that also visits the rule's configurable attribute keys (which are themselves labels).
*
* <p>Note that we directly parse the selectors rather than just calling {@link #visitAttribute}
* to iterate over all possible values. That's because {@link #visitAttribute} can grow
* exponentially with respect to the number of selects (e.g. if an attribute uses three selects
* with three conditions each, it can take nine possible values). So we want to avoid that code
* path whenever actual value iteration isn't specifically needed.
*/
@Override
protected void visitLabels(Attribute attribute, AcceptsLabelAttribute observer)
throws InterruptedException {
visitLabels(attribute, true, observer);
}
private void visitLabels(
final Attribute attribute, boolean includeSelectKeys, final AcceptsLabelAttribute observer)
throws InterruptedException {
Type<?> type = attribute.getType();
SelectorList<?> selectorList = getSelectorList(attribute.getName(), type);
if (selectorList == null) {
if (getComputedDefault(attribute.getName(), attribute.getType()) != null) {
// Computed defaults are a special pain: we have no choice but to iterate through their
// (computed) values and look for labels.
for (Object value : visitAttribute(attribute.getName(), attribute.getType())) {
if (value != null) {
type.visitLabels(new Type.LabelVisitor() {
@Override
public void visit(@Nullable Label label) throws InterruptedException {
if (label != null) {
observer.acceptLabelAttribute(
getLabel().resolveRepositoryRelative(label), attribute);
}
}
}, value);
}
}
} else {
super.visitLabels(attribute, observer);
}
} else {
for (Selector<?> selector : selectorList.getSelectors()) {
for (Map.Entry<Label, ?> selectorEntry : selector.getEntries().entrySet()) {
if (includeSelectKeys && !BuildType.Selector.isReservedLabel(selectorEntry.getKey())) {
observer.acceptLabelAttribute(
getLabel().resolveRepositoryRelative(selectorEntry.getKey()), attribute);
}
Object value = selector.isValueSet(selectorEntry.getKey())
? selectorEntry.getValue()
: attribute.getDefaultValue(null);
type.visitLabels(new Type.LabelVisitor() {
@Override
public void visit(@Nullable Label label) throws InterruptedException {
if (label != null) {
observer.acceptLabelAttribute(
getLabel().resolveRepositoryRelative(label), attribute);
}
}
}, value);
}
}
}
}
/**
* Returns all labels reachable via the given attribute. If a label is listed multiple times, each
* instance appears in the returned list.
*
* @param includeSelectKeys whether to include config_setting keys for configurable attributes
*/
public List<Label> getReachableLabels(String attributeName, boolean includeSelectKeys)
throws InterruptedException {
final ImmutableList.Builder<Label> builder = ImmutableList.builder();
visitLabels(getAttributeDefinition(attributeName), includeSelectKeys,
new AcceptsLabelAttribute() {
@Override
public void acceptLabelAttribute(Label label, Attribute attribute) {
builder.add(label);
}
});
return builder.build();
}
/**
* Returns the labels that might appear multiple times in the same attribute value.
*/
public Set<Label> checkForDuplicateLabels(Attribute attribute) {
String attrName = attribute.getName();
Type<?> attrType = attribute.getType();
ImmutableSet.Builder<Label> duplicates = ImmutableSet.builder();
SelectorList<?> selectorList = getSelectorList(attribute.getName(), attrType);
if (selectorList == null || selectorList.getSelectors().size() == 1) {
// Three possible scenarios:
// 1) Plain old attribute (no selects). Without selects, visitAttribute runs efficiently.
// 2) Computed default, possibly depending on other attributes using select. In this case,
// visitAttribute might be inefficient. But we have no choice but to iterate over all
// possible values (since we have to compute them), so we take the efficiency hit.
// 3) "attr = select({...})". With just a single select, visitAttribute runs efficiently.
for (Object value : visitAttribute(attrName, attrType)) {
if (value != null) {
// TODO(bazel-team): Calculate duplicates directly using attrType.visitLabels in order to
// avoid intermediate collections here.
duplicates.addAll(CollectionUtils.duplicatedElementsOf(extractLabels(attrType, value)));
}
}
} else {
// Multiple selects concatenated together. It's expensive to iterate over every possible
// value, so instead collect all labels across all the selects and check for duplicates.
// This is overly strict, since this counts duplicates across values. We can presumably
// relax this if necessary, but doing so would incur the value iteration expense this
// code path avoids.
List<Label> combinedLabels = new LinkedList<>(); // Labels that appear across all selectors.
for (Selector<?> selector : selectorList.getSelectors()) {
// Labels within a single selector. It's okay for there to be duplicates as long as
// they're in different selector paths (since only one path can actually get chosen).
Set<Label> selectorLabels = new LinkedHashSet<>();
for (Object selectorValue : selector.getEntries().values()) {
List<Label> labelsInSelectorValue = extractLabels(attrType, selectorValue);
// Duplicates within a single path are not okay.
duplicates.addAll(CollectionUtils.duplicatedElementsOf(labelsInSelectorValue));
Iterables.addAll(selectorLabels, labelsInSelectorValue);
}
combinedLabels.addAll(selectorLabels);
}
duplicates.addAll(CollectionUtils.duplicatedElementsOf(combinedLabels));
}
return duplicates.build();
}
/**
* Returns a list of the possible values of the specified attribute in the specified rule.
*
* <p>If the attribute's value is a simple value, then this returns a singleton list of that
* value.
*
* <p>If the attribute's value is an expression containing one or many {@code select(...)}
* expressions, then this returns a list of all values that expression may evaluate to.
*
* <p>If the attribute does not have an explicit value for this rule, and the rule provides a
* computed default, the computed default function is evaluated given the rule's other attribute
* values as inputs and the output is returned in a singleton list.
*
* <p>If the attribute does not have an explicit value for this rule, and the rule provides a
* computed default, and the computed default function depends on other attributes whose values
* contain {@code select(...)} expressions, then the computed default function is evaluated for
* every possible combination of input values, and the list of outputs is returned.
*/
public Iterable<Object> getPossibleAttributeValues(Rule rule, Attribute attr) {
// Values may be null, so use normal collections rather than immutable collections.
// This special case for the visibility attribute is needed because its value is replaced
// with an empty list during package loading if it is public or private in order not to visit
// the package called 'visibility'.
if (attr.getName().equals("visibility")) {
List<Object> result = new ArrayList<>(1);
result.add(rule.getVisibility().getDeclaredLabels());
return result;
}
return Lists.<Object>newArrayList(visitAttribute(attr.getName(), attr.getType()));
}
/**
* Coerces the list {@param possibleValues} of values of type {@param attrType} to a single
* value of that type, in the following way:
*
* <p>If the list contains a single value, return that value.
*
* <p>If the list contains zero or multiple values and the type is a scalar type, return {@code
* null}.
*
* <p>If the list contains zero or multiple values and the type is a collection or map type,
* merge the collections/maps in the list and return the merged collection/map.
*/
@Nullable
@SuppressWarnings("unchecked")
public static Object flattenAttributeValues(Type<?> attrType, Iterable<Object> possibleValues) {
// If there is only one possible value, return it.
if (Iterables.size(possibleValues) == 1) {
return Iterables.getOnlyElement(possibleValues);
}
// Otherwise, there are multiple possible values. To conform to the message shape expected by
// query output's clients, we must transform the list of possible values. This transformation
// will be lossy, but this is the best we can do.
// If the attribute's type is not a collection type, return null. Query output's clients do
// not support list values for scalar attributes.
if (scalarTypes.contains(attrType)) {
return null;
}
// If the attribute's type is a collection type, merge the list of collections into a single
// collection. This is a sensible solution for query output's clients, which are happy to get
// the union of possible values.
if (attrType == STRING_LIST
|| attrType == LABEL_LIST
|| attrType == NODEP_LABEL_LIST
|| attrType == OUTPUT_LIST
|| attrType == DISTRIBUTIONS
|| attrType == INTEGER_LIST
|| attrType == FILESET_ENTRY_LIST) {
Builder<Object> builder = ImmutableList.builder();
for (Object possibleValue : possibleValues) {
Collection<Object> collection = (Collection<Object>) possibleValue;
for (Object o : collection) {
builder.add(o);
}
}
return builder.build();
}
// Same for maps as for collections.
if (attrType == STRING_DICT
|| attrType == STRING_DICT_UNARY
|| attrType == STRING_LIST_DICT
|| attrType == LABEL_DICT_UNARY) {
Map<Object, Object> mergedDict = new HashMap<>();
for (Object possibleValue : possibleValues) {
Map<Object, Object> stringDict = (Map<Object, Object>) possibleValue;
for (Entry<Object, Object> entry : stringDict.entrySet()) {
mergedDict.put(entry.getKey(), entry.getValue());
}
}
return mergedDict;
}
throw new AssertionError("Unknown type: " + attrType);
}
/**
* Returns a list of all possible values an attribute can take for this rule.
*
* <p>Note that when an attribute uses multiple selects, or is a {@link Attribute.ComputedDefault}
* that depends on configurable attributes, it can potentially take on many values. So be cautious
* about unnecessarily relying on this method.
*/
public <T> Iterable<T> visitAttribute(String attributeName, Type<T> type) {
// If this attribute value is configurable, visit all possible values.
SelectorList<T> selectorList = getSelectorList(attributeName, type);
if (selectorList != null) {
ImmutableList.Builder<T> builder = ImmutableList.builder();
visitConfigurableAttribute(selectorList.getSelectors(), new BoundSelectorPaths(), type,
null, builder);
return builder.build();
}
// If this attribute is a computed default, feed it all possible value combinations of
// its declared dependencies and return all computed results. For example, if this default
// uses attributes x and y, x can configurably be x1 or x2, and y can configurably be y1
// or y1, then compute default values for the (x1,y1), (x1,y2), (x2,y1), and (x2,y2) cases.
Attribute.ComputedDefault computedDefault = getComputedDefault(attributeName, type);
if (computedDefault != null) {
return computedDefault.getPossibleValues(type, rule);
}
// For any other attribute, just return its direct value.
T value = get(attributeName, type);
return value == null ? ImmutableList.<T>of() : ImmutableList.of(value);
}
/**
* Determines all possible values a configurable attribute can take. Do not call this method
* unless really necessary (see TODO comment inside).
*
* @param selectors the selectors that make up this attribute assignment (in order)
* @param boundSelectorPaths paths that have already been chosen from previous selectors in an
* earlier recursive call of this method. For example, given
* <pre>cmd = select({':a': 'w', ':b': 'x'}) + select({':a': 'y', ':b': 'z'})</pre>
* the only possible values for <code>cmd</code> are <code>"wy"</code> and <code>"xz"</code>.
* This is because the selects have the same conditions, so whatever matches the first also
* matches the second. Note that this doesn't work for selects with overlapping but
* <i>different</i> key sets. That's because of key specialization (see
* {@link com.google.devtools.build.lib.analysis.ConfiguredAttributeMapper} - if the
* second select also included a condition <code>':c'</code> that includes both the flags
* in <code>':a'</code> and <code>':b'</code>, <code>':c'</code> would be chosen over
* them both.
* @param type the type of this attribute
* @param currentValueSoFar the partial value produced so far from earlier calls to this method
* @param valuesBuilder output container for full values this attribute can take
*/
private <T> void visitConfigurableAttribute(List<Selector<T>> selectors,
BoundSelectorPaths boundSelectorPaths, Type<T> type, T currentValueSoFar,
ImmutableList.Builder<T> valuesBuilder) {
// TODO(bazel-team): minimize or eliminate uses of this interface. It necessarily grows
// exponentially with the number of selects in the attribute. Is that always necessary?
// For example, dependency resolution just needs to know every possible label an attribute
// might reference, but it doesn't need to know the exact combination of labels that make
// up a value. This may be even less important for non-label values (e.g. strings), which
// have no impact on the dependency structure.
if (selectors.isEmpty()) {
if (currentValueSoFar != null) {
// Null values arise when a None is used as the value of a Selector for a type without a
// default value.
// TODO(gregce): visitAttribute should probably convey that an unset attribute is possible.
// Therefore we need to actually handle null values here.
valuesBuilder.add(currentValueSoFar);
}
} else {
Selector<T> firstSelector = selectors.get(0);
List<Selector<T>> remainingSelectors = selectors.subList(1, selectors.size());
Map<Label, T> firstSelectorEntries = firstSelector.getEntries();
Label boundKey = boundSelectorPaths.getChosenKey(firstSelectorEntries.keySet());
if (boundKey != null) {
// If we've already followed some path from a previous selector with the same exact
// conditions as this one, we only need to visit that path (since the same key will
// match both selectors).
T boundValue = firstSelectorEntries.get(boundKey);
visitConfigurableAttribute(remainingSelectors, boundSelectorPaths, type,
currentValueSoFar == null
? boundValue
: type.concat(ImmutableList.of(currentValueSoFar, boundValue)),
valuesBuilder);
} else {
// Otherwise, we need to iterate over all possible paths.
for (Map.Entry<Label, T> selectorBranch : firstSelectorEntries.entrySet()) {
// Bind this particular path for later selectors using the same conditions.
boundSelectorPaths.bind(firstSelectorEntries.keySet(), selectorBranch.getKey());
visitConfigurableAttribute(remainingSelectors, boundSelectorPaths, type,
currentValueSoFar == null
? selectorBranch.getValue()
: type.concat(ImmutableList.of(currentValueSoFar, selectorBranch.getValue())),
valuesBuilder);
// Unbind the path (so when we pop back up the recursive stack we can rebind it to new
// values if we visit this selector again).
boundSelectorPaths.unbind(firstSelectorEntries.keySet());
}
}
}
}
/**
* Helper class for {@link #visitConfigurableAttribute}. See that method's comments for more
* details.
*/
private static class BoundSelectorPaths {
private final Map<Set<Label>, Label> bindings = new HashMap<>();
/**
* Binds the given config key set to the specified path. There should be no previous binding
* for this key set.
*/
public void bind(Set<Label> allKeys, Label chosenKey) {
Preconditions.checkState(allKeys.contains(chosenKey));
Verify.verify(bindings.put(allKeys, chosenKey) == null);
}
/**
* Unbinds the given config key set.
*/
public void unbind(Set<Label> allKeys) {
Verify.verifyNotNull(bindings.remove(allKeys));
}
/**
* Returns the key this config key set is bound to or null if no binding.
*/
public Label getChosenKey(Set<Label> allKeys) {
return bindings.get(allKeys);
}
}
/**
* Given a list of attributes, creates an {attrName -> attrValue} map for every possible
* combination of those attributes' values and returns a list of all the maps.
*
* <p>For example, given attributes x and y, which respectively have possible values x1, x2 and
* y1, y2, this returns:
*
* <pre>
* [
* {x: x1, y: y1},
* {x: x1, y: y2},
* {x: x2, y: y1},
* {x: x2, y: y2}
* ]
* </pre>
*
* <p>The work done by this method may be limited by providing a {@link ComputationLimiter} that
* throws if too much work is attempted.
*/
<TException extends Exception> List<Map<String, Object>> visitAttributes(
List<String> attributes, ComputationLimiter<TException> limiter) throws TException {
List<Map<String, Object>> depMaps = new LinkedList<>();
AtomicInteger combinationsSoFar = new AtomicInteger(0);
visitAttributesInner(
attributes, depMaps, ImmutableMap.<String, Object>of(), combinationsSoFar, limiter);
return depMaps;
}
/**
* A recursive function used in the implementation of {@link #visitAttributes}.
*
* @param attributes a list of attributes that are not yet assigned values in the {@code
* currentMap} parameter.
* @param mappings a mutable list of {attrName --> attrValue} maps collected so far. This method
* will add newly discovered maps to the list.
* @param currentMap a (possibly non-empty) map holding {attrName --> attrValue} assignments for
* attributes not in the {@code attributes} list.
* @param combinationsSoFar a counter for all previously processed combinations of possible
* values.
* @param limiter a strategy to limit the work done by invocations of this method.
*/
private <TException extends Exception> void visitAttributesInner(
List<String> attributes,
List<Map<String, Object>> mappings,
Map<String, Object> currentMap,
AtomicInteger combinationsSoFar,
ComputationLimiter<TException> limiter)
throws TException {
if (attributes.isEmpty()) {
// Because this method uses exponential time/space on the number of inputs, we may limit
// the total number of method calls.
limiter.onComputationCount(combinationsSoFar.incrementAndGet());
// Recursive base case: store whatever's already been populated in currentMap.
mappings.add(currentMap);
return;
}
// Take the first attribute in the dependency list and iterate over all its values. For each
// value x, copy currentMap with the additional entry { firstAttrName: x }, then feed
// this recursively into a subcall over all remaining dependencies. This recursively
// continues until we run out of values.
String firstAttribute = attributes.get(0);
Iterable<?> firstAttributePossibleValues =
visitAttribute(firstAttribute, getAttributeType(firstAttribute));
for (Object value : firstAttributePossibleValues) {
Map<String, Object> newMap = new HashMap<>();
newMap.putAll(currentMap);
newMap.put(firstAttribute, value);
visitAttributesInner(
attributes.subList(1, attributes.size()), mappings, newMap, combinationsSoFar, limiter);
}
}
/**
* Returns an {@link AttributeMap} that delegates to {@code AggregatingAttributeMapper.this}
* except for {@link #get} calls for attributes that are configurable. In that case, the {@link
* AttributeMap} looks up an attribute's value in {@code directMap}. Any attempt to {@link #get} a
* configurable attribute that's not in {@code directMap} causes an {@link
* IllegalArgumentException} to be thrown.
*/
AttributeMap createMapBackedAttributeMap(final Map<String, Object> directMap) {
final AggregatingAttributeMapper owner = AggregatingAttributeMapper.this;
return new AttributeMap() {
@Override
public <T> T get(String attributeName, Type<T> type) {
owner.checkType(attributeName, type);
if (getNonConfigurableAttributes().contains(attributeName)) {
return owner.get(attributeName, type);
}
if (!directMap.containsKey(attributeName)) {
throw new IllegalArgumentException(
"attribute \""
+ attributeName
+ "\" isn't available in this computed default context");
}
return type.cast(directMap.get(attributeName));
}
@Override
public <T> boolean isConfigurable(String attributeName, Type<T> type) {
return owner.isConfigurable(attributeName, type);
}
@Override
public String getName() {
return owner.getName();
}
@Override
public Label getLabel() {
return owner.getLabel();
}
@Override
public Iterable<String> getAttributeNames() {
return ImmutableList.<String>builder()
.addAll(directMap.keySet())
.addAll(getNonConfigurableAttributes())
.build();
}
@Override
public void visitLabels(AcceptsLabelAttribute observer) throws InterruptedException {
owner.visitLabels(observer);
}
@Override
public String getPackageDefaultHdrsCheck() {
return owner.getPackageDefaultHdrsCheck();
}
@Override
public Boolean getPackageDefaultTestOnly() {
return owner.getPackageDefaultTestOnly();
}
@Override
public String getPackageDefaultDeprecation() {
return owner.getPackageDefaultDeprecation();
}
@Override
public ImmutableList<String> getPackageDefaultCopts() {
return owner.getPackageDefaultCopts();
}
@Nullable
@Override
public Type<?> getAttributeType(String attrName) {
return owner.getAttributeType(attrName);
}
@Nullable
@Override
public Attribute getAttributeDefinition(String attrName) {
return owner.getAttributeDefinition(attrName);
}
@Override
public boolean isAttributeValueExplicitlySpecified(String attributeName) {
return owner.isAttributeValueExplicitlySpecified(attributeName);
}
@Override
public boolean has(String attrName, Type<?> type) {
return owner.has(attrName, type);
}
};
}
private static ImmutableList<Label> extractLabels(Type<?> type, Object value) {
try {
final ImmutableList.Builder<Label> result = ImmutableList.builder();
type.visitLabels(new Type.LabelVisitor() {
@Override
public void visit(@Nullable Label label) {
if (label != null) {
result.add(label);
}
}
}, value);
return result.build();
} catch (InterruptedException e) {
throw new IllegalStateException("Unexpected InterruptedException", e);
}
}
}