blob: 416d92e1f3f8da878c3987ba3c9c98f49776e252 [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.annotations.VisibleForTesting;
18import com.google.common.collect.ImmutableCollection;
19import com.google.common.collect.ImmutableList;
20import com.google.common.collect.ImmutableMap;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010021import com.google.common.collect.Iterables;
22import com.google.common.collect.Sets;
23import com.google.devtools.build.lib.actions.Artifact;
24import com.google.devtools.build.lib.actions.BaseSpawn;
25import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
26import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
27import com.google.devtools.build.lib.packages.Type;
28import com.google.devtools.build.lib.syntax.Label;
29import com.google.devtools.build.lib.syntax.SkylarkCallable;
30import com.google.devtools.build.lib.syntax.SkylarkModule;
31import com.google.devtools.build.lib.util.Pair;
32import com.google.devtools.build.lib.vfs.PathFragment;
33
34import java.util.Collection;
35import java.util.HashMap;
36import java.util.List;
37import java.util.Map;
38import java.util.Map.Entry;
39
40/**
41 * Provides shared functionality for parameterized command-line launching
42 * e.g. {@link com.google.devtools.build.lib.view.genrule.GenRule}
43 * Also used by {@link com.google.devtools.build.lib.rules.extra.ExtraActionFactory}.
44 *
45 * Two largely independent separate sets of functionality are provided:
46 * 1- string interpolation for {@code $(location[s] ...)} and {@code $(MakeVariable)}
47 * 2- a utility to build potentially large command lines (presumably made of multiple commands),
48 * that if presumed too large for the kernel's taste can be dumped into a shell script
49 * that will contain the same commands,
50 * at which point the shell script is added to the list of inputs.
51 */
52@SkylarkModule(name = "command_helper", doc = "A helper class to create shell commands.")
53public final class CommandHelper {
54
55 /**
56 * Maximum total command-line length, in bytes, not counting "/bin/bash -c ".
57 * If the command is very long, then we write the command to a script file,
58 * to avoid overflowing any limits on command-line length.
59 * For short commands, we just use /bin/bash -c command.
60 */
61 @VisibleForTesting
62 public static int maxCommandLength = 64000;
63
64 /**
65 * A map of remote path prefixes and corresponding runfiles manifests for tools
66 * used by this rule.
67 */
68 private final ImmutableMap<PathFragment, Artifact> remoteRunfileManifestMap;
69
70 /**
71 * Use labelMap for heuristically expanding labels (does not include "outs")
72 * This is similar to heuristic location expansion in LocationExpander
73 * and should be kept in sync.
74 */
75 private final ImmutableMap<Label, ImmutableCollection<Artifact>> labelMap;
76
77 /**
78 * The ruleContext this helper works on
79 */
80 private final RuleContext ruleContext;
81
82 /**
83 * Output executable files from the 'tools' attribute.
84 */
85 private final ImmutableList<Artifact> resolvedTools;
86
87 /**
Googlerf49bb0c2015-02-11 16:18:28 +000088 * Creates a {@link CommandHelper}.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010089 *
Googlerf49bb0c2015-02-11 16:18:28 +000090 * @param tools resolves set of tools into set of executable binaries. Populates manifests,
91 * remoteRunfiles and label map where required.
92 * @param labelMap adds files to set of known files of label. Used for resolving $(location)
93 * variables.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010094 */
95 public CommandHelper(RuleContext ruleContext,
96 Iterable<FilesToRunProvider> tools,
97 ImmutableMap<Label, Iterable<Artifact>> labelMap) {
98 this.ruleContext = ruleContext;
99
100 ImmutableList.Builder<Artifact> resolvedToolsBuilder = ImmutableList.builder();
101 ImmutableMap.Builder<PathFragment, Artifact> remoteRunfileManifestBuilder =
102 ImmutableMap.builder();
103 Map<Label, Collection<Artifact>> tempLabelMap = new HashMap<>();
104
105 for (Map.Entry<Label, Iterable<Artifact>> entry : labelMap.entrySet()) {
106 Iterables.addAll(mapGet(tempLabelMap, entry.getKey()), entry.getValue());
107 }
108
109 for (FilesToRunProvider tool : tools) { // (Note: host configuration)
110 Label label = tool.getLabel();
111 Collection<Artifact> files = tool.getFilesToRun();
112 resolvedToolsBuilder.addAll(files);
113 Artifact executableArtifact = tool.getExecutable();
114 // If the label has an executable artifact add that to the multimaps.
115 if (executableArtifact != null) {
116 mapGet(tempLabelMap, label).add(executableArtifact);
117 // Also send the runfiles when running remotely.
118 Artifact runfilesManifest = tool.getRunfilesManifest();
119 if (runfilesManifest != null) {
120 remoteRunfileManifestBuilder.put(
121 BaseSpawn.runfilesForFragment(executableArtifact.getExecPath()), runfilesManifest);
122 }
123 } else {
124 // Map all depArtifacts to the respective label using the multimaps.
125 Iterables.addAll(mapGet(tempLabelMap, label), files);
126 }
127 }
128
129 this.resolvedTools = resolvedToolsBuilder.build();
130 this.remoteRunfileManifestMap = remoteRunfileManifestBuilder.build();
131 ImmutableMap.Builder<Label, ImmutableCollection<Artifact>> labelMapBuilder =
132 ImmutableMap.builder();
133 for (Entry<Label, Collection<Artifact>> entry : tempLabelMap.entrySet()) {
134 labelMapBuilder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
135 }
136 this.labelMap = labelMapBuilder.build();
137 }
138
139 @SkylarkCallable(name = "resolved_tools", doc = "", structField = true)
140 public List<Artifact> getResolvedTools() {
141 return resolvedTools;
142 }
143
144 @SkylarkCallable(name = "runfiles_manifests", doc = "", structField = true)
145 public ImmutableMap<PathFragment, Artifact> getRemoteRunfileManifestMap() {
146 return remoteRunfileManifestMap;
147 }
148
149 // Returns the value in the specified corresponding to 'key', creating and
150 // inserting an empty container if absent. We use Map not Multimap because
151 // we need to distinguish the cases of "empty value" and "absent key".
152 private static Collection<Artifact> mapGet(Map<Label, Collection<Artifact>> map, Label key) {
153 Collection<Artifact> values = map.get(key);
154 if (values == null) {
155 // We use sets not lists, because it's conceivable that the same artifact
156 // could appear twice, e.g. in "srcs" and "deps".
157 values = Sets.newHashSet();
158 map.put(key, values);
159 }
160 return values;
161 }
162
163 /**
164 * Resolves the 'cmd' attribute, and expands known locations for $(location)
165 * variables.
166 */
167 @SkylarkCallable(doc = "")
168 public String resolveCommandAndExpandLabels(Boolean supportLegacyExpansion,
169 Boolean allowDataInLabel) {
170 String command = ruleContext.attributes().get("cmd", Type.STRING);
171 command = new LocationExpander(ruleContext, allowDataInLabel).expand("cmd", command);
172
173 if (supportLegacyExpansion) {
174 command = expandLabels(command, labelMap);
175 }
176 return command;
177 }
178
179 /**
180 * Expands labels occurring in the string "expr" in the rule 'cmd'.
181 * Each label must be valid, be a declared prerequisite, and expand to a
182 * unique path.
183 *
184 * <p>If the expansion fails, an attribute error is reported and the original
185 * expression is returned.
186 */
187 private <T extends Iterable<Artifact>> String expandLabels(String expr, Map<Label, T> labelMap) {
188 try {
189 return LabelExpander.expand(expr, labelMap, ruleContext.getLabel());
190 } catch (LabelExpander.NotUniqueExpansionException nuee) {
191 ruleContext.attributeError("cmd", nuee.getMessage());
192 return expr;
193 }
194 }
195
196 private static Pair<List<String>, Artifact> buildCommandLineMaybeWithScriptFile(
Googlerf49bb0c2015-02-11 16:18:28 +0000197 RuleContext ruleContext, String command, String scriptPostFix, PathFragment shellPath) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100198 List<String> argv;
199 Artifact scriptFileArtifact = null;
200 if (command.length() <= maxCommandLength) {
Googlerf49bb0c2015-02-11 16:18:28 +0000201 argv = buildCommandLineSimpleArgv(command, shellPath);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100202 } else {
203 // Use script file.
204 scriptFileArtifact = buildCommandLineArtifact(ruleContext, command, scriptPostFix);
Googlerf49bb0c2015-02-11 16:18:28 +0000205 argv = buildCommandLineArgvWithArtifact(scriptFileArtifact, shellPath);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100206 }
207 return Pair.of(argv, scriptFileArtifact);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100208 }
209
Googlerf49bb0c2015-02-11 16:18:28 +0000210 private static ImmutableList<String> buildCommandLineArgvWithArtifact(Artifact scriptFileArtifact,
211 PathFragment shellPath) {
212 return ImmutableList.of(shellPath.getPathString(), scriptFileArtifact.getExecPathString());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100213 }
214
215 private static Artifact buildCommandLineArtifact(RuleContext ruleContext, String command,
216 String scriptPostFix) {
217 String scriptFileName = ruleContext.getTarget().getName() + scriptPostFix;
218 String scriptFileContents = "#!/bin/bash\n" + command;
219 Artifact scriptFileArtifact = FileWriteAction.createFile(
220 ruleContext, scriptFileName, scriptFileContents, /*executable=*/true);
221 return scriptFileArtifact;
222 }
223
Googlerf49bb0c2015-02-11 16:18:28 +0000224 private static ImmutableList<String> buildCommandLineSimpleArgv(String command,
225 PathFragment shellPath) {
226 return ImmutableList.of(shellPath.getPathString(), "-c", command);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100227 }
228
229 /**
230 * Builds the set of command-line arguments. Creates a bash script if the
Googlerf49bb0c2015-02-11 16:18:28 +0000231 * command line is longer than the allowed maximum {@link #maxCommandLength}.
232 * Fixes up the input artifact list with the created bash script when required.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100233 */
234 public List<String> buildCommandLine(
235 String command, NestedSetBuilder<Artifact> inputs, String scriptPostFix) {
Googlerf49bb0c2015-02-11 16:18:28 +0000236 return buildCommandLine(command, inputs, scriptPostFix,
237 ruleContext.getConfiguration().getShExecutable());
238 }
239
240 /**
241 * Builds the set of command-line arguments using the specified shell path. Creates a bash script
242 * if the command line is longer than the allowed maximum {@link #maxCommandLength}.
243 * Fixes up the input artifact list with the created bash script when required.
244 *
245 * @param shellPath path to the shell that should invoke this command
246 */
247 public List<String> buildCommandLine(
248 String command, NestedSetBuilder<Artifact> inputs, String scriptPostFix,
249 PathFragment shellPath) {
250 Pair<List<String>, Artifact> argvAndScriptFile =
251 buildCommandLineMaybeWithScriptFile(ruleContext, command, scriptPostFix, shellPath);
252 if (argvAndScriptFile.second != null) {
253 inputs.add(argvAndScriptFile.second);
254 }
255 return argvAndScriptFile.first;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100256 }
257
258 /**
259 * Builds the set of command-line arguments. Creates a bash script if the
Googlerf49bb0c2015-02-11 16:18:28 +0000260 * command line is longer than the allowed maximum {@link #maxCommandLength}.
261 * Fixes up the input artifact list with the created bash script when required.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100262 */
263 @SkylarkCallable(doc = "")
264 public List<String> buildCommandLine(
265 String command, List<Artifact> inputs, String scriptPostFix) {
Googlerf49bb0c2015-02-11 16:18:28 +0000266 Pair<List<String>, Artifact> argvAndScriptFile = buildCommandLineMaybeWithScriptFile(
267 ruleContext, command, scriptPostFix, ruleContext.getConfiguration().getShExecutable());
268 if (argvAndScriptFile.second != null) {
269 inputs.add(argvAndScriptFile.second);
270 }
271 return argvAndScriptFile.first;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100272 }
273}