blob: 34090af5e12a8b246c74c2f627330958715a3edf [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.packages;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
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 // This is a lie, but this object is only mutable until its containing package is loaded.
public class EnvironmentGroup implements Target {
private final EnvironmentLabels environmentLabels;
private final Location location;
private final Package containingPackage;
/**
* 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.environmentLabels = new EnvironmentLabels(label, environments, defaults);
this.location = location;
this.containingPackage = pkg;
}
public EnvironmentLabels getEnvironmentLabels() {
environmentLabels.checkInitialized();
return environmentLabels;
}
/**
* 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(environmentLabels.environments, new DifferentPackage(containingPackage))) {
events.add(
Event.error(
location,
environment + " is not in the same package as group " + environmentLabels.label));
}
// The defaults must be a subset of the member environments.
for (Label unknownDefault :
Sets.difference(environmentLabels.defaults, environmentLabels.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
* EnvironmentLabels#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 : environmentLabels.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);
}
}
}
}
Map<Label, NestedSet<Label>> fulfillersMap = new HashMap<>();
// 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.
environmentLabels.assertNotInitialized();
for (Label envName : environmentLabels.environments) {
setTransitiveFulfillers(envName, directFulfillers, fulfillersMap);
}
environmentLabels.setFulfillersMap(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 (!environmentLabels.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 environmentLabels.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 environmentLabels.defaults;
}
@Override
public Label getLabel() {
return environmentLabels.label;
}
@Override
public String getName() {
return environmentLabels.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";
}
}