| // 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.packages; | |
| import com.google.common.base.Predicate; | |
| import com.google.common.base.Verify; | |
| import com.google.common.collect.HashMultimap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Multimap; | |
| import com.google.common.collect.Sets; | |
| import com.google.devtools.build.lib.cmdline.Label; | |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; | |
| import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; | |
| import com.google.devtools.build.lib.collect.nestedset.Order; | |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; | |
| import com.google.devtools.build.lib.events.Event; | |
| import com.google.devtools.build.lib.events.Location; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| /** | |
| * Model for the "environment_group' rule: the piece of Bazel's rule constraint system that binds | |
| * thematically related environments together and determines which environments a rule supports | |
| * by default. See {@link com.google.devtools.build.lib.analysis.constraints.ConstraintSemantics} | |
| * for precise semantic details of how this information is used. | |
| * | |
| * <p>Note that "environment_group" is implemented as a loading-time function, not a rule. This is | |
| * to support proper discovery of defaults: Say rule A has no explicit constraints and depends | |
| * on rule B, which is explicitly constrained to environment ":bar". Since A declares nothing | |
| * explicitly, it's implicitly constrained to DEFAULTS (whatever that is). Therefore, the | |
| * dependency is only allowed if DEFAULTS doesn't include environments beyond ":bar". To figure | |
| * that out, we need to be able to look up the environment group for ":bar", which is what this | |
| * class provides. | |
| * | |
| * <p>If we implemented this as a rule, we'd have to provide that lookup via rule dependencies, | |
| * e.g. something like: | |
| * | |
| * <code> | |
| * environment( | |
| * name = 'bar', | |
| * group = [':sample_environments'], | |
| * is_default = 1 | |
| * ) | |
| * </code> | |
| * | |
| * <p>But this won't work. This would let us find the environment group for ":bar", but the only way | |
| * to determine what other environments belong to the group is to have the group somehow reference | |
| * them. That would produce circular dependencies in the build graph, which is no good. | |
| */ | |
| @Immutable | |
| public class EnvironmentGroup implements Target { | |
| private final Label label; | |
| private final Location location; | |
| private final Package containingPackage; | |
| private final Set<Label> environments; | |
| private final Set<Label> defaults; | |
| /** | |
| * Maps a member environment to the set of environments that directly fulfill it. Note that | |
| * we can't populate this map until all Target instances for member environments have been | |
| * initialized, which may occur after group instantiation (this makes the class mutable). | |
| */ | |
| private final Map<Label, NestedSet<Label>> fulfillersMap = new HashMap<>(); | |
| /** | |
| * Predicate that matches labels from a different package than the initialized package. | |
| */ | |
| private static final class DifferentPackage implements Predicate<Label> { | |
| private final Package containingPackage; | |
| private DifferentPackage(Package containingPackage) { | |
| this.containingPackage = containingPackage; | |
| } | |
| @Override | |
| public boolean apply(Label environment) { | |
| return !environment.getPackageName().equals(containingPackage.getName()); | |
| } | |
| } | |
| /** | |
| * Instantiates a new group without verifying the soundness of its contents. See the validation | |
| * methods below for appropriate checks. | |
| * | |
| * @param label the build label identifying this group | |
| * @param pkg the package this group belongs to | |
| * @param environments the set of environments that belong to this group | |
| * @param defaults the environments a rule implicitly supports unless otherwise specified | |
| * @param location location in the BUILD file of this group | |
| */ | |
| EnvironmentGroup(Label label, Package pkg, final List<Label> environments, List<Label> defaults, | |
| Location location) { | |
| this.label = label; | |
| this.location = location; | |
| this.containingPackage = pkg; | |
| this.environments = ImmutableSet.copyOf(environments); | |
| this.defaults = ImmutableSet.copyOf(defaults); | |
| } | |
| /** | |
| * Checks that all environments declared by this group are in the same package as the group (so | |
| * we can perform an environment --> environment_group lookup and know the package is available) | |
| * and checks that all defaults are legitimate members of the group. | |
| * | |
| * <p>Does <b>not</b> check that the referenced environments exist (see | |
| * {@link #processMemberEnvironments}). | |
| * | |
| * @return a list of validation errors that occurred | |
| */ | |
| List<Event> validateMembership() { | |
| List<Event> events = new ArrayList<>(); | |
| // All environments should belong to the same package as this group. | |
| for (Label environment : | |
| Iterables.filter(environments, new DifferentPackage(containingPackage))) { | |
| events.add(Event.error(location, | |
| environment + " is not in the same package as group " + label)); | |
| } | |
| // The defaults must be a subset of the member environments. | |
| for (Label unknownDefault : Sets.difference(defaults, environments)) { | |
| events.add(Event.error(location, "default " + unknownDefault + " is not a " | |
| + "declared environment for group " + getLabel())); | |
| } | |
| return events; | |
| } | |
| /** | |
| * Checks that the group's declared environments are legitimate same-package environment | |
| * rules and prepares the "fulfills" relationships between these environments to support | |
| * {@link #getFulfillers}. | |
| * | |
| * @param pkgTargets mapping from label name to target instance for this group's package | |
| * @return a list of validation errors that occurred | |
| */ | |
| List<Event> processMemberEnvironments(Map<String, Target> pkgTargets) { | |
| List<Event> events = new ArrayList<>(); | |
| // Maps an environment to the environments that directly fulfill it. | |
| Multimap<Label, Label> directFulfillers = HashMultimap.create(); | |
| for (Label envName : environments) { | |
| Target env = pkgTargets.get(envName.getName()); | |
| if (isValidEnvironment(env, envName, "", events)) { | |
| AttributeMap attr = NonconfigurableAttributeMapper.of((Rule) env); | |
| for (Label fulfilledEnv : attr.get("fulfills", BuildType.LABEL_LIST)) { | |
| if (isValidEnvironment(pkgTargets.get(fulfilledEnv.getName()), fulfilledEnv, | |
| "in \"fulfills\" attribute of " + envName + ": ", events)) { | |
| directFulfillers.put(fulfilledEnv, envName); | |
| } | |
| } | |
| } | |
| } | |
| // Now that we know which environments directly fulfill each other, compute which environments | |
| // transitively fulfill each other. We could alternatively compute this on-demand, but since | |
| // we don't expect these chains to be very large we opt toward computing them once at package | |
| // load time. | |
| Verify.verify(fulfillersMap.isEmpty()); | |
| for (Label envName : environments) { | |
| setTransitiveFulfillers(envName, directFulfillers, fulfillersMap); | |
| } | |
| return events; | |
| } | |
| /** | |
| * Given an environment and set of environments that directly fulfill it, computes a nested | |
| * set of environments that <i>transitively</i> fulfill it, places it into transitiveFulfillers, | |
| * and returns that set. | |
| */ | |
| private static NestedSet<Label> setTransitiveFulfillers(Label env, | |
| Multimap<Label, Label> directFulfillers, Map<Label, NestedSet<Label>> transitiveFulfillers) { | |
| if (transitiveFulfillers.containsKey(env)) { | |
| return transitiveFulfillers.get(env); | |
| } else if (!directFulfillers.containsKey(env)) { | |
| // Nobody fulfills this environment. | |
| NestedSet<Label> emptySet = NestedSetBuilder.emptySet(Order.STABLE_ORDER); | |
| transitiveFulfillers.put(env, emptySet); | |
| return emptySet; | |
| } else { | |
| NestedSetBuilder<Label> set = NestedSetBuilder.stableOrder(); | |
| for (Label fulfillingEnv : directFulfillers.get(env)) { | |
| set.add(fulfillingEnv); | |
| set.addTransitive( | |
| setTransitiveFulfillers(fulfillingEnv, directFulfillers, transitiveFulfillers)); | |
| } | |
| NestedSet<Label> builtSet = set.build(); | |
| transitiveFulfillers.put(env, builtSet); | |
| return builtSet; | |
| } | |
| } | |
| private boolean isValidEnvironment(Target env, Label envName, String prefix, List<Event> events) { | |
| if (env == null) { | |
| events.add(Event.error(location, prefix + "environment " + envName + " does not exist")); | |
| return false; | |
| } else if (!env.getTargetKind().equals("environment rule")) { | |
| events.add(Event.error(location, prefix + env.getLabel() + " is not a valid environment")); | |
| return false; | |
| } else if (!environments.contains(env.getLabel())) { | |
| events.add(Event.error(location, prefix + env.getLabel() + " is not a member of this group")); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Returns the environments that belong to this group. | |
| */ | |
| public Set<Label> getEnvironments() { | |
| return environments; | |
| } | |
| /** | |
| * Returns the environments a rule supports by default, i.e. if it has no explicit references to | |
| * environments in this group. | |
| */ | |
| public Set<Label> getDefaults() { | |
| return defaults; | |
| } | |
| /** | |
| * Determines whether or not an environment is a default. Returns false if the environment | |
| * doesn't belong to this group. | |
| */ | |
| public boolean isDefault(Label environment) { | |
| return defaults.contains(environment); | |
| } | |
| /** | |
| * Returns the set of environments that transitively fulfill the specified environment. | |
| * The environment must be a valid member of this group. | |
| * | |
| * <p>>For example, if the input is <code>":foo"</code> and <code>":bar"</code> fulfills | |
| * <code>":foo"</code> and <code>":baz"</code> fulfills <code>":bar"</code>, this returns | |
| * <code>[":foo", ":bar", ":baz"]</code>. | |
| * | |
| * <p>If no environments fulfill the input, returns an empty set. | |
| */ | |
| public Iterable<Label> getFulfillers(Label environment) { | |
| return Verify.verifyNotNull(fulfillersMap.get(environment)); | |
| } | |
| @Override | |
| public Label getLabel() { | |
| return label; | |
| } | |
| @Override | |
| public String getName() { | |
| return label.getName(); | |
| } | |
| @Override | |
| public Package getPackage() { | |
| return containingPackage; | |
| } | |
| @Override | |
| public String getTargetKind() { | |
| return targetKind(); | |
| } | |
| @Override | |
| public Rule getAssociatedRule() { | |
| return null; | |
| } | |
| @Override | |
| public License getLicense() { | |
| return License.NO_LICENSE; | |
| } | |
| @Override | |
| public Location getLocation() { | |
| return location; | |
| } | |
| @Override | |
| public String toString() { | |
| return targetKind() + " " + getLabel(); | |
| } | |
| @Override | |
| public Set<License.DistributionType> getDistributions() { | |
| return Collections.emptySet(); | |
| } | |
| @Override | |
| public RuleVisibility getVisibility() { | |
| return ConstantRuleVisibility.PRIVATE; // No rule should be referencing an environment_group. | |
| } | |
| @Override | |
| public boolean isConfigurable() { | |
| return false; | |
| } | |
| public static String targetKind() { | |
| return "environment group"; | |
| } | |
| } |