|  | // 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 com.google.common.base.Joiner; | 
|  | import com.google.common.collect.ImmutableCollection; | 
|  | import com.google.common.collect.ImmutableMap; | 
|  | import com.google.common.collect.ImmutableSet; | 
|  | 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.RuleConfiguredTarget.Mode; | 
|  | import com.google.devtools.build.lib.cmdline.Label; | 
|  | import com.google.devtools.build.lib.cmdline.LabelSyntaxException; | 
|  | import com.google.devtools.build.lib.packages.BuildType; | 
|  | import com.google.devtools.build.lib.packages.OutputFile; | 
|  | import com.google.devtools.build.lib.rules.AliasProvider; | 
|  | 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; | 
|  |  | 
|  | /** | 
|  | * Expands $(location) tags inside target attributes. | 
|  | * You can specify something like this in the BUILD file: | 
|  | * | 
|  | * somerule(name='some name', | 
|  | *          someopt = [ '$(location //mypackage:myhelper)' ], | 
|  | *          ...) | 
|  | * | 
|  | * and location will be substituted with //mypackage:myhelper executable output. | 
|  | * Note that //mypackage:myhelper should have just one output. | 
|  | */ | 
|  | public class LocationExpander { | 
|  |  | 
|  | /** | 
|  | * List of options to tweak the LocationExpander. | 
|  | */ | 
|  | public static enum Options { | 
|  | /** output the execPath instead of the relative path */ | 
|  | EXEC_PATHS, | 
|  | /** Allow to take label from the data attribute */ | 
|  | ALLOW_DATA, | 
|  | } | 
|  |  | 
|  | private static final int MAX_PATHS_SHOWN = 5; | 
|  | private static final String LOCATION = "$(location"; | 
|  | private final RuleContext ruleContext; | 
|  | private final ImmutableSet<Options> options; | 
|  |  | 
|  | /** | 
|  | * This is a Map, not a Multimap, because we need to distinguish between the cases of "empty | 
|  | * value" and "absent key." | 
|  | */ | 
|  | private Map<Label, Collection<Artifact>> locationMap; | 
|  | private ImmutableMap<Label, ImmutableCollection<Artifact>> labelMap; | 
|  |  | 
|  | /** | 
|  | * 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 allowDataAttributeEntriesInLabel set to true if the <code>data</code> attribute should | 
|  | *        be used too. | 
|  | */ | 
|  | public LocationExpander( | 
|  | RuleContext ruleContext, ImmutableMap<Label, ImmutableCollection<Artifact>> labelMap, | 
|  | boolean allowDataAttributeEntriesInLabel) { | 
|  | this.ruleContext = ruleContext; | 
|  | ImmutableSet.Builder<Options> builder = ImmutableSet.builder(); | 
|  | builder.add(Options.EXEC_PATHS); | 
|  | if (allowDataAttributeEntriesInLabel) { | 
|  | builder.add(Options.ALLOW_DATA); | 
|  | } | 
|  | this.options = builder.build(); | 
|  | this.labelMap = labelMap; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Creates location expander helper bound to specific target. | 
|  | * | 
|  | * @param ruleContext the BUILD rule's context | 
|  | * @param options the list of options, see {@link Options}. | 
|  | */ | 
|  | public LocationExpander(RuleContext ruleContext, ImmutableSet<Options> options) { | 
|  | this.ruleContext = ruleContext; | 
|  | this.options = options; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Creates location expander helper bound to specific target. | 
|  | * | 
|  | * @param ruleContext the BUILD rule's context | 
|  | * @param options the list of options, see {@link Options}. | 
|  | */ | 
|  | public LocationExpander(RuleContext ruleContext, Options... options) { | 
|  | this.ruleContext = ruleContext; | 
|  | this.options = ImmutableSet.copyOf(options); | 
|  | } | 
|  |  | 
|  | private Map<Label, Collection<Artifact>> getLocationMap() { | 
|  | if (locationMap == null) { | 
|  | locationMap = buildLocationMap(ruleContext, labelMap, options.contains(Options.ALLOW_DATA)); | 
|  | } | 
|  | return locationMap; | 
|  | } | 
|  |  | 
|  | public String expand(String input) { | 
|  | return expand(input, new RuleErrorReporter()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Expands attribute's location and locations tags based on the target and | 
|  | * location map. | 
|  | * | 
|  | * @param attrName  name of the attribute | 
|  | * @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(attrName)); | 
|  | } | 
|  |  | 
|  | private String expand(String value, ErrorReporter reporter) { | 
|  | int restart = 0; | 
|  |  | 
|  | int attrLength = value.length(); | 
|  | StringBuilder result = new StringBuilder(value.length()); | 
|  |  | 
|  | while (true) { | 
|  | // (1) find '$(location ' or '$(locations ' | 
|  | String message = "$(location)"; | 
|  | boolean multiple = false; | 
|  | int start = value.indexOf(LOCATION, restart); | 
|  | int scannedLength = LOCATION.length(); | 
|  | if (start == -1 || start + scannedLength == attrLength) { | 
|  | result.append(value.substring(restart)); | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (value.charAt(start + scannedLength) == 's') { | 
|  | scannedLength++; | 
|  | if (start + scannedLength == attrLength) { | 
|  | result.append(value.substring(restart)); | 
|  | break; | 
|  | } | 
|  | message = "$(locations)"; | 
|  | multiple = true; | 
|  | } | 
|  |  | 
|  | if (value.charAt(start + scannedLength) != ' ') { | 
|  | result.append(value, restart, start + scannedLength); | 
|  | restart = start + scannedLength; | 
|  | continue; | 
|  | } | 
|  | scannedLength++; | 
|  |  | 
|  | int end = value.indexOf(')', start + scannedLength); | 
|  | if (end == -1) { | 
|  | reporter.report(ruleContext, "unterminated " + message + " expression"); | 
|  | return value; | 
|  | } | 
|  |  | 
|  | message = String.format(" in %s expression", message); | 
|  |  | 
|  | // (2) parse label | 
|  | String labelText = value.substring(start + scannedLength, end).trim(); | 
|  | Label label = parseLabel(labelText, message, reporter); | 
|  |  | 
|  | if (label == null) { | 
|  | // Error was already reported in parseLabel() | 
|  | return value; | 
|  | } | 
|  |  | 
|  | // (3) expand label; stop this operation if there is an error | 
|  | try { | 
|  | Collection<String> paths = resolveLabel(label, message, multiple); | 
|  | result.append(value, restart, start); | 
|  |  | 
|  | if (multiple) { | 
|  | Joiner.on(' ').appendTo(result, paths); | 
|  | } else { | 
|  | result.append(Iterables.getOnlyElement(paths)); | 
|  | } | 
|  | } catch (IllegalStateException ise) { | 
|  | reporter.report(ruleContext, ise.getMessage()); | 
|  | return value; | 
|  | } | 
|  |  | 
|  | restart = end + 1; | 
|  | } | 
|  |  | 
|  | return result.toString(); | 
|  | } | 
|  |  | 
|  | private Label parseLabel(String labelText, String message, ErrorReporter reporter) { | 
|  | try { | 
|  | return ruleContext.getLabel().getRelative(labelText); | 
|  | } catch (LabelSyntaxException e) { | 
|  | reporter.report(ruleContext, String.format("invalid label%s: %s", message, e.getMessage())); | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns all possible target location(s) of the given label | 
|  | * @param message Original message, for error reporting purposes only | 
|  | * @param hasMultipleTargets Describes whether the label has multiple target locations | 
|  | * @return The collection of all path strings | 
|  | */ | 
|  | private Collection<String> resolveLabel( | 
|  | Label unresolved, String message, boolean hasMultipleTargets) throws IllegalStateException { | 
|  | // replace with singleton artifact, iff unique. | 
|  | Collection<Artifact> artifacts = getLocationMap().get(unresolved); | 
|  |  | 
|  | if (artifacts == null) { | 
|  | throw new IllegalStateException( | 
|  | "label '" + unresolved + "'" + message + " is not a declared prerequisite of this rule"); | 
|  | } | 
|  |  | 
|  | Set<String> paths = getPaths(artifacts, options.contains(Options.EXEC_PATHS)); | 
|  |  | 
|  | if (paths.isEmpty()) { | 
|  | throw new IllegalStateException( | 
|  | "label '" + unresolved + "'" + message + " expression expands to no files"); | 
|  | } | 
|  |  | 
|  | if (!hasMultipleTargets && paths.size() > 1) { | 
|  | throw new IllegalStateException( | 
|  | String.format( | 
|  | "label '%s'%s expands to more than one file, " | 
|  | + "please use $(locations %s) instead.  Files (at most %d shown) are: %s", | 
|  | unresolved, | 
|  | message, | 
|  | unresolved, | 
|  | MAX_PATHS_SHOWN, | 
|  | Iterables.limit(paths, MAX_PATHS_SHOWN))); | 
|  | } | 
|  |  | 
|  | return paths; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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 | 
|  | */ | 
|  | private 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()); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Add all destination locations. | 
|  | for (OutputFile out : ruleContext.getRule().getOutputFiles()) { | 
|  | 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)) { | 
|  | Iterables.addAll(mapGet(locationMap, AliasProvider.getDependencyLabel(src)), | 
|  | src.getProvider(FileProvider.class).getFilesToBuild()); | 
|  | } | 
|  | } | 
|  |  | 
|  | // 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.DATA, 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()); | 
|  | } | 
|  | } | 
|  | return locationMap; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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 static 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; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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(RuleContext ctx, String error); | 
|  | } | 
|  |  | 
|  | private static final class AttributeErrorReporter implements ErrorReporter { | 
|  | private final String attrName; | 
|  |  | 
|  | public AttributeErrorReporter(String attrName) { | 
|  | this.attrName = attrName; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void report(RuleContext ctx, String error) { | 
|  | ctx.attributeError(attrName, error); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static final class RuleErrorReporter implements ErrorReporter { | 
|  | @Override | 
|  | public void report(RuleContext ctx, String error) { | 
|  | ctx.ruleError(error); | 
|  | } | 
|  | } | 
|  | } |