blob: d6c2992a5368981f3d48477497f7e27885c8160d [file] [log] [blame]
// Copyright 2014 Google Inc. 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.query2;
import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
import static com.google.devtools.build.lib.packages.Type.TRISTATE;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.cmdline.ResolvedTargets;
import com.google.devtools.build.lib.cmdline.TargetParsingException;
import com.google.devtools.build.lib.events.ErrorSensingEventHandler;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.graph.Digraph;
import com.google.devtools.build.lib.graph.Node;
import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.NoSuchThingException;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
import com.google.devtools.build.lib.packages.OutputFile;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.pkgcache.PackageProvider;
import com.google.devtools.build.lib.pkgcache.TargetEdgeObserver;
import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
import com.google.devtools.build.lib.pkgcache.TargetProvider;
import com.google.devtools.build.lib.query2.engine.BlazeQueryEvalResult;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment;
import com.google.devtools.build.lib.query2.engine.QueryException;
import com.google.devtools.build.lib.query2.engine.QueryExpression;
import com.google.devtools.build.lib.query2.engine.SkyframeRestartQueryException;
import com.google.devtools.build.lib.syntax.Label;
import com.google.devtools.build.lib.util.BinaryPredicate;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The environment of a Blaze query. Not thread-safe.
*/
public class BlazeQueryEnvironment implements QueryEnvironment<Target> {
protected final ErrorSensingEventHandler eventHandler;
private final TargetProvider targetProvider;
private final TargetPatternEvaluator targetPatternEvaluator;
private final Digraph<Target> graph = new Digraph<>();
private final ErrorPrintingTargetEdgeErrorObserver errorObserver;
private final LabelVisitor labelVisitor;
private final Map<String, Set<Target>> letBindings = new HashMap<>();
private final Map<String, ResolvedTargets<Target>> resolvedTargetPatterns = new HashMap<>();
protected final boolean keepGoing;
private final boolean strictScope;
protected final int loadingPhaseThreads;
private final BinaryPredicate<Rule, Attribute> dependencyFilter;
private final Predicate<Label> labelFilter;
private final Set<Setting> settings;
private final List<QueryFunction> extraFunctions;
private final BlazeTargetAccessor accessor = new BlazeTargetAccessor();
/**
* Note that the correct operation of this class critically depends on the Reporter being a
* singleton object, shared by all cooperating classes contributing to Query.
* @param strictScope if true, fail the whole query if a label goes out of scope.
* @param loadingPhaseThreads the number of threads to use during loading
* the packages for the query.
* @param labelFilter a predicate that determines if a specific label is
* allowed to be visited during query execution. If it returns false,
* the query execution is stopped with an error message.
* @param settings a set of enabled settings
*/
public BlazeQueryEnvironment(PackageProvider packageProvider,
TargetPatternEvaluator targetPatternEvaluator,
boolean keepGoing,
boolean strictScope,
int loadingPhaseThreads,
Predicate<Label> labelFilter,
EventHandler eventHandler,
Set<Setting> settings,
Iterable<QueryFunction> extraFunctions) {
this.eventHandler = new ErrorSensingEventHandler(eventHandler);
this.targetProvider = packageProvider;
this.targetPatternEvaluator = targetPatternEvaluator;
this.errorObserver = new ErrorPrintingTargetEdgeErrorObserver(this.eventHandler);
this.keepGoing = keepGoing;
this.strictScope = strictScope;
this.loadingPhaseThreads = loadingPhaseThreads;
this.dependencyFilter = constructDependencyFilter(settings);
this.labelVisitor = new LabelVisitor(packageProvider, dependencyFilter);
this.labelFilter = labelFilter;
this.settings = Sets.immutableEnumSet(settings);
this.extraFunctions = ImmutableList.copyOf(extraFunctions);
}
/**
* Note that the correct operation of this class critically depends on the Reporter being a
* singleton object, shared by all cooperating classes contributing to Query.
* @param loadingPhaseThreads the number of threads to use during loading
* the packages for the query.
* @param settings a set of enabled settings
*/
public BlazeQueryEnvironment(PackageProvider packageProvider,
TargetPatternEvaluator targetPatternEvaluator,
boolean keepGoing,
int loadingPhaseThreads,
EventHandler eventHandler,
Set<Setting> settings,
Iterable<QueryFunction> extraFunctions) {
this(packageProvider, targetPatternEvaluator, keepGoing, /*strictScope=*/true,
loadingPhaseThreads, Rule.ALL_LABELS, eventHandler, settings, extraFunctions);
}
private static BinaryPredicate<Rule, Attribute> constructDependencyFilter(Set<Setting> settings) {
BinaryPredicate<Rule, Attribute> specifiedFilter =
settings.contains(Setting.NO_HOST_DEPS) ? Rule.NO_HOST_DEPS : Rule.ALL_DEPS;
if (settings.contains(Setting.NO_IMPLICIT_DEPS)) {
specifiedFilter = Rule.and(specifiedFilter, Rule.NO_IMPLICIT_DEPS);
}
if (settings.contains(Setting.NO_NODEP_DEPS)) {
specifiedFilter = Rule.and(specifiedFilter, Rule.NO_NODEP_ATTRIBUTES);
}
return specifiedFilter;
}
/**
* Evaluate the specified query expression in this environment.
*
* @return a {@link BlazeQueryEvalResult} object that contains the resulting set of targets, the
* partial graph, and a bit to indicate whether errors occured during evaluation; note that the
* success status can only be false if {@code --keep_going} was in effect
* @throws QueryException if the evaluation failed and {@code --nokeep_going} was in
* effect
*/
public BlazeQueryEvalResult<Target> evaluateQuery(QueryExpression expr) throws QueryException {
// Some errors are reported as QueryExceptions and others as ERROR events
// (if --keep_going).
eventHandler.resetErrors();
resolvedTargetPatterns.clear();
// In the --nokeep_going case, errors are reported in the order in which the patterns are
// specified; using a linked hash set here makes sure that the left-most error is reported.
Set<String> targetPatternSet = new LinkedHashSet<>();
expr.collectTargetPatterns(targetPatternSet);
try {
resolvedTargetPatterns.putAll(preloadOrThrow(targetPatternSet));
} catch (TargetParsingException e) {
// Unfortunately, by evaluating the patterns in parallel, we lose some location information.
throw new QueryException(expr, e.getMessage());
}
Set<Target> resultNodes;
try {
resultNodes = expr.eval(this);
} catch (QueryException e) {
throw new QueryException(e, expr);
}
if (eventHandler.hasErrors()) {
if (!keepGoing) {
// This case represents loading-phase errors reported during evaluation
// of target patterns that don't cause evaluation to fail per se.
throw new QueryException("Evaluation of query \"" + expr
+ "\" failed due to BUILD file errors");
} else {
eventHandler.handle(Event.warn("--keep_going specified, ignoring errors. "
+ "Results may be inaccurate"));
}
}
return new BlazeQueryEvalResult<>(!eventHandler.hasErrors(), resultNodes, graph);
}
public BlazeQueryEvalResult<Target> evaluateQuery(String query) throws QueryException {
return evaluateQuery(QueryExpression.parse(query, this));
}
@Override
public void reportBuildFileError(QueryExpression caller, String message) throws QueryException {
if (!keepGoing) {
throw new QueryException(caller, message);
} else {
// Keep consistent with evaluateQuery() above.
eventHandler.handle(Event.error("Evaluation of query \"" + caller + "\" failed: " + message));
}
}
@Override
public Set<Target> getTargetsMatchingPattern(QueryExpression caller,
String pattern) throws QueryException {
// We can safely ignore the boolean error flag. The evaluateQuery() method above wraps the
// entire query computation in an error sensor.
Set<Target> targets = new LinkedHashSet<>(resolvedTargetPatterns.get(pattern).getTargets());
// Sets.filter would be more convenient here, but can't deal with exceptions.
Iterator<Target> targetIterator = targets.iterator();
while (targetIterator.hasNext()) {
Target target = targetIterator.next();
if (!validateScope(target.getLabel(), strictScope)) {
targetIterator.remove();
}
}
Set<PathFragment> packages = new HashSet<>();
for (Target target : targets) {
packages.add(target.getLabel().getPackageFragment());
}
Set<Target> result = new LinkedHashSet<>();
for (Target target : targets) {
result.add(getOrCreate(target));
// Preservation of graph order: it is important that targets obtained via
// a wildcard such as p:* are correctly ordered w.r.t. each other, so to
// ensure this, we add edges between any pair of directly connected
// targets in this set.
if (target instanceof OutputFile) {
OutputFile outputFile = (OutputFile) target;
if (targets.contains(outputFile.getGeneratingRule())) {
makeEdge(outputFile, outputFile.getGeneratingRule());
}
} else if (target instanceof Rule) {
Rule rule = (Rule) target;
for (Label label : rule.getLabels(dependencyFilter)) {
if (!packages.contains(label.getPackageFragment())) {
continue; // don't cause additional package loading
}
try {
if (!validateScope(label, strictScope)) {
continue; // Don't create edges to targets which are out of scope.
}
Target to = getTargetOrThrow(label);
if (targets.contains(to)) {
makeEdge(rule, to);
}
} catch (NoSuchThingException e) {
/* ignore */
} catch (InterruptedException e) {
throw new QueryException("interrupted");
}
}
}
}
return result;
}
public Node<Target> getTarget(Label label) throws TargetNotFoundException, QueryException {
// Can't use strictScope here because we are expecting a target back.
validateScope(label, true);
try {
return getNode(getTargetOrThrow(label));
} catch (NoSuchThingException e) {
throw new TargetNotFoundException(e);
} catch (InterruptedException e) {
throw new QueryException("interrupted");
}
}
private Node<Target> getNode(Target target) {
return graph.createNode(target);
}
private Collection<Node<Target>> getNodes(Iterable<Target> target) {
Set<Node<Target>> result = new LinkedHashSet<>();
for (Target t : target) {
result.add(getNode(t));
}
return result;
}
@Override
public Target getOrCreate(Target target) {
return getNode(target).getLabel();
}
@Override
public Collection<Target> getFwdDeps(Target target) {
return getTargetsFromNodes(getNode(target).getSuccessors());
}
@Override
public Collection<Target> getReverseDeps(Target target) {
return getTargetsFromNodes(getNode(target).getPredecessors());
}
@Override
public Set<Target> getTransitiveClosure(Set<Target> targetNodes) {
for (Target node : targetNodes) {
checkBuilt(node);
}
return getTargetsFromNodes(graph.getFwdReachable(getNodes(targetNodes)));
}
/**
* Checks that the graph rooted at 'targetNode' has been completely built;
* fails if not. Callers of {@link #getTransitiveClosure} must ensure that
* {@link #buildTransitiveClosure} has been called before.
*
* <p>It would be inefficient and failure-prone to make getTransitiveClosure
* call buildTransitiveClosure directly. Also, it would cause
* nondeterministic behavior of the operators, since the set of packages
* loaded (and hence errors reported) would depend on the ordering details of
* the query operators' implementations.
*/
private void checkBuilt(Target targetNode) {
Preconditions.checkState(
labelVisitor.hasVisited(targetNode.getLabel()),
"getTransitiveClosure(%s) called without prior call to buildTransitiveClosure()",
targetNode);
}
protected void preloadTransitiveClosure(Set<Target> targets, int maxDepth) throws QueryException {
}
@Override
public void buildTransitiveClosure(QueryExpression caller,
Set<Target> targetNodes,
int maxDepth) throws QueryException {
Set<Target> targets = targetNodes;
preloadTransitiveClosure(targets, maxDepth);
try {
labelVisitor.syncWithVisitor(eventHandler, targets, keepGoing,
loadingPhaseThreads, maxDepth, errorObserver, new GraphBuildingObserver());
} catch (InterruptedException e) {
throw new QueryException(caller, "transitive closure computation was interrupted");
}
if (errorObserver.hasErrors()) {
reportBuildFileError(caller, "errors were encountered while computing transitive closure");
}
}
@Override
public Set<Target> getNodesOnPath(Target from, Target to) {
return getTargetsFromNodes(graph.getShortestPath(getNode(from), getNode(to)));
}
@Override
public Set<Target> getVariable(String name) {
return letBindings.get(name);
}
@Override
public Set<Target> setVariable(String name, Set<Target> value) {
return letBindings.put(name, value);
}
/**
* It suffices to synchronize the modifications of this.graph from within the
* GraphBuildingObserver, because that's the only concurrent part.
* Concurrency is always encapsulated within the evaluation of a single query
* operator (e.g. deps(), somepath(), etc).
*/
private class GraphBuildingObserver implements TargetEdgeObserver {
@Override
public synchronized void edge(Target from, Attribute attribute, Target to) {
Preconditions.checkState(attribute == null ||
dependencyFilter.apply(((Rule) from), attribute),
"Disallowed edge from LabelVisitor: %s --> %s", from, to);
makeEdge(from, to);
}
@Override
public synchronized void node(Target node) {
graph.createNode(node);
}
@Override
public void missingEdge(Target target, Label to, NoSuchThingException e) {
// No - op.
}
}
private void makeEdge(Target from, Target to) {
graph.addEdge(from, to);
}
private boolean validateScope(Label label, boolean strict) throws QueryException {
if (!labelFilter.apply(label)) {
String error = String.format("target '%s' is not within the scope of the query", label);
if (strict) {
throw new QueryException(error);
} else {
eventHandler.handle(Event.warn(error + ". Skipping"));
return false;
}
}
return true;
}
public Set<Target> evalTargetPattern(QueryExpression caller, String pattern)
throws QueryException {
if (!resolvedTargetPatterns.containsKey(pattern)) {
try {
resolvedTargetPatterns.putAll(preloadOrThrow(ImmutableList.of(pattern)));
} catch (TargetParsingException e) {
// Will skip the target and keep going if -k is specified.
resolvedTargetPatterns.put(pattern, ResolvedTargets.<Target>empty());
reportBuildFileError(caller, e.getMessage());
}
}
return getTargetsMatchingPattern(caller, pattern);
}
private Map<String, ResolvedTargets<Target>> preloadOrThrow(Collection<String> patterns)
throws TargetParsingException {
try {
// Note that this may throw a RuntimeException if deps are missing in Skyframe.
return targetPatternEvaluator.preloadTargetPatterns(
eventHandler, patterns, keepGoing);
} catch (InterruptedException e) {
// TODO(bazel-team): Propagate the InterruptedException from here [skyframe-loading].
throw new TargetParsingException("interrupted");
}
}
private Target getTargetOrThrow(Label label)
throws NoSuchThingException, SkyframeRestartQueryException, InterruptedException {
Target target = targetProvider.getTarget(eventHandler, label);
if (target == null) {
throw new SkyframeRestartQueryException();
}
return target;
}
// TODO(bazel-team): rename this to getDependentFiles when all implementations
// of QueryEnvironment is fixed.
@Override
public Set<Target> getBuildFiles(final QueryExpression caller, Set<Target> nodes)
throws QueryException {
Set<Target> dependentFiles = new LinkedHashSet<>();
Set<Package> seenPackages = new HashSet<>();
// Keep track of seen labels, to avoid adding a fake subinclude label that also exists as a
// real target.
Set<Label> seenLabels = new HashSet<>();
// Adds all the package definition files (BUILD files and build
// extensions) for package "pkg", to "buildfiles".
for (Target x : nodes) {
Package pkg = x.getPackage();
if (seenPackages.add(pkg)) {
addIfUniqueLabel(getNode(pkg.getBuildFile()), seenLabels, dependentFiles);
for (Label subinclude
: Iterables.concat(pkg.getSubincludeLabels(), pkg.getSkylarkFileDependencies())) {
addIfUniqueLabel(getSubincludeTarget(subinclude, pkg), seenLabels, dependentFiles);
// Also add the BUILD file of the subinclude.
try {
addIfUniqueLabel(getSubincludeTarget(
subinclude.getLocalTargetLabel("BUILD"), pkg), seenLabels, dependentFiles);
} catch (Label.SyntaxException e) {
throw new AssertionError("BUILD should always parse as a target name", e);
}
}
}
}
return dependentFiles;
}
private static void addIfUniqueLabel(Node<Target> node, Set<Label> labels, Set<Target> nodes) {
if (labels.add(node.getLabel().getLabel())) {
nodes.add(node.getLabel());
}
}
private Node<Target> getSubincludeTarget(final Label label, Package pkg) {
return getNode(new FakeSubincludeTarget(label, pkg.getBuildFile().getLocation()));
}
@Override
public TargetAccessor<Target> getAccessor() {
return accessor;
}
@Override
public boolean isSettingEnabled(Setting setting) {
return settings.contains(Preconditions.checkNotNull(setting));
}
@Override
public Iterable<QueryFunction> getFunctions() {
ImmutableList.Builder<QueryFunction> builder = ImmutableList.builder();
builder.addAll(DEFAULT_QUERY_FUNCTIONS);
builder.addAll(extraFunctions);
return builder.build();
}
private final class BlazeTargetAccessor implements TargetAccessor<Target> {
@Override
public String getTargetKind(Target target) {
return target.getTargetKind();
}
@Override
public String getLabel(Target target) {
return target.getLabel().toString();
}
@Override
public List<Target> getLabelListAttr(QueryExpression caller, Target target, String attrName,
String errorMsgPrefix) throws QueryException {
Preconditions.checkArgument(target instanceof Rule);
List<Target> result = new ArrayList<>();
Rule rule = (Rule) target;
AggregatingAttributeMapper attrMap = AggregatingAttributeMapper.of(rule);
Type<?> attrType = attrMap.getAttributeType(attrName);
if (attrType == null) {
// Return an empty list if the attribute isn't defined for this rule.
return ImmutableList.of();
}
for (Object value : attrMap.visitAttribute(attrName, attrType)) {
// Computed defaults may have null values.
if (value != null) {
for (Label label : attrType.getLabels(value)) {
try {
result.add(getTarget(label).getLabel());
} catch (TargetNotFoundException e) {
reportBuildFileError(caller, errorMsgPrefix + e.getMessage());
}
}
}
}
return result;
}
@Override
public List<String> getStringListAttr(Target target, String attrName) {
Preconditions.checkArgument(target instanceof Rule);
return NonconfigurableAttributeMapper.of((Rule) target).get(attrName, Type.STRING_LIST);
}
@Override
public String getStringAttr(Target target, String attrName) {
Preconditions.checkArgument(target instanceof Rule);
return NonconfigurableAttributeMapper.of((Rule) target).get(attrName, Type.STRING);
}
@Override
public Iterable<String> getAttrAsString(Target target, String attrName) {
Preconditions.checkArgument(target instanceof Rule);
List<String> values = new ArrayList<>(); // May hold null values.
Attribute attribute = ((Rule) target).getAttributeDefinition(attrName);
if (attribute != null) {
Type<?> attributeType = attribute.getType();
for (Object attrValue : AggregatingAttributeMapper.of((Rule) target).visitAttribute(
attribute.getName(), attributeType)) {
// Ugly hack to maintain backward 'attr' query compatibility for BOOLEAN and TRISTATE
// attributes. These are internally stored as actual Boolean or TriState objects but were
// historically queried as integers. To maintain compatibility, we inspect their actual
// value and return the integer equivalent represented as a String. This code is the
// opposite of the code in BooleanType and TriStateType respectively.
if (attributeType == BOOLEAN) {
values.add(Type.BOOLEAN.cast(attrValue) ? "1" : "0");
} else if (attributeType == TRISTATE) {
switch (Type.TRISTATE.cast(attrValue)) {
case AUTO :
values.add("-1");
break;
case NO :
values.add("0");
break;
case YES :
values.add("1");
break;
default :
throw new AssertionError("This can't happen!");
}
} else {
values.add(attrValue == null ? null : attrValue.toString());
}
}
}
return values;
}
@Override
public boolean isRule(Target target) {
return target instanceof Rule;
}
@Override
public boolean isTestRule(Target target) {
return TargetUtils.isTestRule(target);
}
@Override
public boolean isTestSuite(Target target) {
return TargetUtils.isTestSuiteRule(target);
}
}
/** Given a set of target nodes, returns the targets. */
private static Set<Target> getTargetsFromNodes(Iterable<Node<Target>> input) {
Set<Target> result = new LinkedHashSet<>();
for (Node<Target> node : input) {
result.add(node.getLabel());
}
return result;
}
}