blob: 37cd3986bbe26a73700545f4e4cfab2f141e0897 [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.rules;
16
17import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.DATA;
18import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
19import static com.google.devtools.build.lib.packages.Attribute.attr;
20import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
21import static com.google.devtools.build.lib.packages.Type.INTEGER;
22import static com.google.devtools.build.lib.packages.Type.LABEL;
23import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
24import static com.google.devtools.build.lib.packages.Type.STRING;
25
26import com.google.common.annotations.VisibleForTesting;
27import com.google.common.cache.CacheBuilder;
28import com.google.common.cache.CacheLoader;
29import com.google.common.cache.LoadingCache;
30import com.google.common.collect.ImmutableList;
31import com.google.devtools.build.lib.analysis.BaseRuleClasses;
32import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
33import com.google.devtools.build.lib.analysis.config.RunUnder;
34import com.google.devtools.build.lib.events.Location;
35import com.google.devtools.build.lib.packages.Attribute;
36import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
37import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel;
38import com.google.devtools.build.lib.packages.AttributeMap;
39import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunctionWithCallback;
40import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunctionWithMap;
41import com.google.devtools.build.lib.packages.Package.NameConflictException;
42import com.google.devtools.build.lib.packages.PackageFactory;
43import com.google.devtools.build.lib.packages.PackageFactory.PackageContext;
44import com.google.devtools.build.lib.packages.Rule;
45import com.google.devtools.build.lib.packages.RuleClass;
46import com.google.devtools.build.lib.packages.RuleClass.Builder;
47import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
48import com.google.devtools.build.lib.packages.RuleFactory;
49import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException;
50import com.google.devtools.build.lib.packages.SkylarkFileType;
51import com.google.devtools.build.lib.packages.TargetUtils;
52import com.google.devtools.build.lib.packages.TestSize;
53import com.google.devtools.build.lib.packages.Type;
54import com.google.devtools.build.lib.packages.Type.ConversionException;
55import com.google.devtools.build.lib.syntax.AbstractFunction;
56import com.google.devtools.build.lib.syntax.ClassObject;
57import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
58import com.google.devtools.build.lib.syntax.Environment;
59import com.google.devtools.build.lib.syntax.Environment.NoSuchVariableException;
60import com.google.devtools.build.lib.syntax.EvalException;
61import com.google.devtools.build.lib.syntax.EvalUtils;
62import com.google.devtools.build.lib.syntax.FuncallExpression;
63import com.google.devtools.build.lib.syntax.Function;
64import com.google.devtools.build.lib.syntax.Label;
65import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
66import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
67import com.google.devtools.build.lib.syntax.SkylarkCallbackFunction;
68import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
69import com.google.devtools.build.lib.syntax.SkylarkFunction;
70import com.google.devtools.build.lib.syntax.SkylarkFunction.SimpleSkylarkFunction;
71import com.google.devtools.build.lib.syntax.SkylarkList;
72import com.google.devtools.build.lib.syntax.UserDefinedFunction;
73
74import java.util.List;
75import java.util.Map;
76import java.util.concurrent.ExecutionException;
77
78/**
79 * A helper class to provide an easier API for Skylark rule definitions.
80 * This is experimental code.
81 */
82public class SkylarkRuleClassFunctions {
83
84 //TODO(bazel-team): proper enum support
85 @SkylarkBuiltin(name = "DATA_CFG", returnType = ConfigurationTransition.class,
86 doc = "The default runfiles collection state.")
87 private static final Object dataTransition = ConfigurationTransition.DATA;
88
89 @SkylarkBuiltin(name = "HOST_CFG", returnType = ConfigurationTransition.class,
90 doc = "The default runfiles collection state.")
91 private static final Object hostTransition = ConfigurationTransition.HOST;
92
93 private static final Attribute.ComputedDefault DEPRECATION =
94 new Attribute.ComputedDefault() {
95 @Override
96 public Object getDefault(AttributeMap rule) {
97 return rule.getPackageDefaultDeprecation();
98 }
99 };
100
101 private static final Attribute.ComputedDefault TEST_ONLY =
102 new Attribute.ComputedDefault() {
103 @Override
104 public Object getDefault(AttributeMap rule) {
105 return rule.getPackageDefaultTestOnly();
106 }
107 };
108
109 private static final LateBoundLabel<BuildConfiguration> RUN_UNDER =
110 new LateBoundLabel<BuildConfiguration>() {
111 @Override
112 public Label getDefault(Rule rule, BuildConfiguration configuration) {
113 RunUnder runUnder = configuration.getRunUnder();
114 return runUnder == null ? null : runUnder.getLabel();
115 }
116 };
117
118 // TODO(bazel-team): Copied from ConfiguredRuleClassProvider for the transition from built-in
119 // rules to skylark extensions. Using the same instance would require a large refactoring.
120 // If we don't want to support old built-in rules and Skylark simultaneously
121 // (except for transition phase) it's probably OK.
122 private static LoadingCache<String, Label> labelCache =
123 CacheBuilder.newBuilder().build(new CacheLoader<String, Label>() {
124 @Override
125 public Label load(String from) throws Exception {
126 try {
127 return Label.parseAbsolute(from);
128 } catch (Label.SyntaxException e) {
129 throw new Exception(from);
130 }
131 }
132 });
133
134 // TODO(bazel-team): Remove the code duplication (BaseRuleClasses and this class).
135 private static final RuleClass baseRule =
136 BaseRuleClasses.commonCoreAndSkylarkAttributes(
137 new RuleClass.Builder("$base_rule", RuleClassType.ABSTRACT, true))
138 .add(attr("expect_failure", STRING))
139 .build();
140
141 private static final RuleClass testBaseRule =
142 new RuleClass.Builder("$test_base_rule", RuleClassType.ABSTRACT, true, baseRule)
143 .add(attr("size", STRING).value("medium").taggable()
144 .nonconfigurable("used in loading phase rule validation logic"))
145 .add(attr("timeout", STRING).taggable()
146 .nonconfigurable("used in loading phase rule validation logic").value(
147 new Attribute.ComputedDefault() {
148 @Override
149 public Object getDefault(AttributeMap rule) {
150 TestSize size = TestSize.getTestSize(rule.get("size", Type.STRING));
151 if (size != null) {
152 String timeout = size.getDefaultTimeout().toString();
153 if (timeout != null) {
154 return timeout;
155 }
156 }
157 return "illegal";
158 }
159 }))
160 .add(attr("flaky", BOOLEAN).value(false).taggable()
161 .nonconfigurable("taggable - called in Rule.getRuleTags"))
162 .add(attr("shard_count", INTEGER).value(-1))
163 .add(attr("local", BOOLEAN).value(false).taggable()
164 .nonconfigurable("policy decision: this should be consistent across configurations"))
165 .add(attr("$test_runtime", LABEL_LIST).cfg(HOST).value(ImmutableList.of(
166 labelCache.getUnchecked("//tools/test:runtime"))))
167 .add(attr(":run_under", LABEL).cfg(DATA).value(RUN_UNDER))
168 .build();
169
170 /**
171 * In native code, private values start with $.
172 * In Skylark, private values start with _, because of the grammar.
173 */
174 private static String attributeToNative(String oldName, Location loc, boolean isLateBound)
175 throws EvalException {
176 if (oldName.isEmpty()) {
177 throw new EvalException(loc, "Attribute name cannot be empty");
178 }
179 if (isLateBound) {
180 if (oldName.charAt(0) != '_') {
181 throw new EvalException(loc, "When an attribute value is a function, "
182 + "the attribute must be private (start with '_')");
183 }
184 return ":" + oldName.substring(1);
185 }
186 if (oldName.charAt(0) == '_') {
187 return "$" + oldName.substring(1);
188 }
189 return oldName;
190 }
191
192 // TODO(bazel-team): implement attribute copy and other rule properties
193
194 @SkylarkBuiltin(name = "rule", doc =
195 "Creates a new rule. Store it in a global value, so that it can be loaded and called "
196 + "from BUILD files.",
197 onlyLoadingPhase = true,
198 returnType = Function.class,
199 mandatoryParams = {
200 @Param(name = "implementation", type = UserDefinedFunction.class,
201 doc = "the function implementing this rule, has to have exactly one parameter: "
202 + "<code>ctx</code>. The function is called during analysis phase for each "
203 + "instance of the rule. It can access the attributes provided by the user. "
204 + "It must create actions to generate all the declared outputs.")
205 },
206 optionalParams = {
207 @Param(name = "test", type = Boolean.class, doc = "Whether this rule is a test rule. "
208 + "If True, the rule must end with <code>_test</code> (otherwise it cannot)."),
209 @Param(name = "attrs", doc =
210 "dictionary to declare all the attributes of the rule. It maps from an attribute name "
211 + "to an attribute object (see 'attr' module). Attributes starting with <code>_</code> "
212 + "are private, and can be used to add an implicit dependency on a label."),
213 @Param(name = "outputs", doc = "outputs of this rule. "
Laszlo Csomoraded1c22015-02-16 16:57:12 +0000214 + "It is a dictionary mapping from string to a template name. "
215 + "For example: <code>{\"ext\": \"${name}.ext\"}</code>. <br>"
216 + "The dictionary key becomes a field in <code>ctx.outputs</code>. "
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100217 // TODO(bazel-team): Make doc more clear, wrt late-bound attributes.
218 + "It may also be a function (which receives <code>ctx.attr</code> as argument) "
219 + "returning such a dictionary."),
220 @Param(name = "executable", type = Boolean.class,
221 doc = "whether this rule always outputs an executable of the same name or not. If True, "
222 + "there must be an action that generates <code>ctx.outputs.executable</code>.")})
223 private static final SkylarkFunction rule = new SkylarkFunction("rule") {
224
225 @Override
226 public Object call(Map<String, Object> arguments, FuncallExpression ast,
227 Environment funcallEnv) throws EvalException, ConversionException {
228 final Location loc = ast.getLocation();
229
230 RuleClassType type = RuleClassType.NORMAL;
231 if (arguments.containsKey("test") && EvalUtils.toBoolean(arguments.get("test"))) {
232 type = RuleClassType.TEST;
233 }
234
235 // We'll set the name later, pass the empty string for now.
236 final RuleClass.Builder builder = type == RuleClassType.TEST
237 ? new RuleClass.Builder("", type, true, testBaseRule)
238 : new RuleClass.Builder("", type, true, baseRule);
239
240 for (Map.Entry<String, Attribute.Builder> attr : castMap(
241 arguments.get("attrs"), String.class, Attribute.Builder.class, "attrs")) {
242 Attribute.Builder<?> attrBuilder = attr.getValue();
243 String attrName = attributeToNative(attr.getKey(), loc,
244 attrBuilder.hasLateBoundValue());
245 builder.addOrOverrideAttribute(attrBuilder.build(attrName));
246 }
247 if (arguments.containsKey("executable") && (Boolean) arguments.get("executable")) {
248 builder.addOrOverrideAttribute(
249 attr("$is_executable", BOOLEAN).value(true)
250 .nonconfigurable("Called from RunCommand.isExecutable, which takes a Target")
251 .build());
252 builder.setOutputsDefaultExecutable();
253 }
254
255 if (arguments.containsKey("outputs")) {
256 final Object implicitOutputs = arguments.get("outputs");
257 if (implicitOutputs instanceof UserDefinedFunction) {
258 UserDefinedFunction func = (UserDefinedFunction) implicitOutputs;
259 final SkylarkCallbackFunction callback =
260 new SkylarkCallbackFunction(func, ast, (SkylarkEnvironment) funcallEnv);
261 builder.setImplicitOutputsFunction(
262 new SkylarkImplicitOutputsFunctionWithCallback(callback, loc));
263 } else {
264 builder.setImplicitOutputsFunction(new SkylarkImplicitOutputsFunctionWithMap(
265 toMap(castMap(arguments.get("outputs"), String.class, String.class,
266 "implicit outputs of the rule class"))));
267 }
268 }
269
270 builder.setConfiguredTargetFunction(
271 (UserDefinedFunction) arguments.get("implementation"));
272 builder.setRuleDefinitionEnvironment((SkylarkEnvironment) funcallEnv);
273 return new RuleFunction(builder, type);
274 }
275 };
276
277 // This class is needed for testing
278 static final class RuleFunction extends AbstractFunction {
279 // Note that this means that we can reuse the same builder.
280 // This is fine since we don't modify the builder from here.
281 private final RuleClass.Builder builder;
282 private final RuleClassType type;
283
284 public RuleFunction(Builder builder, RuleClassType type) {
285 super("rule");
286 this.builder = builder;
287 this.type = type;
288 }
289
290 @Override
291 public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
292 Environment env) throws EvalException, InterruptedException {
293 try {
294 String ruleClassName = ast.getFunction().getName();
295 if (ruleClassName.startsWith("_")) {
296 throw new EvalException(ast.getLocation(), "Invalid rule class name '" + ruleClassName
297 + "', cannot be private");
298 }
299 if (type == RuleClassType.TEST != TargetUtils.isTestRuleName(ruleClassName)) {
300 throw new EvalException(ast.getLocation(), "Invalid rule class name '" + ruleClassName
301 + "', test rule class names must end with '_test' and other rule classes must not");
302 }
303 RuleClass ruleClass = builder.build(ruleClassName);
304 PackageContext pkgContext = (PackageContext) env.lookup(PackageFactory.PKG_CONTEXT);
305 return RuleFactory.createAndAddRule(pkgContext, ruleClass, kwargs, ast);
306 } catch (InvalidRuleException | NameConflictException | NoSuchVariableException e) {
307 throw new EvalException(ast.getLocation(), e.getMessage());
308 }
309 }
310
311 @VisibleForTesting
312 RuleClass.Builder getBuilder() {
313 return builder;
314 }
315 }
316
317 @SkylarkBuiltin(name = "Label", doc = "Creates a Label referring to a BUILD target. Use "
318 + "this function only when you want to give a default value for the label attributes. "
319 + "Example: <br><pre class=language-python>Label(\"//tools:default\")</pre>",
320 returnType = Label.class,
321 mandatoryParams = {@Param(name = "label_string", type = String.class,
322 doc = "the label string")})
323 private static final SkylarkFunction label = new SimpleSkylarkFunction("Label") {
324 @Override
325 public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
326 ConversionException {
327 String labelString = (String) arguments.get("label_string");
328 try {
329 return labelCache.get(labelString);
330 } catch (ExecutionException e) {
331 throw new EvalException(loc, "Illegal absolute label syntax: " + labelString);
332 }
333 }
334 };
335
336 @SkylarkBuiltin(name = "FileType",
337 doc = "Creates a file filter from a list of strings. For example, to match files ending "
338 + "with .cc or .cpp, use: <pre class=language-python>FileType([\".cc\", \".cpp\"])</pre>",
339 returnType = SkylarkFileType.class,
340 mandatoryParams = {
341 @Param(name = "types", type = SkylarkList.class, generic1 = String.class,
342 doc = "a list of the accepted file extensions")})
343 private static final SkylarkFunction fileType = new SimpleSkylarkFunction("FileType") {
344 @Override
345 public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
346 ConversionException {
347 return SkylarkFileType.of(castList(arguments.get("types"), String.class));
348 }
349 };
350
351 @SkylarkBuiltin(name = "to_proto",
352 doc = "Creates a text message from the struct parameter. This method only works if all "
353 + "struct elements (recursively) are strings, ints, booleans, other structs or a "
354 + "list of these types. Quotes and new lines in strings are escaped. "
355 + "Examples:<br><pre class=language-python>"
356 + "struct(key=123).to_proto()\n# key: 123\n\n"
357 + "struct(key=True).to_proto()\n# key: true\n\n"
358 + "struct(key=[1, 2, 3]).to_proto()\n# key: 1\n# key: 2\n# key: 3\n\n"
359 + "struct(key='text').to_proto()\n# key: \"text\"\n\n"
360 + "struct(key=struct(inner_key='text')).to_proto()\n"
361 + "# key {\n# inner_key: \"text\"\n# }\n\n"
362 + "struct(key=[struct(inner_key=1), struct(inner_key=2)]).to_proto()\n"
363 + "# key {\n# inner_key: 1\n# }\n# key {\n# inner_key: 2\n# }\n\n"
364 + "struct(key=struct(inner_key=struct(inner_inner_key='text'))).to_proto()\n"
365 + "# key {\n# inner_key {\n# inner_inner_key: \"text\"\n# }\n# }\n</pre>",
366 objectType = SkylarkClassObject.class, returnType = String.class)
367 private static final SkylarkFunction toProto = new SimpleSkylarkFunction("to_proto") {
368 @Override
369 public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
370 ConversionException {
371 ClassObject object = (ClassObject) arguments.get("self");
372 StringBuilder sb = new StringBuilder();
373 printTextMessage(object, sb, 0, loc);
374 return sb.toString();
375 }
376
377 private void printTextMessage(ClassObject object, StringBuilder sb,
378 int indent, Location loc) throws EvalException {
379 for (String key : object.getKeys()) {
380 printTextMessage(key, object.getValue(key), sb, indent, loc);
381 }
382 }
383
384 private void printSimpleTextMessage(String key, Object value, StringBuilder sb,
385 int indent, Location loc, String container) throws EvalException {
386 if (value instanceof ClassObject) {
387 print(sb, key + " {", indent);
388 printTextMessage((ClassObject) value, sb, indent + 1, loc);
389 print(sb, "}", indent);
390 } else if (value instanceof String) {
391 print(sb, key + ": \"" + escape((String) value) + "\"", indent);
392 } else if (value instanceof Integer) {
393 print(sb, key + ": " + value, indent);
394 } else if (value instanceof Boolean) {
395 // We're relying on the fact that Java converts Booleans to Strings in the same way
396 // as the protocol buffers do.
397 print(sb, key + ": " + value, indent);
398 } else {
399 throw new EvalException(loc,
400 "Invalid text format, expected a struct, a string, a bool, or an int but got a "
Francois-Rene Rideaucbebd632015-02-11 16:56:37 +0000401 + EvalUtils.getDataTypeName(value) + " for " + container + " '" + key + "'");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100402 }
403 }
404
405 private void printTextMessage(String key, Object value, StringBuilder sb,
406 int indent, Location loc) throws EvalException {
407 if (value instanceof SkylarkList) {
408 for (Object item : ((SkylarkList) value)) {
409 // TODO(bazel-team): There should be some constraint on the fields of the structs
410 // in the same list but we ignore that for now.
411 printSimpleTextMessage(key, item, sb, indent, loc, "list element in struct field");
412 }
413 } else {
414 printSimpleTextMessage(key, value, sb, indent, loc, "struct field");
415 }
416 }
417
418 private String escape(String string) {
419 // TODO(bazel-team): use guava's SourceCodeEscapers when it's released.
420 return string.replace("\"", "\\\"").replace("\n", "\\n");
421 }
422
423 private void print(StringBuilder sb, String text, int indent) {
424 for (int i = 0; i < indent; i++) {
425 sb.append(" ");
426 }
427 sb.append(text);
428 sb.append("\n");
429 }
430 };
431}