// 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 com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Predicates; import com.google.common.base.Verify; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.packages.AbstractAttributeMapper; import com.google.devtools.build.lib.packages.Attribute; import com.google.devtools.build.lib.packages.AttributeMap; import com.google.devtools.build.lib.packages.BuildType; import com.google.devtools.build.lib.packages.BuildType.Selector; import com.google.devtools.build.lib.packages.BuildType.SelectorList; import com.google.devtools.build.lib.packages.Rule; import com.google.devtools.build.lib.syntax.EvalException; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.Preconditions; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * {@link AttributeMap} implementation that binds a rule's attribute as follows: * * <ol> * <li>If the attribute is selectable (i.e. its BUILD declaration is of the form * "attr = { config1: "value1", "config2: "value2", ... }", returns the subset of values * chosen by the current configuration in accordance with Bazel's documented policy on * configurable attribute selection. * <li>If the attribute is not selectable (i.e. its value is static), returns that value with * no additional processing. * </ol> * * <p>Example usage: * <pre> * Label fooLabel = ConfiguredAttributeMapper.of(ruleConfiguredTarget).get("foo", Type.LABEL); * </pre> */ public class ConfiguredAttributeMapper extends AbstractAttributeMapper { private final Map<Label, ConfigMatchingProvider> configConditions; private Rule rule; private ConfiguredAttributeMapper(Rule rule, ImmutableMap<Label, ConfigMatchingProvider> configConditions) { super(Preconditions.checkNotNull(rule).getPackage(), rule.getRuleClassObject(), rule.getLabel(), rule.getAttributeContainer()); this.configConditions = configConditions; this.rule = rule; } /** * "Do-it-all" constructor that just needs a {@link RuleConfiguredTarget}. */ public static ConfiguredAttributeMapper of(RuleConfiguredTarget ct) { return new ConfiguredAttributeMapper(ct.getTarget(), ct.getConfigConditions()); } /** * "Manual" constructor that requires the caller to pass the set of configurability conditions * that trigger this rule's configurable attributes. * * <p>If you don't know how to do this, you really want to use one of the "do-it-all" * constructors. */ @VisibleForTesting public static ConfiguredAttributeMapper of( Rule rule, ImmutableMap<Label, ConfigMatchingProvider> configConditions) { return new ConfiguredAttributeMapper(rule, configConditions); } /** * Checks that all attributes can be mapped to their configured values. This is * useful for checking that the configuration space in a configured attribute doesn't * contain unresolvable contradictions. * * @throws EvalException if any attribute's value can't be resolved under this mapper */ public void validateAttributes() throws EvalException { for (String attrName : getAttributeNames()) { getAndValidate(attrName, getAttributeType(attrName)); } } /** * Variation of {@link #get} that throws an informative exception if the attribute * can't be resolved due to intrinsic contradictions in the configuration. */ private <T> T getAndValidate(String attributeName, Type<T> type) throws EvalException { SelectorList<T> selectorList = getSelectorList(attributeName, type); if (selectorList == null) { // This is a normal attribute. return super.get(attributeName, type); } List<T> resolvedList = new ArrayList<>(); for (Selector<T> selector : selectorList.getSelectors()) { ConfigKeyAndValue<T> resolvedPath = resolveSelector(attributeName, selector); if (!selector.isValueSet(resolvedPath.configKey)) { // Use the default. We don't have access to the rule here, so pass null to // Attribute.getValue(). This has the result of making attributes with condition // predicates ineligible for "None" values. But no user-facing attributes should // do that anyway, so that isn't a loss. Attribute attr = getAttributeDefinition(attributeName); Verify.verify(attr.getCondition() == Predicates.<AttributeMap>alwaysTrue()); resolvedList.add((T) attr.getDefaultValue(null)); } else { resolvedList.add(resolvedPath.value); } } return resolvedList.size() == 1 ? resolvedList.get(0) : type.concat(resolvedList); } private static class ConfigKeyAndValue<T> { Label configKey; T value; ConfigKeyAndValue(Label key, T value) { this.configKey = key; this.value = value; } } private <T> ConfigKeyAndValue<T> resolveSelector(String attributeName, Selector<T> selector) throws EvalException { Map<ConfigMatchingProvider, ConfigKeyAndValue<T>> matchingConditions = new LinkedHashMap<>(); Set<Label> conditionLabels = new LinkedHashSet<>(); ConfigKeyAndValue<T> matchingResult = null; // Find the matching condition and record its value (checking for duplicates). for (Map.Entry<Label, T> entry : selector.getEntries().entrySet()) { Label selectorKey = entry.getKey(); if (BuildType.Selector.isReservedLabel(selectorKey)) { continue; } ConfigMatchingProvider curCondition = configConditions.get( rule.getLabel().resolveRepositoryRelative(selectorKey)); if (curCondition == null) { // This can happen if the rule is in error continue; } conditionLabels.add(curCondition.label()); if (curCondition.matches()) { // We keep track of all matches which are more precise than any we have found so far. // Therefore, we remove any previous matches which are strictly less precise than this // one, and only add this one if none of the previous matches are more precise. // It is an error if we do not end up with only one most-precise match. boolean suppressed = false; Iterator<ConfigMatchingProvider> it = matchingConditions.keySet().iterator(); while (it.hasNext()) { ConfigMatchingProvider existingMatch = it.next(); if (curCondition.refines(existingMatch)) { it.remove(); } else if (existingMatch.refines(curCondition)) { suppressed = true; break; } } if (!suppressed) { matchingConditions.put( curCondition, new ConfigKeyAndValue<>(selectorKey, entry.getValue())); } } } if (matchingConditions.size() > 1) { throw new EvalException( rule.getAttributeLocation(attributeName), "Illegal ambiguous match on configurable attribute \"" + attributeName + "\" in " + getLabel() + ":\n" + Joiner.on("\n").join(matchingConditions.keySet()) + "\nMultiple matches are not allowed unless one is unambiguously more specialized."); } else if (matchingConditions.size() == 1) { matchingResult = Iterables.getOnlyElement(matchingConditions.values()); } // If nothing matched, choose the default condition. if (matchingResult == null) { if (!selector.hasDefault()) { String noMatchMessage = "Configurable attribute \"" + attributeName + "\" doesn't match this configuration"; if (!selector.getNoMatchError().isEmpty()) { noMatchMessage += ": " + selector.getNoMatchError(); } else { noMatchMessage += " (would a default condition help?).\nConditions checked:\n " + Joiner.on("\n ").join(conditionLabels); } throw new EvalException(rule.getAttributeLocation(attributeName), noMatchMessage); } matchingResult = selector.hasDefault() ? new ConfigKeyAndValue<>(Selector.DEFAULT_CONDITION_LABEL, selector.getDefault()) : null; } return matchingResult; } @Override public <T> T get(String attributeName, Type<T> type) { try { return getAndValidate(attributeName, type); } catch (EvalException e) { // Callers that reach this branch should explicitly validate the attribute through an // appropriate call and handle the exception directly. This method assumes // pre-validated attributes. throw new IllegalStateException( "lookup failed on attribute " + attributeName + ": " + e.getMessage()); } } @Override public boolean isAttributeValueExplicitlySpecified(String attributeName) { SelectorList<?> selectorList = getSelectorList(attributeName, getAttributeType(attributeName)); if (selectorList == null) { // This is a normal attribute. return super.isAttributeValueExplicitlySpecified(attributeName); } for (Selector<?> selector : selectorList.getSelectors()) { try { ConfigKeyAndValue<?> resolvedPath = resolveSelector(attributeName, selector); if (selector.isValueSet(resolvedPath.configKey)) { return true; } } catch (EvalException e) { // This will trigger an error via any other call, so the actual return doesn't matter much. return true; } } return false; // Every select() in this list chooses a path with value "None". } }