Add native.rule(NAME), which returns the attributes of a previously defined rule.
Add native.rules(), which returns all previously defined rules.
These primitives can be used to write Skylark extensions that aggregate over the contents of a BUILD file, eg.
def instantiate_if_needed(name):
n = name + "_wrapped"
if not native.rule(n):
py_test(name = n , ... )
def archive_cc_src_files(tag):
all_src = []
for r in native.rules().values():
if tag in r["tags"] and r["kind"] == "cc_library":
all_src.append(r["srcs"])
native.genrule(cmd = "zip $@ $^", srcs = all_src, outs = ["out.zip"])
RELNOTES: Support aggregation over existing rules in Skylark extensions
through native.rules and native.rule.
--
MOS_MIGRATED_REVID=112249050
diff --git a/site/docs/skylark/cookbook.md b/site/docs/skylark/cookbook.md
index 4952d91..c4f718f 100644
--- a/site/docs/skylark/cookbook.md
+++ b/site/docs/skylark/cookbook.md
@@ -59,6 +59,76 @@
macro(name = "myrule")
```
+## <a name="conditional-instantiation"></a>Conditional instantiation.</a>
+
+Macros can look at previously instantiated rules. This is done with
+`native.rule`, which returns information on a single rule defined in the same
+`BUILD` file, eg.,
+
+```python
+native.rule("descriptor_proto")
+```
+
+This is useful to avoid instantiating the same rule twice, which is an
+error. For example, the following rule will simulate a test suite, instantiating
+tests for diverse flavors of the same test.
+
+`extension.bzl`:
+
+```python
+def system_test(test_file, flavor):
+ n = "system_test_%s_%s_test" % (test_file, flavor)
+ if native.rule(n) == None:
+ native.py_test(
+ name = n,
+ srcs = [ "test_driver.py", test_file ],
+ args = [ "--flavor=" + flavor])
+ return n
+
+def system_test_suite(name, flavors=["default"], test_files):
+ ts = []
+ for flavor in flavors:
+ for test in test_files:
+ ts.append(system_test(name, flavor, test))
+ native.test_suite(name = name, tests = ts)
+```
+
+In the following BUILD file, note how `(fast, basic_test.py)` is emitted for
+both the `smoke` test suite and the `thorough` test suite.
+
+```python
+load("/pkg/extension", "system_test_suite")
+
+# Run all files through the 'fast' flavor.
+system_test_suite("smoke", flavors=["fast"], glob(["*_test.py"]))
+
+# Run the basic test through all flavors.
+system_test_suite("thorough", flavors=["fast", "debug", "opt"], ["basic_test.py"])
+```
+
+
+## <a name="aggregation"></a>Aggregating over the BUILD file.</a>
+
+Macros can collect information from the BUILD file as processed so far. We call
+this aggregation. The typical example is collecting data from all rules of a
+certain kind. This is done by calling `native.rules`, which returns a
+dictionary representing all rules defined so far in the current BUILD file. The
+dictionary has entries of the form `name` => `rule`, with the values using the
+same format as `native.rule`.
+
+```python
+def archive_cc_src_files(tag):
+ """Create an archive of all C++ sources that have the given tag."""
+ all_src = []
+ for r in native.rules().values():
+ if tag in r["tags"] and r["kind"] == "cc_library":
+ all_src.append(r["srcs"])
+ native.genrule(cmd = "zip $@ $^", srcs = all_src, outs = ["out.zip"])
+```
+
+Since `native.rules` constructs a potentially large dictionary, you should avoid
+calling it repeatedly within BUILD file.
+
## <a name="empty"></a>Empty rule
Minimalist example of a rule that does nothing. If you build it, the target will
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Package.java b/src/main/java/com/google/devtools/build/lib/packages/Package.java
index bc78044..ce49383 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Package.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Package.java
@@ -49,6 +49,8 @@
import java.util.Map;
import java.util.Set;
+import javax.annotation.Nullable;
+
/**
* A package, which is a container of {@link Rule}s, each of
* which contains a dictionary of named attributes.
@@ -1045,6 +1047,11 @@
return Package.getTargets(targets);
}
+ @Nullable
+ public Target getTarget(String name) {
+ return targets.get(name);
+ }
+
/**
* Returns an (immutable, unordered) view of all the targets belonging to
* this package which are instances of the specified class.
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
index 1de065e..f738a0d 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
@@ -66,9 +66,12 @@
import com.google.devtools.build.lib.vfs.UnixGlob;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -828,6 +831,112 @@
}
};
+ @Nullable
+ static Map<String, Object> callGetRuleFunction(
+ String name, FuncallExpression ast, Environment env)
+ throws EvalException, ConversionException {
+ PackageContext context = getContext(env, ast);
+ Target target = context.pkgBuilder.getTarget(name);
+
+ return targetDict(target);
+ }
+
+ @Nullable
+ private static Map<String, Object> targetDict(Target target) {
+ if (target == null && !(target instanceof Rule)) {
+ return null;
+ }
+ Map<String, Object> values = new TreeMap<>();
+
+ Rule rule = (Rule) target;
+ AttributeContainer cont = rule.getAttributeContainer();
+ for (Attribute attr : rule.getAttributes()) {
+ if (!Character.isAlphabetic(attr.getName().charAt(0))) {
+ continue;
+ }
+
+ Object val = skylarkifyValue(cont.getAttr(attr.getName()), target.getPackage());
+ if (val == null) {
+ continue;
+ }
+ values.put(attr.getName(), val);
+ }
+
+ values.put("name", rule.getName());
+ values.put("kind", rule.getRuleClass());
+ return values;
+ }
+
+ /**
+ * Converts back to type that will work in BUILD and skylark,
+ * such as string instead of label, SkylarkList instead of List,
+ * Returns null if we don't want to export the value.
+ *
+ * <p>All of the types returned are immutable. If we want, we can change this to
+ * immutable in the future, but this is the safe choice for now.
+ */
+ private static Object skylarkifyValue(Object val, Package pkg) {
+ if (val == null) {
+ return null;
+ }
+ if (val instanceof Integer) {
+ return val;
+ }
+ if (val instanceof String) {
+ return val;
+ }
+ if (val instanceof Label) {
+ Label l = (Label) val;
+ if (l.getPackageName().equals(pkg.getName())) {
+ return ":" + l.getName();
+ }
+ return l.getCanonicalForm();
+ }
+ if (val instanceof List) {
+ List<Object> l = new ArrayList<>();
+ for (Object o : (List) val) {
+ l.add(skylarkifyValue(o, pkg));
+ }
+
+ return SkylarkList.Tuple.copyOf(l);
+ }
+ if (val instanceof Map) {
+ Map<Object, Object> m = new TreeMap<>();
+ for (Map.Entry<?, ?> e : ((Map<?, ?>) val).entrySet()) {
+ m.put(skylarkifyValue(e.getKey(), pkg), skylarkifyValue(e.getValue(), pkg));
+ }
+ return m;
+ }
+ if (val.getClass().isAnonymousClass()) {
+ // Computed defaults. They will be represented as
+ // "deprecation": com.google.devtools.build.lib.analysis.BaseRuleClasses$2@6960884a,
+ // Filter them until we invent something more clever.
+ return null;
+ }
+
+ // Add any types we want to allow through here.
+ return null;
+ }
+
+ static Map callGetRulesFunction(FuncallExpression ast, Environment env) throws EvalException {
+
+ PackageContext context = getContext(env, ast);
+ Collection<Target> targets = context.pkgBuilder.getTargets();
+
+ // Sort by name.
+ Map<String, Map<String, Object>> rules = new TreeMap<>();
+ for (Target t : targets) {
+ if (t instanceof Rule) {
+ Map<String, Object> m = targetDict(t);
+ Preconditions.checkNotNull(m);
+
+ rules.put(t.getName(), m);
+ }
+ }
+
+ return rules;
+ }
+
static Runtime.NoneType callPackageFunction(String name, Object packagesO, Object includesO,
FuncallExpression ast, Environment env) throws EvalException, ConversionException {
PackageContext context = getContext(env, ast);
@@ -1082,7 +1191,7 @@
}
/**
- * Same as {@link #createPackage}, but does the required validation of "packageName" first,
+ * Same as createPackage, but does the required validation of "packageName" first,
* throwing a {@link NoSuchPackageException} if the name is invalid.
*/
@VisibleForTesting
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkNativeModule.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkNativeModule.java
index be648b0..ad73b16 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/SkylarkNativeModule.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkNativeModule.java
@@ -26,6 +26,8 @@
import com.google.devtools.build.lib.syntax.SkylarkSignatureProcessor;
import com.google.devtools.build.lib.syntax.Type.ConversionException;
+import java.util.Map;
+
/**
* A class for the Skylark native module.
*/
@@ -39,34 +41,111 @@
public class SkylarkNativeModule {
// TODO(bazel-team): shouldn't we return a SkylarkList instead?
- @SkylarkSignature(name = "glob", objectType = SkylarkNativeModule.class,
- returnType = SkylarkList.class,
- doc = "Glob returns a list of every file in the current package that:<ul>\n"
- + "<li>Matches at least one pattern in <code>include</code>.</li>\n"
- + "<li>Does not match any of the patterns in <code>exclude</code> "
- + "(default <code>[]</code>).</li></ul>\n"
- + "If the <code>exclude_directories</code> argument is enabled (set to <code>1</code>), "
- + "files of type directory will be omitted from the results (default <code>1</code>).",
- mandatoryPositionals = {
- @Param(name = "include", type = SkylarkList.class, generic1 = String.class,
- defaultValue = "[]", doc = "The list of glob patterns to include.")},
- optionalPositionals = {
- @Param(name = "exclude", type = SkylarkList.class, generic1 = String.class,
- defaultValue = "[]", doc = "The list of glob patterns to exclude."),
+ @SkylarkSignature(
+ name = "glob",
+ objectType = SkylarkNativeModule.class,
+ returnType = SkylarkList.class,
+ doc =
+ "Glob returns a list of every file in the current package that:<ul>\n"
+ + "<li>Matches at least one pattern in <code>include</code>.</li>\n"
+ + "<li>Does not match any of the patterns in <code>exclude</code> "
+ + "(default <code>[]</code>).</li></ul>\n"
+ + "If the <code>exclude_directories</code> argument is enabled (set to <code>1</code>), "
+ + "files of type directory will be omitted from the results (default <code>1</code>).",
+ mandatoryPositionals = {
+ @Param(
+ name = "include",
+ type = SkylarkList.class,
+ generic1 = String.class,
+ defaultValue = "[]",
+ doc = "The list of glob patterns to include."
+ )
+ },
+ optionalPositionals = {
+ @Param(
+ name = "exclude",
+ type = SkylarkList.class,
+ generic1 = String.class,
+ defaultValue = "[]",
+ doc = "The list of glob patterns to exclude."
+ ),
// TODO(bazel-team): accept booleans as well as integers? (and eventually migrate?)
- @Param(name = "exclude_directories", type = Integer.class, defaultValue = "1",
- doc = "A flag whether to exclude directories or not.")},
- useAst = true, useEnvironment = true)
- private static final BuiltinFunction glob = new BuiltinFunction("glob") {
- public SkylarkList invoke(
- SkylarkList include, SkylarkList exclude,
- Integer excludeDirectories, FuncallExpression ast, Environment env)
- throws EvalException, ConversionException, InterruptedException {
- env.checkLoadingPhase("native.glob", ast.getLocation());
- return PackageFactory.callGlob(
- null, false, include, exclude, excludeDirectories != 0, ast, env);
- }
- };
+ @Param(
+ name = "exclude_directories",
+ type = Integer.class,
+ defaultValue = "1",
+ doc = "A flag whether to exclude directories or not."
+ )
+ },
+ useAst = true,
+ useEnvironment = true
+ )
+ private static final BuiltinFunction glob =
+ new BuiltinFunction("glob") {
+ public SkylarkList invoke(
+ SkylarkList include,
+ SkylarkList exclude,
+ Integer excludeDirectories,
+ FuncallExpression ast,
+ Environment env)
+ throws EvalException, ConversionException, InterruptedException {
+ env.checkLoadingPhase("native.glob", ast.getLocation());
+ return PackageFactory.callGlob(
+ null, false, include, exclude, excludeDirectories != 0, ast, env);
+ }
+ };
+
+ @SkylarkSignature(
+ name = "rule",
+ objectType = SkylarkNativeModule.class,
+ returnType = Object.class,
+ doc =
+ "Returns a dictionary representing the attributes of a previously defined rule, "
+ + "or None if the rule does not exist.",
+ mandatoryPositionals = {
+ @Param(name = "name", type = String.class, doc = "The name of the rule.")
+ },
+ useAst = true,
+ useEnvironment = true
+ )
+ private static final BuiltinFunction getRule =
+ new BuiltinFunction("rule") {
+ public Object invoke(String name, FuncallExpression ast, Environment env)
+ throws EvalException, InterruptedException {
+ env.checkLoadingPhase("native.rule", ast.getLocation());
+ Map<String, Object> rule = PackageFactory.callGetRuleFunction(name, ast, env);
+ if (rule != null) {
+ return rule;
+ }
+
+ return Runtime.NONE;
+ }
+ };
+
+ /*
+ If necessary, we could allow filtering by tag (anytag, alltags), name (regexp?), kind ?
+ For now, we ignore this, since users can implement it in Skylark.
+ */
+ @SkylarkSignature(
+ name = "rules",
+ objectType = SkylarkNativeModule.class,
+ returnType = Map.class,
+ doc =
+ "Returns a dict containing all the rules instantiated so far. "
+ + "The map key is the name of the rule. The map value is equivalent to the "
+ + "get_rule output for that rule.",
+ mandatoryPositionals = {},
+ useAst = true,
+ useEnvironment = true
+ )
+ private static final BuiltinFunction getRules =
+ new BuiltinFunction("rules") {
+ public Map invoke(FuncallExpression ast, Environment env)
+ throws EvalException, InterruptedException {
+ env.checkLoadingPhase("native.rules", ast.getLocation());
+ return PackageFactory.callGetRulesFunction(ast, env);
+ }
+ };
@SkylarkSignature(name = "package_group", objectType = SkylarkNativeModule.class,
returnType = Runtime.NoneType.class,
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
index a2af3e7..4ed30ba 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
@@ -354,6 +354,75 @@
}
@Test
+ public void testGetRule() throws Exception {
+ scratch.file("test/skylark/BUILD");
+ scratch.file(
+ "test/skylark/rulestr.bzl",
+ "def rule_dict(name):",
+ " return native.rule(name)",
+ "def rules_dict():",
+ " return native.rules()",
+ "def nop(ctx):",
+ " pass",
+ "nop_rule = rule(attrs = {'x': attr.label()}, implementation = nop)",
+ "consume_rule = rule(attrs = {'s': attr.string_list()}, implementation = nop)");
+
+ scratch.file(
+ "test/getrule/BUILD",
+ "load('/test/skylark/rulestr', 'rules_dict', 'rule_dict', 'nop_rule', 'consume_rule')",
+ "genrule(name = 'a', outs = ['a.txt'], tools = [ '//test:bla' ], cmd = 'touch $@')",
+ "nop_rule(name = 'c', x = ':a')",
+ "rlist= rules_dict()",
+ "consume_rule(name = 'all_str', s = [rlist['a']['kind'], rlist['a']['name'], ",
+ " rlist['c']['kind'], rlist['c']['name']])",
+ "adict = rule_dict('a')",
+ "cdict = rule_dict('c')",
+ "consume_rule(name = 'a_str', ",
+ " s = [adict['kind'], adict['name'], adict['outs'][0], adict['tools'][0]])",
+ "consume_rule(name = 'genrule_attr', ",
+ " s = adict.keys())",
+ "consume_rule(name = 'c_str', s = [cdict['kind'], cdict['name'], cdict['x']])");
+
+ SkylarkRuleContext allContext = createRuleContext("//test/getrule:all_str");
+ Object result = evalRuleContextCode(allContext, "ruleContext.attr.s");
+ assertEquals(
+ new SkylarkList.MutableList(ImmutableList.<String>of("genrule", "a", "nop_rule", "c")),
+ result);
+
+ result = evalRuleContextCode(createRuleContext("//test/getrule:a_str"), "ruleContext.attr.s");
+ assertEquals(
+ new SkylarkList.MutableList(
+ ImmutableList.<String>of("genrule", "a", ":a.txt", "//test:bla")),
+ result);
+
+ result = evalRuleContextCode(createRuleContext("//test/getrule:c_str"), "ruleContext.attr.s");
+ assertEquals(
+ new SkylarkList.MutableList(ImmutableList.<String>of("nop_rule", "c", ":a")), result);
+
+ result =
+ evalRuleContextCode(createRuleContext("//test/getrule:genrule_attr"), "ruleContext.attr.s");
+ assertEquals(
+ new SkylarkList.MutableList(
+ ImmutableList.<String>of(
+ "cmd",
+ "compatible_with",
+ "features",
+ "generator_function",
+ "generator_location",
+ "generator_name",
+ "kind",
+ "message",
+ "name",
+ "outs",
+ "restricted_to",
+ "srcs",
+ "tags",
+ "tools",
+ "visibility")),
+ result);
+ }
+
+ @Test
public void testGetRuleAttributeListValue() throws Exception {
SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
Object result = evalRuleContextCode(ruleContext, "ruleContext.attr.outs");