blob: ae70699c1372711a10ad579b550e96ae48b6bd84 [file] [log] [blame]
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01001// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.analysis;
16
17import com.google.common.base.Joiner;
18import com.google.common.collect.Iterables;
19import com.google.common.collect.Lists;
20import com.google.common.collect.Sets;
21import com.google.devtools.build.lib.actions.Artifact;
22import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
23import com.google.devtools.build.lib.packages.OutputFile;
24import com.google.devtools.build.lib.packages.Type;
25import com.google.devtools.build.lib.syntax.Label;
26import com.google.devtools.build.lib.vfs.PathFragment;
27
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.HashMap;
32import java.util.List;
33import java.util.Map;
34
35/**
36 * Expands $(location) tags inside target attributes.
37 * You can specify something like this in the BUILD file:
38 *
39 * somerule(name='some name',
40 * someopt = [ '$(location //mypackage:myhelper)' ],
41 * ...)
42 *
43 * and location will be substituted with //mypackage:myhelper executable output.
44 * Note that //mypackage:myhelper should have just one output.
45 */
46public class LocationExpander {
47 private static final int MAX_PATHS_SHOWN = 5;
48 private static final String LOCATION = "$(location";
49 private final RuleContext ruleContext;
50 private Map<Label, Collection<Artifact>> locationMap;
51 private boolean allowDataAttributeEntriesInLabel = false;
52
53 /**
54 * Creates location expander helper bound to specific target and with default
55 * location map.
56 *
57 * @param ruleContext BUILD rule
58 */
59 public LocationExpander(RuleContext ruleContext) {
60 this(ruleContext, false);
61 }
62
63 public LocationExpander(RuleContext ruleContext,
64 boolean allowDataAttributeEntriesInLabel) {
65 this.ruleContext = ruleContext;
66 this.allowDataAttributeEntriesInLabel = allowDataAttributeEntriesInLabel;
67 }
68
69 public Map<Label, Collection<Artifact>> getLocationMap() {
70 if (locationMap == null) {
71 locationMap = buildLocationMap(ruleContext, allowDataAttributeEntriesInLabel);
72 }
73 return locationMap;
74 }
75
76 /**
77 * Expands attribute's location and locations tags based on the target and
78 * location map.
79 *
80 * @param attrName name of the attribute
81 * @param attrValue initial value of the attribute
82 * @return attribute value with expanded location tags or original value in
83 * case of errors
84 */
85 public String expand(String attrName, String attrValue) {
86 int restart = 0;
87
88 int attrLength = attrValue.length();
89 StringBuilder result = new StringBuilder(attrValue.length());
90
91 while (true) {
92 // (1) find '$(location ' or '$(locations '
93 String message = "$(location)";
94 boolean multiple = false;
95 int start = attrValue.indexOf(LOCATION, restart);
96 int scannedLength = LOCATION.length();
97 if (start == -1 || start + scannedLength == attrLength) {
98 result.append(attrValue.substring(restart));
99 break;
100 }
101
102 if (attrValue.charAt(start + scannedLength) == 's') {
103 scannedLength++;
104 if (start + scannedLength == attrLength) {
105 result.append(attrValue.substring(restart));
106 break;
107 }
108 message = "$(locations)";
109 multiple = true;
110 }
111
112 if (attrValue.charAt(start + scannedLength) != ' ') {
Ulf Adams07dba942015-03-05 14:47:37 +0000113 result.append(attrValue, restart, start + scannedLength);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100114 restart = start + scannedLength;
115 continue;
116 }
117 scannedLength++;
118
119 int end = attrValue.indexOf(')', start + scannedLength);
120 if (end == -1) {
121 ruleContext.attributeError(attrName, "unterminated " + message + " expression");
122 return attrValue;
123 }
124
125 // (2) parse label
126 String labelText = attrValue.substring(start + scannedLength, end);
127 Label label;
128 try {
129 label = ruleContext.getLabel().getRelative(labelText);
130 } catch (Label.SyntaxException e) {
131 ruleContext.attributeError(attrName,
132 "invalid label in " + message + " expression: " + e.getMessage());
133 return attrValue;
134 }
135
136 // (3) replace with singleton artifact, iff unique.
137 Collection<Artifact> artifacts = getLocationMap().get(label);
138 if (artifacts == null) {
139 ruleContext.attributeError(attrName,
140 "label '" + label + "' in " + message + " expression is not a "
141 + "declared prerequisite of this rule");
142 return attrValue;
143 }
144 List<String> paths = getPaths(artifacts);
145 if (paths.isEmpty()) {
146 ruleContext.attributeError(attrName,
147 "label '" + label + "' in " + message + " expression expands to no "
148 + "files");
149 return attrValue;
150 }
151
Ulf Adams07dba942015-03-05 14:47:37 +0000152 result.append(attrValue, restart, start);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100153 if (multiple) {
154 Collections.sort(paths);
155 Joiner.on(' ').appendTo(result, paths);
156 } else {
157 if (paths.size() > 1) {
158 ruleContext.attributeError(attrName,
159 String.format(
160 "label '%s' in %s expression expands to more than one file, "
161 + "please use $(locations %s) instead. Files (at most %d shown) are: %s",
162 label, message, label,
163 MAX_PATHS_SHOWN, Iterables.limit(paths, MAX_PATHS_SHOWN)));
164 return attrValue;
165 }
166 result.append(Iterables.getOnlyElement(paths));
167 }
168 restart = end + 1;
169 }
170 return result.toString();
171 }
172
173 /**
174 * Extracts all possible target locations from target specification.
175 *
176 * @param ruleContext BUILD target object
177 * @return map of all possible target locations
178 */
179 private static Map<Label, Collection<Artifact>> buildLocationMap(RuleContext ruleContext,
180 boolean allowDataAttributeEntriesInLabel) {
181 Map<Label, Collection<Artifact>> locationMap = new HashMap<>();
182
183 // Add all destination locations.
184 for (OutputFile out : ruleContext.getRule().getOutputFiles()) {
185 mapGet(locationMap, out.getLabel()).add(ruleContext.createOutputArtifact(out));
186 }
187
188 if (ruleContext.getRule().isAttrDefined("srcs", Type.LABEL_LIST)) {
189 for (FileProvider src : ruleContext
190 .getPrerequisites("srcs", Mode.TARGET, FileProvider.class)) {
191 Iterables.addAll(mapGet(locationMap, src.getLabel()), src.getFilesToBuild());
192 }
193 }
194
195 // Add all locations associated with dependencies and tools
196 List<FilesToRunProvider> depsDataAndTools = new ArrayList<>();
197 if (ruleContext.getRule().isAttrDefined("deps", Type.LABEL_LIST)) {
198 Iterables.addAll(depsDataAndTools,
199 ruleContext.getPrerequisites("deps", Mode.DONT_CHECK, FilesToRunProvider.class));
200 }
201 if (allowDataAttributeEntriesInLabel
202 && ruleContext.getRule().isAttrDefined("data", Type.LABEL_LIST)) {
203 Iterables.addAll(depsDataAndTools,
204 ruleContext.getPrerequisites("data", Mode.DATA, FilesToRunProvider.class));
205 }
206 if (ruleContext.getRule().isAttrDefined("tools", Type.LABEL_LIST)) {
207 Iterables.addAll(depsDataAndTools,
208 ruleContext.getPrerequisites("tools", Mode.HOST, FilesToRunProvider.class));
209 }
210
211 for (FilesToRunProvider dep : depsDataAndTools) {
212 Label label = dep.getLabel();
213 Artifact executableArtifact = dep.getExecutable();
214
215 // If the label has an executable artifact add that to the multimaps.
216 if (executableArtifact != null) {
217 mapGet(locationMap, label).add(executableArtifact);
218 } else {
219 mapGet(locationMap, label).addAll(dep.getFilesToRun());
220 }
221 }
222 return locationMap;
223 }
224
225 /**
226 * Extracts list of all executables associated with given collection of label
227 * artifacts.
228 *
229 * @param artifacts to get the paths of
230 * @return all associated executable paths
231 */
232 private static List<String> getPaths(Collection<Artifact> artifacts) {
233 List<String> paths = Lists.newArrayListWithCapacity(artifacts.size());
234 for (Artifact artifact : artifacts) {
235 PathFragment execPath = artifact.getExecPath();
236 if (execPath != null) { // omit middlemen etc
237 paths.add(execPath.getPathString());
238 }
239 }
240 return paths;
241 }
242
243 /**
244 * Returns the value in the specified map corresponding to 'key', creating and
245 * inserting an empty container if absent. We use Map not Multimap because
246 * we need to distinguish the cases of "empty value" and "absent key".
247 *
248 * @return the value in the specified map corresponding to 'key'
249 */
250 private static <K, V> Collection<V> mapGet(Map<K, Collection<V>> map, K key) {
251 Collection<V> values = map.get(key);
252 if (values == null) {
253 // We use sets not lists, because it's conceivable that the same label
254 // could appear twice, in "srcs" and "deps".
255 values = Sets.newHashSet();
256 map.put(key, values);
257 }
258 return values;
259 }
260}