| // Copyright 2020 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.devtools.build.lib.analysis.constraints.SupportedEnvironmentsProvider.RemovedEnvironmentCulprit; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.packages.EnvironmentGroup; |
| import com.google.devtools.build.lib.packages.Rule; |
| import com.google.devtools.build.lib.packages.Target; |
| import com.google.devtools.build.lib.server.FailureDetails.Analysis; |
| import com.google.devtools.build.lib.server.FailureDetails.Analysis.Code; |
| import com.google.devtools.build.lib.server.FailureDetails.FailureDetail; |
| import com.google.devtools.build.lib.skyframe.DetailedException; |
| import com.google.devtools.build.lib.util.DetailedExitCode; |
| import java.util.Map; |
| 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 |
| * com.google.devtools.build.lib.packages.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 |
| * com.google.devtools.build.lib.packages.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 |
| * com.google.devtools.build.lib.packages.RuleClass.Builder#restrictedTo} and {@link |
| * com.google.devtools.build.lib.packages.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>. |
| * |
| * @param <T> The type of object to check for constraints. |
| */ |
| public interface ConstraintSemantics<T> { |
| |
| /** |
| * 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 |
| */ |
| static EnvironmentGroup getEnvironmentGroup(Target envTarget) throws EnvironmentLookupException { |
| if (!(envTarget instanceof Rule) |
| || !((Rule) envTarget).getRuleClass().equals(ConstraintConstants.ENVIRONMENT_RULE)) { |
| throw createEnvironmentLookupException( |
| envTarget.getLabel() + " is not a valid environment definition", |
| Code.INVALID_ENVIRONMENT); |
| } |
| for (EnvironmentGroup group : envTarget.getPackage().getTargets(EnvironmentGroup.class)) { |
| if (group.getEnvironments().contains(envTarget.getLabel())) { |
| return group; |
| } |
| } |
| throw createEnvironmentLookupException( |
| "cannot find the group for environment " + envTarget.getLabel(), |
| Code.ENVIRONMENT_MISSING_FROM_GROUPS); |
| } |
| |
| /** |
| * Returns the set of environments this rule supports. |
| * |
| * <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 context 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 |
| EnvironmentCollection getSupportedEnvironments(T context); |
| |
| /** |
| * 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 context 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. |
| */ |
| void checkConstraints( |
| T context, |
| EnvironmentCollection staticEnvironments, |
| EnvironmentCollection.Builder refinedEnvironments, |
| Map<Label, RemovedEnvironmentCulprit> removedEnvironmentCulprits); |
| |
| /** |
| * Returns an {@link EnvironmentLookupException} with the specified message and detailed failure |
| * code. |
| */ |
| static EnvironmentLookupException createEnvironmentLookupException(String message, Code code) { |
| return new EnvironmentLookupException( |
| DetailedExitCode.of( |
| FailureDetail.newBuilder() |
| .setMessage(message) |
| .setAnalysis(Analysis.newBuilder().setCode(code)) |
| .build())); |
| } |
| |
| /** Exception indicating errors finding/parsing environments or their containing groups. */ |
| class EnvironmentLookupException extends Exception implements DetailedException { |
| private final DetailedExitCode detailedExitCode; |
| |
| private EnvironmentLookupException(DetailedExitCode detailedExitCode) { |
| super(detailedExitCode.getFailureDetail().getMessage()); |
| this.detailedExitCode = detailedExitCode; |
| } |
| |
| @Override |
| public DetailedExitCode getDetailedExitCode() { |
| return detailedExitCode; |
| } |
| } |
| } |