// Copyright 2015 Google Inc. 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.constraints; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.analysis.RuleConfiguredTarget; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.analysis.constraints.EnvironmentCollection.EnvironmentWithGroup; import com.google.devtools.build.lib.packages.AttributeMap; import com.google.devtools.build.lib.packages.EnvironmentGroup; 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.syntax.Label; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; /** * Implementation of the semantics of Bazel's constraint specification and enforcement system. * *

This is how the system works: * *

All build rules can declare which "environments" they can be built for, where an "environment" * is a label instance of an {@link EnvironmentRule} rule declared in a BUILD file. There are * various ways to do this: * *

* *

Groups exist to model the idea that some environments are related while others have nothing * to do with each other. Say, for example, we want to say a rule works for PowerPC platforms but * not x86. We can do so by setting its "restricted to" attribute to * {@code ['//sample/path:powerpc']}. Because both PowerPC and x86 are in the same * "target architectures" group, this setting removes x86 from the set of supported environments. * But since JDK support belongs to its own group ("JDK versions") it says nothing about which JDK * the rule supports. * *

More precisely, if a rule has a "restricted to" value of [A, B, C], this removes support * for all default environments D such that group(D) is in [group(A), group(B), group(C)] AND * D is not in [A, B, C] (in other words, D isn't explicitly opted back in). The rule's full * set of supported environments thus becomes [A, B, C] + all defaults that belong to unrelated * groups. * *

If the rule has a "compatible with" value of [E, F, G], these are unconditionally * added to its set of supported environments (in addition to the results from above). * *

An environment may not appear in both a rule's "restricted to" and "compatible with" values. * If two environments belong to the same group, they must either both be in "restricted to", * both be in "compatible with", or not explicitly specified. * *

Given all the above, constraint enforcement is this: rule A can depend on rule B if, for * every environment A supports, B also supports that environment. */ public class ConstraintSemantics { private ConstraintSemantics() { } /** * Provides a set of default environments for a given environment group. */ private interface DefaultsProvider { Collection

If this rule doesn't have the given attributes, returns an empty set. */ private EnvironmentCollection collectEnvironments(String attrName, EnvironmentCollection.Builder supportedEnvironments) { if (!ruleContext.getRule().isAttrDefined(attrName, Type.LABEL_LIST)) { return EnvironmentCollection.EMPTY; } EnvironmentCollection.Builder environments = new EnvironmentCollection.Builder(); for (TransitiveInfoCollection envTarget : ruleContext.getPrerequisites(attrName, RuleConfiguredTarget.Mode.DONT_CHECK)) { EnvironmentWithGroup envInfo = resolveEnvironment(envTarget); environments.put(envInfo.group(), envInfo.environment()); supportedEnvironments.put(envInfo.group(), envInfo.environment()); } return environments.build(); } /** * Returns the environment and its group. An {@link Environment} rule only "supports" one * environment: itself. Extract that from its more generic provider interface and sanity * check that that's in fact what we see. */ private static EnvironmentWithGroup resolveEnvironment(TransitiveInfoCollection envRule) { SupportedEnvironmentsProvider prereq = Preconditions.checkNotNull(envRule.getProvider(SupportedEnvironmentsProvider.class)); return Iterables.getOnlyElement(prereq.getEnvironments().getGroupedEnvironments()); } } /** * Exception indicating errors finding/parsing environments or their containing groups. */ public static class EnvironmentLookupException extends Exception { private EnvironmentLookupException(String message) { super(message); } } /** * Returns the environment group that owns the given environment. Both must belong to * the same package. * * @throws EnvironmentLookupException if the input is not an {@link EnvironmentRule} or no * matching group is found */ public static EnvironmentGroup getEnvironmentGroup(Target envTarget) throws EnvironmentLookupException { if (!(envTarget instanceof Rule) || !((Rule) envTarget).getRuleClass().equals(EnvironmentRule.RULE_NAME)) { throw new EnvironmentLookupException( envTarget.getLabel() + " is not a valid environment definition"); } for (EnvironmentGroup group : envTarget.getPackage().getTargets(EnvironmentGroup.class)) { if (group.getEnvironments().contains(envTarget.getLabel())) { return group; } } throw new EnvironmentLookupException( "cannot find the group for environment " + envTarget.getLabel()); } /** * Returns the set of environments this rule supports, applying the logic described in * {@link ConstraintSemantics}. * *

Note this set is not complete - it doesn't include environments from groups we don't * "know about". Environments and groups can be declared in any package. If the rule includes * no references to that package, then it simply doesn't know anything about them. But the * constraint semantics say the rule should support the defaults for that group. We encode this * implicitly: given the returned set, for any group that's not in the set the rule is also * considered to support that group's defaults. * * @param ruleContext analysis context for the rule. A rule error is triggered here if * invalid constraint settings are discovered. * @return the environments this rule supports, not counting defaults "unknown" to this rule * as described above. Returns null if any errors are encountered. */ @Nullable public static EnvironmentCollection getSupportedEnvironments(RuleContext ruleContext) { if (!validateAttributes(ruleContext)) { return null; } // This rule's rule class defaults (or null if the rule class has no defaults). EnvironmentCollector ruleClassCollector = maybeGetRuleClassDefaults(ruleContext); // Default environments for this rule. If the rule has rule class defaults, this is // those defaults. Otherwise it's the global defaults specified by environment_group // declarations. DefaultsProvider ruleDefaults; if (ruleClassCollector != null) { if (!ruleClassCollector.validateEnvironmentSpecifications()) { return null; } ruleDefaults = new RuleClassDefaultsProvider(ruleClassCollector.getEnvironments()); } else { ruleDefaults = new GroupDefaultsProvider(); } EnvironmentCollector ruleCollector = new EnvironmentCollector(ruleContext, RuleClass.RESTRICTED_ENVIRONMENT_ATTR, RuleClass.COMPATIBLE_ENVIRONMENT_ATTR, ruleDefaults); if (!ruleCollector.validateEnvironmentSpecifications()) { return null; } EnvironmentCollection supportedEnvironments = ruleCollector.getEnvironments(); if (ruleClassCollector != null) { // If we have rule class defaults from groups that aren't referenced from the rule itself, // we need to add them in too to override the global defaults. supportedEnvironments = addUnknownGroupsToCollection(supportedEnvironments, ruleClassCollector.getEnvironments()); } return supportedEnvironments; } /** * Returns the rule class defaults specified for this rule, or null if there are * no such defaults. */ @Nullable private static EnvironmentCollector maybeGetRuleClassDefaults(RuleContext ruleContext) { Rule rule = ruleContext.getRule(); String restrictionAttr = RuleClass.DEFAULT_RESTRICTED_ENVIRONMENT_ATTR; String compatibilityAttr = RuleClass.DEFAULT_COMPATIBLE_ENVIRONMENT_ATTR; if (rule.isAttrDefined(restrictionAttr, Type.LABEL_LIST) || rule.isAttrDefined(compatibilityAttr, Type.LABEL_LIST)) { return new EnvironmentCollector(ruleContext, restrictionAttr, compatibilityAttr, new GroupDefaultsProvider()); } else { return null; } } /** * Adds environments to an {@link EnvironmentCollection} from groups that aren't already * a part of that collection. * * @param environments the collection to add to * @param toAdd the collection to add. All environments in this collection in groups * that aren't represented in {@code environments} are added to {@code environments}. * @return the expanded collection. */ private static EnvironmentCollection addUnknownGroupsToCollection( EnvironmentCollection environments, EnvironmentCollection toAdd) { EnvironmentCollection.Builder builder = new EnvironmentCollection.Builder(); builder.putAll(environments); for (EnvironmentGroup candidateGroup : toAdd.getGroups()) { if (!environments.getGroups().contains(candidateGroup)) { builder.putAll(candidateGroup, toAdd.getEnvironments(candidateGroup)); } } return builder.build(); } /** * Validity-checks this rule's constraint-related attributes. Returns true if all is good, * returns false and reports appropriate errors if there are any problems. */ private static boolean validateAttributes(RuleContext ruleContext) { AttributeMap attributes = ruleContext.attributes(); // Report an error if "restricted to" is explicitly set to nothing. Even if this made // conceptual sense, we don't know which groups we should apply that to. String restrictionAttr = RuleClass.RESTRICTED_ENVIRONMENT_ATTR; List restrictionEnvironments = ruleContext .getPrerequisites(restrictionAttr, RuleConfiguredTarget.Mode.DONT_CHECK); if (restrictionEnvironments.isEmpty() && attributes.isAttributeValueExplicitlySpecified(restrictionAttr)) { ruleContext.attributeError(restrictionAttr, "attribute cannot be empty"); return false; } return true; } /** * Performs constraint checking on the given rule's dependencies and reports any errors. * * @param ruleContext the rule to analyze * @param ruleEnvironments the rule's supported environments, as defined by the return * value of {@link #getSupportedEnvironments}. In particular, for any environment group that's * not in this collection, the rule is assumed to support the defaults for that group. */ public static void checkConstraints(RuleContext ruleContext, EnvironmentCollection ruleEnvironments) { for (TransitiveInfoCollection dependency : getAllPrerequisites(ruleContext)) { SupportedEnvironmentsProvider depProvider = dependency.getProvider(SupportedEnvironmentsProvider.class); if (depProvider == null) { // Input files (InputFileConfiguredTarget) don't support environments. We may subsequently // opt them into constraint checking, but for now just pass them by. continue; } Collection