blob: bfd1bb16b79dafd57bd20ef76092581da4c2f253 [file] [log] [blame]
// Copyright 2014 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;
import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.OutputFile;
import com.google.devtools.build.lib.packages.RuleErrorConsumer;
import com.google.devtools.build.lib.util.ShellEscaper;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.Nullable;
/**
* Expands $(location) and $(locations) tags inside target attributes. You can specify something
* like this in the BUILD file:
*
* <pre>
* somerule(name='some name',
* someopt = [ '$(location //mypackage:myhelper)' ],
* ...)
* </pre>
*
* and location will be substituted with //mypackage:myhelper executable output.
*
* <p>Note that this expander will always expand labels in srcs, deps, and tools attributes, with
* data being optional.
*
* <p>DO NOT USE DIRECTLY! Use RuleContext.getExpander() instead.
*/
public final class LocationExpander {
private static final boolean EXACTLY_ONE = false;
private static final boolean ALLOW_MULTIPLE = true;
private static final boolean USE_ROOT_PATHS = false;
private static final boolean USE_EXEC_PATHS = true;
private final RuleErrorConsumer ruleErrorConsumer;
private final ImmutableMap<String, LocationFunction> functions;
private final ImmutableMap<RepositoryName, RepositoryName> repositoryMapping;
@VisibleForTesting
LocationExpander(
RuleErrorConsumer ruleErrorConsumer,
Map<String, LocationFunction> functions,
ImmutableMap<RepositoryName, RepositoryName> repositoryMapping) {
this.ruleErrorConsumer = ruleErrorConsumer;
this.functions = ImmutableMap.copyOf(functions);
this.repositoryMapping = repositoryMapping;
}
private LocationExpander(
RuleErrorConsumer ruleErrorConsumer,
Label root,
Supplier<Map<Label, Collection<Artifact>>> locationMap,
boolean execPaths,
ImmutableMap<RepositoryName, RepositoryName> repositoryMapping) {
this(ruleErrorConsumer, allLocationFunctions(root, locationMap, execPaths), repositoryMapping);
}
/**
* Creates location expander helper bound to specific target and with default location map.
*
* @param ruleContext BUILD rule
* @param labelMap A mapping of labels to build artifacts.
* @param execPaths If true, this expander will expand $(location)/$(locations) using
* Artifact.getExecPath(); otherwise with Artifact.getRootRelativePath().
* @param allowData If true, this expander will expand locations from the `data` attribute;
* otherwise it will not.
*/
private LocationExpander(
RuleContext ruleContext,
@Nullable ImmutableMap<Label, ImmutableCollection<Artifact>> labelMap,
boolean execPaths,
boolean allowData) {
this(
ruleContext,
ruleContext.getLabel(),
// Use a memoizing supplier to avoid eagerly building the location map.
Suppliers.memoize(
() -> LocationExpander.buildLocationMap(ruleContext, labelMap, allowData)),
execPaths,
ruleContext.getRule().getPackage().getRepositoryMapping());
}
/**
* Creates an expander that expands $(location)/$(locations) using Artifact.getRootRelativePath().
*
* <p>The expander expands $(rootpath)/$(rootpaths) using Artifact.getRootRelativePath(), and
* $(execpath)/$(execpaths) using Artifact.getExecPath().
*
* @param ruleContext BUILD rule
*/
public static LocationExpander withRunfilesPaths(RuleContext ruleContext) {
return new LocationExpander(ruleContext, null, false, false);
}
/**
* Creates an expander that expands $(location)/$(locations) using Artifact.getExecPath().
*
* <p>The expander expands $(rootpath)/$(rootpaths) using Artifact.getRootRelativePath(), and
* $(execpath)/$(execpaths) using Artifact.getExecPath().
*
* @param ruleContext BUILD rule
* @param labelMap A mapping of labels to build artifacts.
*/
public static LocationExpander withExecPaths(
RuleContext ruleContext, ImmutableMap<Label, ImmutableCollection<Artifact>> labelMap) {
return new LocationExpander(ruleContext, labelMap, true, false);
}
/**
* Creates an expander that expands $(location)/$(locations) using Artifact.getExecPath().
*
* <p>The expander expands $(rootpath)/$(rootpaths) using Artifact.getRootRelativePath(), and
* $(execpath)/$(execpaths) using Artifact.getExecPath().
*
* @param ruleContext BUILD rule
* @param labelMap A mapping of labels to build artifacts.
*/
public static LocationExpander withExecPathsAndData(
RuleContext ruleContext, ImmutableMap<Label, ImmutableCollection<Artifact>> labelMap) {
return new LocationExpander(ruleContext, labelMap, true, true);
}
public String expand(String input) {
return expand(input, new RuleErrorReporter(ruleErrorConsumer));
}
/**
* Expands attribute's location and locations tags based on the target and
* location map.
*
* @param attrName name of the attribute; only used for error reporting
* @param attrValue initial value of the attribute
* @return attribute value with expanded location tags or original value in
* case of errors
*/
public String expandAttribute(String attrName, String attrValue) {
return expand(attrValue, new AttributeErrorReporter(ruleErrorConsumer, attrName));
}
private String expand(String value, ErrorReporter reporter) {
int restart = 0;
StringBuilder result = new StringBuilder(value.length());
while (true) {
// (1) Find '$(<fname> '.
int start = value.indexOf("$(", restart);
if (start == -1) {
result.append(value.substring(restart));
break;
}
int nextWhitespace = value.indexOf(' ', start);
if (nextWhitespace == -1) {
result.append(value, restart, start + 2);
restart = start + 2;
continue;
}
String fname = value.substring(start + 2, nextWhitespace);
if (!functions.containsKey(fname)) {
result.append(value, restart, start + 2);
restart = start + 2;
continue;
}
result.append(value, restart, start);
int end = value.indexOf(')', nextWhitespace);
if (end == -1) {
reporter.report(
String.format(
"unterminated $(%s) expression",
value.substring(start + 2, nextWhitespace)));
return value;
}
// (2) Call appropriate function to obtain string replacement.
String functionValue = value.substring(nextWhitespace + 1, end).trim();
try {
String replacement = functions.get(fname).apply(functionValue, repositoryMapping);
result.append(replacement);
} catch (IllegalStateException ise) {
reporter.report(ise.getMessage());
return value;
}
restart = end + 1;
}
return result.toString();
}
@VisibleForTesting
static final class LocationFunction {
private static final int MAX_PATHS_SHOWN = 5;
private final Label root;
private final Supplier<Map<Label, Collection<Artifact>>> locationMapSupplier;
private final boolean execPaths;
private final boolean multiple;
LocationFunction(
Label root,
Supplier<Map<Label, Collection<Artifact>>> locationMapSupplier,
boolean execPaths,
boolean multiple) {
this.root = root;
this.locationMapSupplier = locationMapSupplier;
this.execPaths = execPaths;
this.multiple = multiple;
}
/**
* Looks up the label-like string in the locationMap and returns the resolved path string. If
* the label-like string begins with a repository name, the repository name may be remapped
* using the {@code repositoryMapping}.
*
* @param arg The label-like string to be expanded, e.g. ":foo" or "//foo:bar"
* @param repositoryMapping map of {@code RepositoryName}s defined in the main workspace
* @return The expanded value
*/
public String apply(
String arg, ImmutableMap<RepositoryName, RepositoryName> repositoryMapping) {
Label label;
try {
label = root.getRelativeWithRemapping(arg, repositoryMapping);
} catch (LabelSyntaxException e) {
throw new IllegalStateException(
String.format(
"invalid label in %s expression: %s", functionName(), e.getMessage()), e);
}
Collection<String> paths = resolveLabel(label);
return joinPaths(paths);
}
/**
* Returns all target location(s) of the given label.
*/
private Collection<String> resolveLabel(Label unresolved) throws IllegalStateException {
Collection<Artifact> artifacts = locationMapSupplier.get().get(unresolved);
if (artifacts == null) {
throw new IllegalStateException(
String.format(
"label '%s' in %s expression is not a declared prerequisite of this rule",
unresolved, functionName()));
}
Set<String> paths = getPaths(artifacts, execPaths);
if (paths.isEmpty()) {
throw new IllegalStateException(
String.format(
"label '%s' in %s expression expands to no files",
unresolved, functionName()));
}
if (!multiple && paths.size() > 1) {
throw new IllegalStateException(
String.format(
"label '%s' in $(location) expression expands to more than one file, "
+ "please use $(locations %s) instead. Files (at most %d shown) are: %s",
unresolved,
unresolved,
MAX_PATHS_SHOWN,
Iterables.limit(paths, MAX_PATHS_SHOWN)));
}
return paths;
}
/**
* Extracts list of all executables associated with given collection of label
* artifacts.
*
* @param artifacts to get the paths of
* @param takeExecPath if false, the root relative path will be taken
* @return all associated executable paths
*/
private Set<String> getPaths(Collection<Artifact> artifacts, boolean takeExecPath) {
TreeSet<String> paths = Sets.newTreeSet();
for (Artifact artifact : artifacts) {
PathFragment execPath =
takeExecPath ? artifact.getExecPath() : artifact.getRootRelativePath();
if (execPath != null) { // omit middlemen etc
paths.add(execPath.getCallablePathString());
}
}
return paths;
}
private String joinPaths(Collection<String> paths) {
return paths.stream().map(ShellEscaper::escapeString).collect(joining(" "));
}
private String functionName() {
return multiple ? "$(locations)" : "$(location)";
}
}
static ImmutableMap<String, LocationFunction> allLocationFunctions(
Label root, Supplier<Map<Label, Collection<Artifact>>> locationMap, boolean execPaths) {
return new ImmutableMap.Builder<String, LocationFunction>()
.put("location", new LocationFunction(root, locationMap, execPaths, EXACTLY_ONE))
.put("locations", new LocationFunction(root, locationMap, execPaths, ALLOW_MULTIPLE))
.put("rootpath", new LocationFunction(root, locationMap, USE_ROOT_PATHS, EXACTLY_ONE))
.put("rootpaths", new LocationFunction(root, locationMap, USE_ROOT_PATHS, ALLOW_MULTIPLE))
.put("execpath", new LocationFunction(root, locationMap, USE_EXEC_PATHS, EXACTLY_ONE))
.put("execpaths", new LocationFunction(root, locationMap, USE_EXEC_PATHS, ALLOW_MULTIPLE))
.build();
}
/**
* Extracts all possible target locations from target specification.
*
* @param ruleContext BUILD target object
* @param labelMap map of labels to build artifacts
* @return map of all possible target locations
*/
static Map<Label, Collection<Artifact>> buildLocationMap(
RuleContext ruleContext,
Map<Label, ? extends Collection<Artifact>> labelMap,
boolean allowDataAttributeEntriesInLabel) {
Map<Label, Collection<Artifact>> locationMap = Maps.newHashMap();
if (labelMap != null) {
for (Map.Entry<Label, ? extends Collection<Artifact>> entry : labelMap.entrySet()) {
mapGet(locationMap, entry.getKey()).addAll(entry.getValue());
}
}
// We don't want to do this if we're processing aspect rules. It will
// create output artifacts and unbalance the input/output state, leading
// to an error (output artifact with no action to create its inputs).
if (ruleContext.getMainAspect() == null) {
// Add all destination locations.
for (OutputFile out : ruleContext.getRule().getOutputFiles()) {
// Not in aspect processing, so explicitly build an artifact & let it verify.
mapGet(locationMap, out.getLabel()).add(ruleContext.createOutputArtifact(out));
}
}
if (ruleContext.getRule().isAttrDefined("srcs", BuildType.LABEL_LIST)) {
for (TransitiveInfoCollection src : ruleContext
.getPrerequisitesIf("srcs", Mode.TARGET, FileProvider.class)) {
mapGet(locationMap, AliasProvider.getDependencyLabel(src))
.addAll(src.getProvider(FileProvider.class).getFilesToBuild().toList());
}
}
// Add all locations associated with dependencies and tools
List<TransitiveInfoCollection> depsDataAndTools = new ArrayList<>();
if (ruleContext.getRule().isAttrDefined("deps", BuildType.LABEL_LIST)) {
Iterables.addAll(depsDataAndTools,
ruleContext.getPrerequisitesIf("deps", Mode.DONT_CHECK, FilesToRunProvider.class));
}
if (allowDataAttributeEntriesInLabel
&& ruleContext.getRule().isAttrDefined("data", BuildType.LABEL_LIST)) {
Iterables.addAll(depsDataAndTools,
ruleContext.getPrerequisitesIf("data", Mode.DONT_CHECK, FilesToRunProvider.class));
}
if (ruleContext.getRule().isAttrDefined("tools", BuildType.LABEL_LIST)) {
Iterables.addAll(depsDataAndTools,
ruleContext.getPrerequisitesIf("tools", Mode.HOST, FilesToRunProvider.class));
}
for (TransitiveInfoCollection dep : depsDataAndTools) {
Label label = AliasProvider.getDependencyLabel(dep);
FilesToRunProvider filesToRun = dep.getProvider(FilesToRunProvider.class);
Artifact executableArtifact = filesToRun.getExecutable();
// If the label has an executable artifact add that to the multimaps.
if (executableArtifact != null) {
mapGet(locationMap, label).add(executableArtifact);
} else {
mapGet(locationMap, label).addAll(filesToRun.getFilesToRun().toList());
}
}
return locationMap;
}
/**
* Returns the value in the specified map corresponding to 'key', creating and
* inserting an empty container if absent. We use Map not Multimap because
* we need to distinguish the cases of "empty value" and "absent key".
*
* @return the value in the specified map corresponding to 'key'
*/
private static <K, V> Collection<V> mapGet(Map<K, Collection<V>> map, K key) {
Collection<V> values = map.get(key);
if (values == null) {
// We use sets not lists, because it's conceivable that the same label
// could appear twice, in "srcs" and "deps".
values = Sets.newHashSet();
map.put(key, values);
}
return values;
}
private static interface ErrorReporter {
void report(String error);
}
private static final class AttributeErrorReporter implements ErrorReporter {
private final RuleErrorConsumer delegate;
private final String attrName;
public AttributeErrorReporter(RuleErrorConsumer delegate, String attrName) {
this.delegate = delegate;
this.attrName = attrName;
}
@Override
public void report(String error) {
delegate.attributeError(attrName, error);
}
}
private static final class RuleErrorReporter implements ErrorReporter {
private final RuleErrorConsumer delegate;
public RuleErrorReporter(RuleErrorConsumer delegate) {
this.delegate = delegate;
}
@Override
public void report(String error) {
delegate.ruleError(error);
}
}
}