| // 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.common.base.Preconditions.checkArgument; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| 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.CoreOptions; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.collect.CollectionUtils; |
| import com.google.devtools.build.lib.packages.BuildType.Selector; |
| import com.google.devtools.build.lib.packages.BuildType.SelectorList; |
| 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; |
| import javax.annotation.Nullable; |
| |
| /** |
| * {@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 final String configHash; |
| private final boolean alwaysSucceed; |
| |
| private ConfiguredAttributeMapper( |
| Rule rule, |
| ImmutableMap<Label, ConfigMatchingProvider> configConditions, |
| String configHash, |
| boolean alwaysSucceed) { |
| super(Preconditions.checkNotNull(rule)); |
| this.configConditions = configConditions; |
| this.configHash = configHash; |
| this.alwaysSucceed = alwaysSucceed; |
| } |
| |
| /** |
| * "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. |
| */ |
| public static ConfiguredAttributeMapper of( |
| Rule rule, |
| ImmutableMap<Label, ConfigMatchingProvider> configConditions, |
| String configHash, |
| boolean alwaysSucceed) { |
| return new ConfiguredAttributeMapper(rule, configConditions, configHash, alwaysSucceed); |
| } |
| |
| /** |
| * "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. |
| */ |
| public static ConfiguredAttributeMapper of( |
| Rule rule, |
| ImmutableMap<Label, ConfigMatchingProvider> configConditions, |
| BuildConfigurationValue configuration) { |
| boolean alwaysSucceed = |
| configuration.getOptions().get(CoreOptions.class).debugSelectsAlwaysSucceed; |
| return of(rule, configConditions, configuration.checksum(), alwaysSucceed); |
| } |
| |
| /** |
| * 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 ValidationException if any attribute's value can't be resolved under this mapper |
| */ |
| public void validateAttributes() throws ValidationException { |
| for (String attrName : getAttributeNames()) { |
| getAndValidate(attrName, getAttributeType(attrName)); |
| } |
| } |
| |
| /** ValidationException indicates an error during attribute validation. */ |
| public static final class ValidationException extends Exception { |
| private ValidationException(String message) { |
| super(message); |
| } |
| } |
| |
| /** |
| * 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 ValidationException { |
| 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); |
| if (attr.isMandatory()) { |
| throw new ValidationException( |
| String.format( |
| "Mandatory attribute '%s' resolved to 'None' after evaluating 'select'" |
| + " expression", |
| attributeName)); |
| } |
| @SuppressWarnings("unchecked") |
| T defaultValue = (T) attr.getDefaultValue(); |
| resolvedList.add(defaultValue); |
| } else { |
| resolvedList.add(resolvedPath.value); |
| } |
| } |
| return resolvedList.size() == 1 ? resolvedList.get(0) : type.concat(resolvedList); |
| } |
| |
| private static class ConfigKeyAndValue<T> { |
| final Label configKey; |
| final T value; |
| /** If null, this means the default condition (doesn't correspond to a config_setting). */ |
| @Nullable final ConfigMatchingProvider provider; |
| |
| ConfigKeyAndValue(Label key, T value, @Nullable ConfigMatchingProvider provider) { |
| this.configKey = key; |
| this.value = value; |
| this.provider = provider; |
| } |
| } |
| |
| private <T> ConfigKeyAndValue<T> resolveSelector(String attributeName, Selector<T> selector) |
| throws ValidationException { |
| Map<Label, ConfigKeyAndValue<T>> matchingConditions = new LinkedHashMap<>(); |
| // Use a LinkedHashSet to guarantee deterministic error message ordering. We use a LinkedHashSet |
| // vs. a more general SortedSet because the latter supports insertion-order, which should more |
| // closely match how users see select() structures in BUILD files. |
| LinkedHashSet<Label> conditionLabels = new LinkedHashSet<>(); |
| |
| // Find the matching condition and record its value (checking for duplicates). |
| selector.forEach( |
| (selectorKey, value) -> { |
| if (BuildType.Selector.isDefaultConditionLabel(selectorKey)) { |
| return; |
| } |
| |
| ConfigMatchingProvider curCondition = configConditions.get(selectorKey); |
| if (curCondition == null) { |
| // This can happen if the rule is in error |
| return; |
| } |
| conditionLabels.add(selectorKey); |
| |
| 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<Map.Entry<Label, ConfigKeyAndValue<T>>> it = |
| matchingConditions.entrySet().iterator(); |
| while (it.hasNext()) { |
| ConfigMatchingProvider existingMatch = it.next().getValue().provider; |
| if (curCondition.refines(existingMatch)) { |
| it.remove(); |
| } else if (existingMatch.refines(curCondition)) { |
| suppressed = true; |
| break; |
| } |
| } |
| if (!suppressed) { |
| matchingConditions.put( |
| selectorKey, new ConfigKeyAndValue<>(selectorKey, value, curCondition)); |
| } |
| } |
| }); |
| |
| if (matchingConditions.size() > 1) { |
| throw new ValidationException( |
| "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) { |
| return Iterables.getOnlyElement(matchingConditions.values()); |
| } |
| |
| // If nothing matched, choose the default condition. |
| if (selector.hasDefault()) { |
| return new ConfigKeyAndValue<>(Selector.DEFAULT_CONDITION_LABEL, selector.getDefault(), null); |
| } |
| |
| // If we're in a debugging mode, set a fake default using the empty value for this select's |
| // type. |
| if (alwaysSucceed) { |
| return new ConfigKeyAndValue<>( |
| Selector.DEFAULT_CONDITION_LABEL, selector.getOriginalType().getDefaultValue(), null); |
| } |
| |
| throw new ValidationException( |
| noMatchError( |
| attributeName, selector.getNoMatchError(), conditionLabels, getLabel(), configHash)); |
| } |
| |
| /** |
| * Constructs a <a href="https://bazel.build/designs/2016/05/23/beautiful-error-messages.html"> |
| * beautiful error</a> for when no conditions in a configurable attribute match. |
| */ |
| private static String noMatchError( |
| String attribute, |
| String customNoMatchError, |
| LinkedHashSet<Label> conditionLabels, |
| Label targetLabel, |
| String configHash) { |
| String error = |
| String.format( |
| "configurable attribute \"%s\" in %s doesn't match this configuration", |
| attribute, targetLabel); |
| if (!customNoMatchError.isEmpty()) { |
| error += String.format(": %s\n", customNoMatchError); |
| } else { |
| error += |
| ". Would a default condition help?\n\n" |
| + "Conditions checked:\n " |
| + Joiner.on("\n ").join(conditionLabels) |
| + "\n\n" |
| + "To see a condition's definition, run: bazel query --output=build " |
| + "<condition label>.\n"; |
| } |
| // See ConfiguredTargetQueryEnvironment#shortID for the substring rationale. |
| String configShortHash = configHash.substring(0, 7); |
| error += |
| String.format( |
| "\nThis instance of %s has configuration identifier %s. " |
| + "To inspect its configuration, run: bazel config %s.\n", |
| targetLabel, configShortHash, configShortHash); |
| error += |
| "\n" |
| + "For more help, see" |
| + " https://bazel.build/docs/configurable-attributes#faq-select-choose-condition.\n\n"; |
| return error; |
| } |
| |
| @Override |
| public <T> T get(String attributeName, Type<T> type) { |
| try { |
| return getAndValidate(attributeName, type); |
| } catch (ValidationException 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 (ValidationException unused) { |
| // 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". |
| } |
| |
| /** Returns the labels that appear multiple times in the same attribute value. */ |
| public Set<Label> checkForDuplicateLabels(Attribute attribute) { |
| Type<List<Label>> attrType = BuildType.LABEL_LIST; |
| checkArgument(attribute.getType() == attrType, "Not a label list type: %s", attribute); |
| String attrName = attribute.getName(); |
| SelectorList<List<Label>> selectorList = getSelectorList(attrName, attrType); |
| // already checked in RuleClass via AggregatingAttributeMapper.checkForDuplicateLabels |
| if (selectorList == null || selectorList.getSelectors().size() == 1) { |
| return ImmutableSet.of(); |
| } |
| List<Label> labels = get(attrName, attrType); |
| return CollectionUtils.duplicatedElementsOf(labels); |
| } |
| } |