Update from Google.
--
MOE_MIGRATED_REVID=85702957
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleClassFunctions.java
new file mode 100644
index 0000000..39b8836
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleClassFunctions.java
@@ -0,0 +1,430 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.DATA;
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.RunUnder;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunctionWithCallback;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunctionWithMap;
+import com.google.devtools.build.lib.packages.Package.NameConflictException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageFactory.PackageContext;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.RuleFactory;
+import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException;
+import com.google.devtools.build.lib.packages.SkylarkFileType;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.TestSize;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.AbstractFunction;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Environment.NoSuchVariableException;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
+import com.google.devtools.build.lib.syntax.SkylarkCallbackFunction;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.SkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkFunction.SimpleSkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.UserDefinedFunction;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A helper class to provide an easier API for Skylark rule definitions.
+ * This is experimental code.
+ */
+public class SkylarkRuleClassFunctions {
+
+ //TODO(bazel-team): proper enum support
+ @SkylarkBuiltin(name = "DATA_CFG", returnType = ConfigurationTransition.class,
+ doc = "The default runfiles collection state.")
+ private static final Object dataTransition = ConfigurationTransition.DATA;
+
+ @SkylarkBuiltin(name = "HOST_CFG", returnType = ConfigurationTransition.class,
+ doc = "The default runfiles collection state.")
+ private static final Object hostTransition = ConfigurationTransition.HOST;
+
+ private static final Attribute.ComputedDefault DEPRECATION =
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.getPackageDefaultDeprecation();
+ }
+ };
+
+ private static final Attribute.ComputedDefault TEST_ONLY =
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.getPackageDefaultTestOnly();
+ }
+ };
+
+ private static final LateBoundLabel<BuildConfiguration> RUN_UNDER =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ RunUnder runUnder = configuration.getRunUnder();
+ return runUnder == null ? null : runUnder.getLabel();
+ }
+ };
+
+ // TODO(bazel-team): Copied from ConfiguredRuleClassProvider for the transition from built-in
+ // rules to skylark extensions. Using the same instance would require a large refactoring.
+ // If we don't want to support old built-in rules and Skylark simultaneously
+ // (except for transition phase) it's probably OK.
+ private static LoadingCache<String, Label> labelCache =
+ CacheBuilder.newBuilder().build(new CacheLoader<String, Label>() {
+ @Override
+ public Label load(String from) throws Exception {
+ try {
+ return Label.parseAbsolute(from);
+ } catch (Label.SyntaxException e) {
+ throw new Exception(from);
+ }
+ }
+ });
+
+ // TODO(bazel-team): Remove the code duplication (BaseRuleClasses and this class).
+ private static final RuleClass baseRule =
+ BaseRuleClasses.commonCoreAndSkylarkAttributes(
+ new RuleClass.Builder("$base_rule", RuleClassType.ABSTRACT, true))
+ .add(attr("expect_failure", STRING))
+ .build();
+
+ private static final RuleClass testBaseRule =
+ new RuleClass.Builder("$test_base_rule", RuleClassType.ABSTRACT, true, baseRule)
+ .add(attr("size", STRING).value("medium").taggable()
+ .nonconfigurable("used in loading phase rule validation logic"))
+ .add(attr("timeout", STRING).taggable()
+ .nonconfigurable("used in loading phase rule validation logic").value(
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ TestSize size = TestSize.getTestSize(rule.get("size", Type.STRING));
+ if (size != null) {
+ String timeout = size.getDefaultTimeout().toString();
+ if (timeout != null) {
+ return timeout;
+ }
+ }
+ return "illegal";
+ }
+ }))
+ .add(attr("flaky", BOOLEAN).value(false).taggable()
+ .nonconfigurable("taggable - called in Rule.getRuleTags"))
+ .add(attr("shard_count", INTEGER).value(-1))
+ .add(attr("local", BOOLEAN).value(false).taggable()
+ .nonconfigurable("policy decision: this should be consistent across configurations"))
+ .add(attr("$test_runtime", LABEL_LIST).cfg(HOST).value(ImmutableList.of(
+ labelCache.getUnchecked("//tools/test:runtime"))))
+ .add(attr(":run_under", LABEL).cfg(DATA).value(RUN_UNDER))
+ .build();
+
+ /**
+ * In native code, private values start with $.
+ * In Skylark, private values start with _, because of the grammar.
+ */
+ private static String attributeToNative(String oldName, Location loc, boolean isLateBound)
+ throws EvalException {
+ if (oldName.isEmpty()) {
+ throw new EvalException(loc, "Attribute name cannot be empty");
+ }
+ if (isLateBound) {
+ if (oldName.charAt(0) != '_') {
+ throw new EvalException(loc, "When an attribute value is a function, "
+ + "the attribute must be private (start with '_')");
+ }
+ return ":" + oldName.substring(1);
+ }
+ if (oldName.charAt(0) == '_') {
+ return "$" + oldName.substring(1);
+ }
+ return oldName;
+ }
+
+ // TODO(bazel-team): implement attribute copy and other rule properties
+
+ @SkylarkBuiltin(name = "rule", doc =
+ "Creates a new rule. Store it in a global value, so that it can be loaded and called "
+ + "from BUILD files.",
+ onlyLoadingPhase = true,
+ returnType = Function.class,
+ mandatoryParams = {
+ @Param(name = "implementation", type = UserDefinedFunction.class,
+ doc = "the function implementing this rule, has to have exactly one parameter: "
+ + "<code>ctx</code>. The function is called during analysis phase for each "
+ + "instance of the rule. It can access the attributes provided by the user. "
+ + "It must create actions to generate all the declared outputs.")
+ },
+ optionalParams = {
+ @Param(name = "test", type = Boolean.class, doc = "Whether this rule is a test rule. "
+ + "If True, the rule must end with <code>_test</code> (otherwise it cannot)."),
+ @Param(name = "attrs", doc =
+ "dictionary to declare all the attributes of the rule. It maps from an attribute name "
+ + "to an attribute object (see 'attr' module). Attributes starting with <code>_</code> "
+ + "are private, and can be used to add an implicit dependency on a label."),
+ @Param(name = "outputs", doc = "outputs of this rule. "
+ + "It is a dictionary mapping from string to a template name. For example: "
+ + "<code>{\"ext\": \"${name}.ext\"}</code>. <br>"
+ // TODO(bazel-team): Make doc more clear, wrt late-bound attributes.
+ + "It may also be a function (which receives <code>ctx.attr</code> as argument) "
+ + "returning such a dictionary."),
+ @Param(name = "executable", type = Boolean.class,
+ doc = "whether this rule always outputs an executable of the same name or not. If True, "
+ + "there must be an action that generates <code>ctx.outputs.executable</code>.")})
+ private static final SkylarkFunction rule = new SkylarkFunction("rule") {
+
+ @Override
+ public Object call(Map<String, Object> arguments, FuncallExpression ast,
+ Environment funcallEnv) throws EvalException, ConversionException {
+ final Location loc = ast.getLocation();
+
+ RuleClassType type = RuleClassType.NORMAL;
+ if (arguments.containsKey("test") && EvalUtils.toBoolean(arguments.get("test"))) {
+ type = RuleClassType.TEST;
+ }
+
+ // We'll set the name later, pass the empty string for now.
+ final RuleClass.Builder builder = type == RuleClassType.TEST
+ ? new RuleClass.Builder("", type, true, testBaseRule)
+ : new RuleClass.Builder("", type, true, baseRule);
+
+ for (Map.Entry<String, Attribute.Builder> attr : castMap(
+ arguments.get("attrs"), String.class, Attribute.Builder.class, "attrs")) {
+ Attribute.Builder<?> attrBuilder = attr.getValue();
+ String attrName = attributeToNative(attr.getKey(), loc,
+ attrBuilder.hasLateBoundValue());
+ builder.addOrOverrideAttribute(attrBuilder.build(attrName));
+ }
+ if (arguments.containsKey("executable") && (Boolean) arguments.get("executable")) {
+ builder.addOrOverrideAttribute(
+ attr("$is_executable", BOOLEAN).value(true)
+ .nonconfigurable("Called from RunCommand.isExecutable, which takes a Target")
+ .build());
+ builder.setOutputsDefaultExecutable();
+ }
+
+ if (arguments.containsKey("outputs")) {
+ final Object implicitOutputs = arguments.get("outputs");
+ if (implicitOutputs instanceof UserDefinedFunction) {
+ UserDefinedFunction func = (UserDefinedFunction) implicitOutputs;
+ final SkylarkCallbackFunction callback =
+ new SkylarkCallbackFunction(func, ast, (SkylarkEnvironment) funcallEnv);
+ builder.setImplicitOutputsFunction(
+ new SkylarkImplicitOutputsFunctionWithCallback(callback, loc));
+ } else {
+ builder.setImplicitOutputsFunction(new SkylarkImplicitOutputsFunctionWithMap(
+ toMap(castMap(arguments.get("outputs"), String.class, String.class,
+ "implicit outputs of the rule class"))));
+ }
+ }
+
+ builder.setConfiguredTargetFunction(
+ (UserDefinedFunction) arguments.get("implementation"));
+ builder.setRuleDefinitionEnvironment((SkylarkEnvironment) funcallEnv);
+ return new RuleFunction(builder, type);
+ }
+ };
+
+ // This class is needed for testing
+ static final class RuleFunction extends AbstractFunction {
+ // Note that this means that we can reuse the same builder.
+ // This is fine since we don't modify the builder from here.
+ private final RuleClass.Builder builder;
+ private final RuleClassType type;
+
+ public RuleFunction(Builder builder, RuleClassType type) {
+ super("rule");
+ this.builder = builder;
+ this.type = type;
+ }
+
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ Environment env) throws EvalException, InterruptedException {
+ try {
+ String ruleClassName = ast.getFunction().getName();
+ if (ruleClassName.startsWith("_")) {
+ throw new EvalException(ast.getLocation(), "Invalid rule class name '" + ruleClassName
+ + "', cannot be private");
+ }
+ if (type == RuleClassType.TEST != TargetUtils.isTestRuleName(ruleClassName)) {
+ throw new EvalException(ast.getLocation(), "Invalid rule class name '" + ruleClassName
+ + "', test rule class names must end with '_test' and other rule classes must not");
+ }
+ RuleClass ruleClass = builder.build(ruleClassName);
+ PackageContext pkgContext = (PackageContext) env.lookup(PackageFactory.PKG_CONTEXT);
+ return RuleFactory.createAndAddRule(pkgContext, ruleClass, kwargs, ast);
+ } catch (InvalidRuleException | NameConflictException | NoSuchVariableException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+
+ @VisibleForTesting
+ RuleClass.Builder getBuilder() {
+ return builder;
+ }
+ }
+
+ @SkylarkBuiltin(name = "Label", doc = "Creates a Label referring to a BUILD target. Use "
+ + "this function only when you want to give a default value for the label attributes. "
+ + "Example: <br><pre class=language-python>Label(\"//tools:default\")</pre>",
+ returnType = Label.class,
+ mandatoryParams = {@Param(name = "label_string", type = String.class,
+ doc = "the label string")})
+ private static final SkylarkFunction label = new SimpleSkylarkFunction("Label") {
+ @Override
+ public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
+ ConversionException {
+ String labelString = (String) arguments.get("label_string");
+ try {
+ return labelCache.get(labelString);
+ } catch (ExecutionException e) {
+ throw new EvalException(loc, "Illegal absolute label syntax: " + labelString);
+ }
+ }
+ };
+
+ @SkylarkBuiltin(name = "FileType",
+ doc = "Creates a file filter from a list of strings. For example, to match files ending "
+ + "with .cc or .cpp, use: <pre class=language-python>FileType([\".cc\", \".cpp\"])</pre>",
+ returnType = SkylarkFileType.class,
+ mandatoryParams = {
+ @Param(name = "types", type = SkylarkList.class, generic1 = String.class,
+ doc = "a list of the accepted file extensions")})
+ private static final SkylarkFunction fileType = new SimpleSkylarkFunction("FileType") {
+ @Override
+ public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
+ ConversionException {
+ return SkylarkFileType.of(castList(arguments.get("types"), String.class));
+ }
+ };
+
+ @SkylarkBuiltin(name = "to_proto",
+ doc = "Creates a text message from the struct parameter. This method only works if all "
+ + "struct elements (recursively) are strings, ints, booleans, other structs or a "
+ + "list of these types. Quotes and new lines in strings are escaped. "
+ + "Examples:<br><pre class=language-python>"
+ + "struct(key=123).to_proto()\n# key: 123\n\n"
+ + "struct(key=True).to_proto()\n# key: true\n\n"
+ + "struct(key=[1, 2, 3]).to_proto()\n# key: 1\n# key: 2\n# key: 3\n\n"
+ + "struct(key='text').to_proto()\n# key: \"text\"\n\n"
+ + "struct(key=struct(inner_key='text')).to_proto()\n"
+ + "# key {\n# inner_key: \"text\"\n# }\n\n"
+ + "struct(key=[struct(inner_key=1), struct(inner_key=2)]).to_proto()\n"
+ + "# key {\n# inner_key: 1\n# }\n# key {\n# inner_key: 2\n# }\n\n"
+ + "struct(key=struct(inner_key=struct(inner_inner_key='text'))).to_proto()\n"
+ + "# key {\n# inner_key {\n# inner_inner_key: \"text\"\n# }\n# }\n</pre>",
+ objectType = SkylarkClassObject.class, returnType = String.class)
+ private static final SkylarkFunction toProto = new SimpleSkylarkFunction("to_proto") {
+ @Override
+ public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
+ ConversionException {
+ ClassObject object = (ClassObject) arguments.get("self");
+ StringBuilder sb = new StringBuilder();
+ printTextMessage(object, sb, 0, loc);
+ return sb.toString();
+ }
+
+ private void printTextMessage(ClassObject object, StringBuilder sb,
+ int indent, Location loc) throws EvalException {
+ for (String key : object.getKeys()) {
+ printTextMessage(key, object.getValue(key), sb, indent, loc);
+ }
+ }
+
+ private void printSimpleTextMessage(String key, Object value, StringBuilder sb,
+ int indent, Location loc, String container) throws EvalException {
+ if (value instanceof ClassObject) {
+ print(sb, key + " {", indent);
+ printTextMessage((ClassObject) value, sb, indent + 1, loc);
+ print(sb, "}", indent);
+ } else if (value instanceof String) {
+ print(sb, key + ": \"" + escape((String) value) + "\"", indent);
+ } else if (value instanceof Integer) {
+ print(sb, key + ": " + value, indent);
+ } else if (value instanceof Boolean) {
+ // We're relying on the fact that Java converts Booleans to Strings in the same way
+ // as the protocol buffers do.
+ print(sb, key + ": " + value, indent);
+ } else {
+ throw new EvalException(loc,
+ "Invalid text format, expected a struct, a string, a bool, or an int but got a "
+ + EvalUtils.getDatatypeName(value) + " for " + container + " '" + key + "'");
+ }
+ }
+
+ private void printTextMessage(String key, Object value, StringBuilder sb,
+ int indent, Location loc) throws EvalException {
+ if (value instanceof SkylarkList) {
+ for (Object item : ((SkylarkList) value)) {
+ // TODO(bazel-team): There should be some constraint on the fields of the structs
+ // in the same list but we ignore that for now.
+ printSimpleTextMessage(key, item, sb, indent, loc, "list element in struct field");
+ }
+ } else {
+ printSimpleTextMessage(key, value, sb, indent, loc, "struct field");
+ }
+ }
+
+ private String escape(String string) {
+ // TODO(bazel-team): use guava's SourceCodeEscapers when it's released.
+ return string.replace("\"", "\\\"").replace("\n", "\\n");
+ }
+
+ private void print(StringBuilder sb, String text, int indent) {
+ for (int i = 0; i < indent; i++) {
+ sb.append(" ");
+ }
+ sb.append(text);
+ sb.append("\n");
+ }
+ };
+}