blob: 346308ceeb6bb89cdc48b2a74e1db46ee0dfcb48 [file] [log] [blame]
// Copyright 2015 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.constraints;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.analysis.LabelAndLocation;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.configuredtargets.OutputFileConfiguredTarget;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
import com.google.devtools.build.lib.analysis.constraints.EnvironmentCollection.EnvironmentWithGroup;
import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironmentsProvider.RemovedEnvironmentCulprit;
import com.google.devtools.build.lib.cmdline.Label;
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.DependencyFilter;
import com.google.devtools.build.lib.packages.EnvironmentGroup;
import com.google.devtools.build.lib.packages.EnvironmentLabels;
import com.google.devtools.build.lib.packages.RawAttributeMapper;
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.packages.Type.LabelClass;
import com.google.devtools.build.lib.packages.Type.LabelVisitor;
import com.google.devtools.build.lib.rules.AliasConfiguredTarget;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import javax.annotation.Nullable;
/**
* Implementation of the semantics of Bazel's constraint specification and enforcement system.
*
* <p>This is how the system works:
*
* <p>All build rules can declare which "static environments" they can be built for, where a
* "static environment" is a label instance of an {@link EnvironmentRule} rule declared in a
* BUILD file. There are various ways to do this:
*
* <ul>
* <li>Through a "restricted to" attribute setting
* ({@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR}). This is the most direct form of
* specification - it declares the exact set of environments the rule supports (for its group -
* see precise details below).
* <li>Through a "compatible with" attribute setting
* ({@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR}. This declares <b>additional</b>
* environments a rule supports in addition to "standard" environments that are supported by
* default (see below).
* <li>Through "default" specifications in {@link EnvironmentGroup} rules. Every environment
* belongs to a group of thematically related peers (e.g. "target architectures", "JDK versions",
* or "mobile devices"). An environment group's definition includes which of these
* environments should be supported "by default" if not otherwise specified by one of the above
* mechanisms. In particular, a rule with no environment-related attributes automatically
* inherits all defaults.
* <li>Through a rule class default ({@link RuleClass.Builder#restrictedTo} and
* {@link RuleClass.Builder#compatibleWith}). This overrides global defaults for all instances
* of the given rule class. This can be used, for example, to make all *_test rules "testable"
* without each instance having to explicitly declare this capability.
* </ul>
*
* <p>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.
*
* <p>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.
*
* <p>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).
*
* <p>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.
*
* <p>Given all the above, constraint enforcement is this: rule A can depend on rule B if, for
* every static environment A supports, B also supports that environment.
*
* <p>Configurable attributes introduce the additional concept of "refined environments". Given:
*
* <pre>
* java_library(
* name = "lib",
* restricted_to = [":A", ":B"],
* deps = select({
* ":config_a": [":depA"],
* ":config_b": [":depB"],
* }))
* java_library(
* name = "depA",
* restricted_to = [":A"])
* java_library(
* name = "depB",
* restricted_to = [":B"])
* </pre>
*
* "lib"'s static environments are what are declared via restricted_to: {@code [":A", ":B"]}.
* But normal constraint checking doesn't work well here: neither "depA" or "depB" supports both
* environments, so each is technically invalid. But the two of them together <i>do</i> support
* both environments. So constraint checking with selects checks that "lib"'s environments
* are supported by the <i>union</i> of its selectable dependencies, then <i>refines</i> its
* environments to whichever deps get chosen. In other words:
*
* <ol>
* <li>The above example is considered constraint-valid.
* <li>When building with "config_a", "lib"'s refined environment set is {@code [":A"]}.
* <li>When building with "config_b", "lib"'s refined environment set is {@code [":B"]}.
* <li>Any rule depending on "lib" has its environments refined by the intersection with "lib".
* So if "depender" has {@code restricted_to = [":A", ":B"]} and {@code deps = [":lib"]},
* then when building with "config_a", "depender"'s refined environment set is {@code [":A"]}.
* <li>For each environment group, every rule's refined environment set must be non-empty. This
* ensures the "chosen" dep in a select matches all rules up the dependency chain. So if
* "depender" had {@code restricted_to = [":B"]}, it wouldn't be allowed in a "config_a"
* build.
* </ol>
* </code>.
*/
public class ConstraintSemantics {
public ConstraintSemantics() {
}
/**
* Logs an error message that the current rule violates constraints.
*/
public void ruleError(RuleContext ruleContext, String message) {
ruleContext.ruleError(message);
}
/**
* Logs an error message that an attribute on the current rule doesn't properly declare
* constraints.
*/
public void attributeError(RuleContext ruleContext, String attribute, String message) {
ruleContext.attributeError(attribute, message);
}
/**
* Provides a set of default environments for a given environment group.
*/
private interface DefaultsProvider {
Collection<Label> getDefaults(EnvironmentLabels group);
}
/**
* Provides a group's defaults as specified in the environment group's BUILD declaration.
*/
private static class GroupDefaultsProvider implements DefaultsProvider {
@Override
public Collection<Label> getDefaults(EnvironmentLabels group) {
return group.getDefaults();
}
}
/**
* Provides a group's defaults, factoring in rule class defaults as specified by
* {@link com.google.devtools.build.lib.packages.RuleClass.Builder#compatibleWith}
* and {@link com.google.devtools.build.lib.packages.RuleClass.Builder#restrictedTo}.
*/
private static class RuleClassDefaultsProvider implements DefaultsProvider {
private final EnvironmentCollection ruleClassDefaults;
private final GroupDefaultsProvider groupDefaults;
RuleClassDefaultsProvider(EnvironmentCollection ruleClassDefaults) {
this.ruleClassDefaults = ruleClassDefaults;
this.groupDefaults = new GroupDefaultsProvider();
}
@Override
public Collection<Label> getDefaults(EnvironmentLabels group) {
if (ruleClassDefaults.getGroups().contains(group)) {
return ruleClassDefaults.getEnvironments(group);
} else {
// If there are no rule class defaults for this group, just inherit global defaults.
return groupDefaults.getDefaults(group);
}
}
}
/**
* Collects the set of supported environments for a given rule by merging its
* restriction-style and compatibility-style environment declarations as specified by
* the given attributes. Only includes environments from "known" groups, i.e. the groups
* owning the environments explicitly referenced from these attributes.
*/
private class EnvironmentCollector {
private final RuleContext ruleContext;
private final String restrictionAttr;
private final String compatibilityAttr;
private final DefaultsProvider defaultsProvider;
private final EnvironmentCollection restrictionEnvironments;
private final EnvironmentCollection compatibilityEnvironments;
private final EnvironmentCollection supportedEnvironments;
/**
* Constructs a new collector on the given attributes.
*
* @param ruleContext analysis context for the rule
* @param restrictionAttr the name of the attribute that declares "restricted to"-style
* environments. If the rule doesn't have this attribute, this is considered an
* empty declaration.
* @param compatibilityAttr the name of the attribute that declares "compatible with"-style
* environments. If the rule doesn't have this attribute, this is considered an
* empty declaration.
* @param defaultsProvider provider for the default environments within a group if not
* otherwise overridden by the above attributes
*/
EnvironmentCollector(RuleContext ruleContext, String restrictionAttr, String compatibilityAttr,
DefaultsProvider defaultsProvider) {
this.ruleContext = ruleContext;
this.restrictionAttr = restrictionAttr;
this.compatibilityAttr = compatibilityAttr;
this.defaultsProvider = defaultsProvider;
EnvironmentCollection.Builder environmentsBuilder = new EnvironmentCollection.Builder();
restrictionEnvironments = collectRestrictionEnvironments(environmentsBuilder);
compatibilityEnvironments = collectCompatibilityEnvironments(environmentsBuilder);
supportedEnvironments = environmentsBuilder.build();
}
/**
* Returns the set of environments supported by this rule, as determined by the
* restriction-style attribute, compatibility-style attribute, and group defaults
* provider instantiated with this class.
*/
EnvironmentCollection getEnvironments() {
return supportedEnvironments;
}
/**
* Validity-checks that no group has its environment referenced in both the "compatible with"
* and restricted to" attributes. Returns true if all is good, returns false and reports
* appropriate errors if there are any problems.
*/
boolean validateEnvironmentSpecifications() {
ImmutableCollection<EnvironmentLabels> restrictionGroups =
restrictionEnvironments.getGroups();
boolean hasErrors = false;
for (EnvironmentLabels group : compatibilityEnvironments.getGroups()) {
if (restrictionGroups.contains(group)) {
// To avoid error-spamming the user, when we find a conflict we only report one example
// environment from each attribute for that group.
Label compatibilityEnv =
compatibilityEnvironments.getEnvironments(group).iterator().next();
Label restrictionEnv = restrictionEnvironments.getEnvironments(group).iterator().next();
if (compatibilityEnv.equals(restrictionEnv)) {
attributeError(ruleContext, compatibilityAttr, compatibilityEnv
+ " cannot appear both here and in " + restrictionAttr);
} else {
attributeError(ruleContext, compatibilityAttr, compatibilityEnv + " and "
+ restrictionEnv + " belong to the same environment group. They should be declared "
+ "together either here or in " + restrictionAttr);
}
hasErrors = true;
}
}
return !hasErrors;
}
/**
* Adds environments specified in the "restricted to" attribute to the set of supported
* environments and returns the environments added.
*/
private EnvironmentCollection collectRestrictionEnvironments(
EnvironmentCollection.Builder supportedEnvironments) {
return collectEnvironments(restrictionAttr, supportedEnvironments);
}
/**
* Adds environments specified in the "compatible with" attribute to the set of supported
* environments, along with all defaults from the groups they belong to. Returns these
* environments, not including the defaults.
*/
private EnvironmentCollection collectCompatibilityEnvironments(
EnvironmentCollection.Builder supportedEnvironments) {
EnvironmentCollection compatibilityEnvironments =
collectEnvironments(compatibilityAttr, supportedEnvironments);
for (EnvironmentLabels group : compatibilityEnvironments.getGroups()) {
supportedEnvironments.putAll(group, defaultsProvider.getDefaults(group));
}
return compatibilityEnvironments;
}
/**
* Adds environments specified by the given attribute to the set of supported environments
* and returns the environments added.
*
* <p>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, BuildType.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 EnvironmentWithGroup resolveEnvironment(TransitiveInfoCollection envRule) {
SupportedEnvironmentsProvider prereq =
Preconditions.checkNotNull(envRule.getProvider(SupportedEnvironmentsProvider.class));
return Iterables.getOnlyElement(prereq.getStaticEnvironments().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}.
*
* <p>Note this set is <b>not complete</b> - 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 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 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, BuildType.LABEL_LIST)
|| rule.isAttrDefined(compatibilityAttr, BuildType.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 (EnvironmentLabels 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 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<? extends TransitiveInfoCollection> restrictionEnvironments = ruleContext
.getPrerequisites(restrictionAttr, RuleConfiguredTarget.Mode.DONT_CHECK);
if (restrictionEnvironments.isEmpty()
&& attributes.isAttributeValueExplicitlySpecified(restrictionAttr)) {
attributeError(ruleContext, restrictionAttr, "attribute cannot be empty");
return false;
}
return true;
}
/**
* Helper container for checkConstraints: stores both a set of deps that need to be
* constraint-checked and the subset of those deps that only appear inside selects.
*/
private static class DepsToCheck {
private final Set<TransitiveInfoCollection> allDeps;
private final Set<TransitiveInfoCollection> selectOnlyDeps;
DepsToCheck(Set<TransitiveInfoCollection> depsToCheck,
Set<TransitiveInfoCollection> selectOnlyDeps) {
this.allDeps = depsToCheck;
this.selectOnlyDeps = selectOnlyDeps;
}
Set<TransitiveInfoCollection> allDeps() {
return allDeps;
}
boolean isSelectOnly(TransitiveInfoCollection dep) {
return selectOnlyDeps.contains(dep);
}
}
/**
* Performs constraint checking on the given rule's dependencies and reports any errors. This
* includes:
*
* <ul>
* <li>Static environment checking: if this rule supports environment E, all deps outside
* selects must also support E
* <li>Refined environment computation: this rule's refined environments are its static
* environments intersected with the refined environments of all dependencies (including
* chosen deps in selects)
* <li>Refined environment checking: no environment groups can be "emptied" due to refinement
* </ul>
*
* @param ruleContext the rule to analyze
* @param staticEnvironments 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.
* @param refinedEnvironments a builder for populating this rule's refined environments
* @param removedEnvironmentCulprits a builder for populating the core dependencies that trigger
* pruning away environments through refinement. If multiple dependencies qualify (e.g. two
* direct deps under the current rule), one is arbitrarily chosen.
*/
public void checkConstraints(
RuleContext ruleContext,
EnvironmentCollection staticEnvironments,
EnvironmentCollection.Builder refinedEnvironments,
Map<Label, RemovedEnvironmentCulprit> removedEnvironmentCulprits) {
Set<EnvironmentWithGroup> refinedEnvironmentsSoFar = new LinkedHashSet<>();
// Start with the full set of static environments:
refinedEnvironmentsSoFar.addAll(staticEnvironments.getGroupedEnvironments());
Set<EnvironmentLabels> groupsWithEnvironmentsRemoved = new LinkedHashSet<>();
// Maps the label results of getUnsupportedEnvironments() to EnvironmentWithGroups. We can't
// have that method just return EnvironmentWithGroups because it also collects group defaults,
// which we only have labels for.
Map<Label, EnvironmentWithGroup> labelsToEnvironments = new HashMap<>();
for (EnvironmentWithGroup envWithGroup : staticEnvironments.getGroupedEnvironments()) {
labelsToEnvironments.put(envWithGroup.environment(), envWithGroup);
}
DepsToCheck depsToCheck = getConstraintCheckedDependencies(ruleContext);
for (TransitiveInfoCollection dep : depsToCheck.allDeps()) {
if (!depsToCheck.isSelectOnly(dep)) {
// TODO(bazel-team): support static constraint checking for selects. A selectable constraint
// is valid if the union of all deps in the select includes all of this rule's static
// environments. Determining that requires following the select paths that don't get chosen,
// which means we won't have ConfiguredTargets for those deps and need to find another
// way to get their environments.
checkStaticConstraints(ruleContext, staticEnvironments, dep);
}
refineEnvironmentsForDep(ruleContext, staticEnvironments, dep, labelsToEnvironments,
refinedEnvironmentsSoFar, groupsWithEnvironmentsRemoved, removedEnvironmentCulprits);
}
checkRefinedConstraints(ruleContext, groupsWithEnvironmentsRemoved,
refinedEnvironmentsSoFar, refinedEnvironments, removedEnvironmentCulprits);
}
/**
* Performs static constraint checking against the given dep.
*
* @param ruleContext the rule being analyzed
* @param staticEnvironments the static environments of the rule being analyzed
* @param dep the dep to check
*/
private void checkStaticConstraints(RuleContext ruleContext,
EnvironmentCollection staticEnvironments, TransitiveInfoCollection dep) {
SupportedEnvironmentsProvider depEnvironments =
dep.getProvider(SupportedEnvironmentsProvider.class);
Collection<Label> unsupportedEnvironments =
getUnsupportedEnvironments(depEnvironments.getStaticEnvironments(), staticEnvironments);
if (!unsupportedEnvironments.isEmpty()) {
ruleError(ruleContext,
"dependency " + dep.getLabel() + " doesn't support expected environment"
+ (unsupportedEnvironments.size() == 1 ? "" : "s")
+ ": " + Joiner.on(", ").join(unsupportedEnvironments));
}
}
/**
* Helper method for {@link #checkConstraints}: refines a rule's environments with the given dep.
*
* <p>A rule's <b>complete</b> refined set applies this process to every dep.
*/
private static void refineEnvironmentsForDep(
RuleContext ruleContext,
EnvironmentCollection staticEnvironments,
TransitiveInfoCollection dep,
Map<Label, EnvironmentWithGroup> labelsToEnvironments,
Set<EnvironmentWithGroup> refinedEnvironmentsSoFar,
Set<EnvironmentLabels> groupsWithEnvironmentsRemoved,
Map<Label, RemovedEnvironmentCulprit> removedEnvironmentCulprits) {
SupportedEnvironmentsProvider depEnvironments =
dep.getProvider(SupportedEnvironmentsProvider.class);
// Stores the environments that are pruned from the refined set because of this dep. Even
// though they're removed, some subset of the environments they fulfill may belong in the
// refined set. For example, if environment "both" fulfills "a" and "b" and "lib" statically
// sets restricted_to = ["both"] and "dep" sets restricted_to = ["a"], then lib's refined set
// excludes "both". But rather than be emptied out it can be reduced to "a".
Set<Label> prunedEnvironmentsFromThisDep = new LinkedHashSet<>();
// Refine this rule's environments by intersecting with the dep's refined environments:
for (Label refinedEnvironmentToPrune : getUnsupportedEnvironments(
depEnvironments.getRefinedEnvironments(), staticEnvironments)) {
EnvironmentWithGroup envToPrune = labelsToEnvironments.get(refinedEnvironmentToPrune);
if (envToPrune == null) {
// If we have no record of this environment, that means the current rule implicitly uses
// the defaults for this group. So explicitly opt that group's defaults into the refined
// set before trying to remove specific items.
for (EnvironmentWithGroup defaultEnv :
getDefaults(refinedEnvironmentToPrune, depEnvironments.getRefinedEnvironments())) {
refinedEnvironmentsSoFar.add(defaultEnv);
labelsToEnvironments.put(defaultEnv.environment(), defaultEnv);
}
envToPrune = Verify.verifyNotNull(labelsToEnvironments.get(refinedEnvironmentToPrune));
}
refinedEnvironmentsSoFar.remove(envToPrune);
groupsWithEnvironmentsRemoved.add(envToPrune.group());
removedEnvironmentCulprits.put(envToPrune.environment(),
findOriginalRefiner(ruleContext, dep.getLabel(), depEnvironments, envToPrune));
prunedEnvironmentsFromThisDep.add(envToPrune.environment());
}
// Add in any dep environment that one of the environments we removed fulfills. In other
// words, the removed environment is no good, but some subset of it may be.
for (EnvironmentWithGroup depEnv :
depEnvironments.getRefinedEnvironments().getGroupedEnvironments()) {
for (Label fulfiller : depEnv.group().getFulfillers(depEnv.environment()).toList()) {
if (prunedEnvironmentsFromThisDep.contains(fulfiller)) {
refinedEnvironmentsSoFar.add(depEnv);
}
}
}
}
/**
* Helper method for {@link #checkConstraints}: performs refined environment constraint checking.
*
* <p>Refined environment expectations: no environment group should be emptied out due to
* refining. This reflects the idea that some of the static declared environments get pruned out
* by the build configuration, but <i>all</i> environments shouldn't be pruned out.
*
* <p>Violations of this expectation trigger rule analysis errors.
*/
private void checkRefinedConstraints(
RuleContext ruleContext,
Set<EnvironmentLabels> groupsWithEnvironmentsRemoved,
Set<EnvironmentWithGroup> refinedEnvironmentsSoFar,
EnvironmentCollection.Builder refinedEnvironments,
Map<Label, RemovedEnvironmentCulprit> removedEnvironmentCulprits) {
Set<EnvironmentLabels> refinedGroups = new LinkedHashSet<>();
for (EnvironmentWithGroup envWithGroup : refinedEnvironmentsSoFar) {
refinedEnvironments.put(envWithGroup.group(), envWithGroup.environment());
refinedGroups.add(envWithGroup.group());
}
Set<EnvironmentLabels> newlyEmptyGroups =
groupsWithEnvironmentsRemoved.isEmpty()
? ImmutableSet.of()
: Sets.difference(groupsWithEnvironmentsRemoved, refinedGroups);
if (!newlyEmptyGroups.isEmpty()) {
ruleError(ruleContext, getOverRefinementError(newlyEmptyGroups, removedEnvironmentCulprits));
}
}
/**
* Constructs an error message for when all environments have been pruned out of one or more
* environment groups due to refining.
*/
private static String getOverRefinementError(
Set<EnvironmentLabels> newlyEmptyGroups,
Map<Label, RemovedEnvironmentCulprit> removedEnvironmentCulprits) {
StringJoiner message = new StringJoiner("\n")
.add("the current command line flags disqualify all supported environments because of "
+ "incompatible select() paths:");
for (EnvironmentLabels group : newlyEmptyGroups) {
if (newlyEmptyGroups.size() > 1) {
message
.add(" ")
.add("environment group: " + group.getLabel() + ":");
}
for (Label prunedEnvironment : group.getEnvironments()) {
RemovedEnvironmentCulprit culprit = removedEnvironmentCulprits.get(prunedEnvironment);
// Only environments this rule statically declared support for have culprits.
if (culprit != null) {
message
.add(" ")
.add(getMissingEnvironmentCulpritMessage(prunedEnvironment, culprit));
}
}
}
return message.toString();
}
static String getMissingEnvironmentCulpritMessage(Label environment,
RemovedEnvironmentCulprit reason) {
LabelAndLocation culprit = reason.culprit();
return new StringJoiner("\n")
.add(" environment: " + environment)
.add(" removed by: " + culprit.getLabel() + " (" + culprit.getLocation() + ")")
.add(" which has a select() that chooses dep: " + reason.selectedDepForCulprit())
.add(" which lacks: " + environment)
.toString();
}
/**
* Given an environment that should be refined out of the current rule because of the given dep,
* returns the original dep that caused the removal.
*
* <p>For example, say we have R -> D1 -> D2 and all rules support environment E. If the
* refinement happens because D2 has
* <pre>
* deps = select({":foo": ["restricted_to_E"], ":bar": ["restricted_to_F"]}} # Choose F.
* </pre>
*
* <p>then D2 is the original refiner (even though D1 and R inherit the same pruning).
*/
private static RemovedEnvironmentCulprit findOriginalRefiner(RuleContext ruleContext, Label dep,
SupportedEnvironmentsProvider depEnvironments, EnvironmentWithGroup envToPrune) {
RemovedEnvironmentCulprit depCulprit =
depEnvironments.getRemovedEnvironmentCulprit(envToPrune.environment());
if (depCulprit != null) {
return depCulprit;
}
// If the dep has no record of this environment being refined, that means the current rule
// is the culprit.
return RemovedEnvironmentCulprit.create(
LabelAndLocation.of(ruleContext.getTarget()),
// While it'd be nice to know the dep's location too, it isn't strictly necessary.
// Especially since we already have the parent's location. So it's easy enough to find the
// dep. And we want to respect the efficiency concerns described in LabelAndLocation.
//
// Alternatively, we could prepare error strings directly in SupportedEnvironmentsProvider,
// which should remove the need for LabelAndLocation for any target.
dep);
}
/**
* Finds the given environment in the given set and returns the default environments for its
* group.
*/
private static Collection<EnvironmentWithGroup> getDefaults(Label env,
EnvironmentCollection allEnvironments) {
EnvironmentLabels group = null;
for (EnvironmentLabels candidateGroup : allEnvironments.getGroups()) {
if (candidateGroup.getDefaults().contains(env)) {
group = candidateGroup;
break;
}
}
Verify.verifyNotNull(group);
ImmutableSet.Builder<EnvironmentWithGroup> builder = ImmutableSet.builder();
for (Label defaultEnv : group.getDefaults()) {
builder.add(EnvironmentWithGroup.create(defaultEnv, group));
}
return builder.build();
}
/**
* Given a collection of environments and a collection of expected environments, returns the
* missing environments that would cause constraint expectations to be violated. Includes the
* effects of environment group defaults.
*/
static Collection<Label> getUnsupportedEnvironments(
EnvironmentCollection actualEnvironments, EnvironmentCollection expectedEnvironments) {
Set<Label> missingEnvironments = new LinkedHashSet<>();
Collection<Label> actualEnvironmentLabels = actualEnvironments.getEnvironments();
// Check if each explicitly expected environment is satisfied.
for (EnvironmentWithGroup expectedEnv : expectedEnvironments.getGroupedEnvironments()) {
EnvironmentLabels group = expectedEnv.group();
Label environment = expectedEnv.environment();
boolean isSatisfied = false;
if (actualEnvironments.getGroups().contains(group)) {
// If the actual environments include members from the expected environment's group, we
// need to either find the environment itself or another one that transitively fulfills it.
if (actualEnvironmentLabels.contains(environment)
|| intersect(actualEnvironmentLabels, group.getFulfillers(environment).toList())) {
isSatisfied = true;
}
} else {
// If the actual environments don't reference the expected environment's group at all,
// the group's defaults are implicitly included. So we need to check those defaults for
// either the expected environment or another environment that transitively fulfills it.
if (group.isDefault(environment)
|| intersect(group.getFulfillers(environment).toList(), group.getDefaults())) {
isSatisfied = true;
}
}
if (!isSatisfied) {
missingEnvironments.add(environment);
}
}
// For any environment group not referenced by the expected environments, its defaults are
// implicitly expected. We can ignore this if the actual environments also don't reference the
// group (since in that case the same defaults apply), otherwise we have to check.
for (EnvironmentLabels group : actualEnvironments.getGroups()) {
if (!expectedEnvironments.getGroups().contains(group)) {
for (Label expectedDefault : group.getDefaults()) {
if (!actualEnvironmentLabels.contains(expectedDefault)
&& !intersect(
actualEnvironmentLabels, group.getFulfillers(expectedDefault).toList())) {
missingEnvironments.add(expectedDefault);
}
}
}
}
return missingEnvironments;
}
private static boolean intersect(Iterable<Label> labels1, Iterable<Label> labels2) {
return !Sets.intersection(Sets.newHashSet(labels1), Sets.newHashSet(labels2)).isEmpty();
}
/**
* Returns all dependencies that should be constraint-checked against the current rule,
* including both "uncoditional" deps (outside selects) and deps that only appear in selects.
*/
private static DepsToCheck getConstraintCheckedDependencies(RuleContext ruleContext) {
Set<TransitiveInfoCollection> depsToCheck = new LinkedHashSet<>();
Set<TransitiveInfoCollection> selectOnlyDeps = new LinkedHashSet<>();
Set<TransitiveInfoCollection> depsOutsideSelects = new LinkedHashSet<>();
AttributeMap attributes = ruleContext.attributes();
for (String attr : attributes.getAttributeNames()) {
Attribute attrDef = attributes.getAttributeDefinition(attr);
if (attrDef.getType().getLabelClass() != LabelClass.DEPENDENCY
|| attrDef.skipConstraintsOverride()) {
continue;
}
if (!attrDef.checkConstraintsOverride()) {
// Use the same implicit deps check that query uses. This facilitates running queries to
// determine exactly which rules need to be constraint-annotated for depot migrations.
if (!DependencyFilter.NO_IMPLICIT_DEPS.apply(ruleContext.getRule(), attrDef)
// We can't identify host deps by calling BuildConfiguration.isHostConfiguration()
// because --nodistinct_host_configuration subverts that call.
|| attrDef.getTransitionFactory().isHost()) {
continue;
}
}
Set<Label> selectOnlyDepsForThisAttribute =
getDepsOnlyInSelects(ruleContext, attr, attributes.getAttributeType(attr));
for (TransitiveInfoCollection dep :
ruleContext.getPrerequisites(attr, RuleConfiguredTarget.Mode.DONT_CHECK)) {
// Output files inherit the environment spec of their generating rule.
if (dep instanceof OutputFileConfiguredTarget) {
// Note this reassignment means constraint violation errors reference the generating
// rule, not the file. This makes the source of the environmental mismatch more clear.
dep = ((OutputFileConfiguredTarget) dep).getGeneratingRule();
}
// Input files don't support environments. We may subsequently opt them into constraint
// checking, but for now just pass them by.
if (dep.getProvider(SupportedEnvironmentsProvider.class) != null) {
depsToCheck.add(dep);
// For normal configured targets the target's label is the same label appearing in the
// select(). But for AliasConfiguredTargets the label in the select() refers to the alias,
// while dep.getLabel() refers to the target the alias points to. So add this quick check
// to make sure we're comparing the same labels.
Label depLabelInSelect =
(dep instanceof AliasConfiguredTarget)
? ((AliasConfiguredTarget) dep).getOriginalLabel()
: dep.getLabel();
if (!selectOnlyDepsForThisAttribute.contains(depLabelInSelect)) {
depsOutsideSelects.add(dep);
}
}
}
}
for (TransitiveInfoCollection dep : depsToCheck) {
if (!depsOutsideSelects.contains(dep)) {
selectOnlyDeps.add(dep);
}
}
return new DepsToCheck(depsToCheck, selectOnlyDeps);
}
/**
* Returns the deps for this attribute that only appear in selects.
*
* <p>For example:
* <pre>
* deps = [":a"] + select({"//foo:cond": [":b"]}) + select({"//conditions:default": [":c"]})
* </pre>
*
* returns {@code [":b"]}. Even though {@code [":c"]} also appears in a select, that's a
* degenerate case with only one always-chosen condition. So that's considered the same as
* an unconditional dep.
*
* <p>Note that just because a dep only appears in selects for this attribute doesn't mean it
* won't appear unconditionally in another attribute.
*/
private static Set<Label> getDepsOnlyInSelects(RuleContext ruleContext, String attr,
Type<?> attrType) {
Rule rule = ruleContext.getRule();
if (!rule.isConfigurableAttribute(attr) || !BuildType.isLabelType(attrType)) {
return ImmutableSet.of();
}
Set<Label> unconditionalDeps = new LinkedHashSet<>();
Set<Label> selectableDeps = new LinkedHashSet<>();
BuildType.SelectorList<?> selectList = (BuildType.SelectorList<?>)
RawAttributeMapper.of(rule).getRawAttributeValue(rule, attr);
for (BuildType.Selector<?> select : selectList.getSelectors()) {
addSelectValuesToSet(select, select.isUnconditional() ? unconditionalDeps : selectableDeps);
}
return Sets.difference(selectableDeps, unconditionalDeps);
}
/**
* Adds all label values from the given select to the given set. Automatically handles different
* value types (e.g. labels vs. label lists).
*/
private static void addSelectValuesToSet(BuildType.Selector<?> select, final Set<Label> set) {
Type<?> type = select.getOriginalType();
LabelVisitor<?> visitor = (label, dummy) -> set.add(label);
for (Object value : select.getEntries().values()) {
type.visitLabels(visitor, value, /*context=*/ null);
}
}
}