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