// 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"; } }