|
| 1 | +# Extending the rules |
| 2 | + |
| 3 | +:::{important} |
| 4 | +**This is public, but volatile, functionality.** |
| 5 | + |
| 6 | +Extending and customizing the rules is supported functionality, but with weaker |
| 7 | +backwards compatibility guarantees, and is not fully subject to the normal |
| 8 | +backwards compatibility procedures and policies. It's simply not feasible to |
| 9 | +support every possible customization with strong backwards compatibility |
| 10 | +guarantees. |
| 11 | +::: |
| 12 | + |
| 13 | +Because of the rich ecosystem of tools and variety of use cases, APIs are |
| 14 | +provided to make it easy to create custom rules using the existing rules as a |
| 15 | +basis. This allows implementing behaviors that aren't possible using |
| 16 | +wrapper macros around the core rules, and can make certain types of changes |
| 17 | +much easier and transparent to implement. |
| 18 | + |
| 19 | +:::{note} |
| 20 | +It is not required to extend a core rule. The minimum requirement for a custom |
| 21 | +rule is to return the appropriate provider (e.g. {bzl:obj}`PyInfo` etc). |
| 22 | +Extending the core rules is most useful when you want all or most of the |
| 23 | +behavior of a core rule. |
| 24 | +::: |
| 25 | + |
| 26 | +Follow or comment on https://github.com/bazelbuild/rules_python/issues/1647 |
| 27 | +for the development of APIs to support custom derived rules. |
| 28 | + |
| 29 | +## Creating custom rules |
| 30 | + |
| 31 | +Custom rules can be created using the core rules as a basis by using their rule |
| 32 | +builder APIs. |
| 33 | + |
| 34 | +* [`//python/apis:executables.bzl`](#python-apis-executables-bzl): builders for |
| 35 | + executables. |
| 36 | +* [`//python/apis:libraries.bzl`](#python-apis-libraries-bzl): builders for |
| 37 | + libraries. |
| 38 | + |
| 39 | +These builders create {bzl:obj}`ruleb.Rule` objects, which are thin |
| 40 | +wrappers around the keyword arguments eventually passed to the `rule()` |
| 41 | +function. These builder APIs give access to the _entire_ rule definition and |
| 42 | +allow arbitrary modifications. |
| 43 | + |
| 44 | +This is level of control is powerful, but also volatile. A rule definition |
| 45 | +contains many details that _must_ change as the implementation changes. What |
| 46 | +is more or less likely to change isn't known in advance, but some general |
| 47 | +rules are: |
| 48 | + |
| 49 | +* Additive behavior to public attributes will be less prone to breaking. |
| 50 | +* Internal attributes that directly support a public attribute are likely |
| 51 | + reliable. |
| 52 | +* Internal attributes that support an action are more likely to change. |
| 53 | +* Rule toolchains are moderately stable (toolchains are mostly internal to |
| 54 | + how a rule works, but custom toolchains are supported). |
| 55 | + |
| 56 | +## Example: validating a source file |
| 57 | + |
| 58 | +In this example, we derive from `py_library` a custom rule that verifies source |
| 59 | +code contains the word "snakes". It does this by: |
| 60 | + |
| 61 | +* Adding an implicit dependency on a checker program |
| 62 | +* Calling the base implementation function |
| 63 | +* Running the checker on the srcs files |
| 64 | +* Adding the result to the `_validation` output group (a special output |
| 65 | + group for validation behaviors). |
| 66 | + |
| 67 | +To users, they can use `has_snakes_library` the same as `py_library`. The same |
| 68 | +is true for other targets that might consume the rule. |
| 69 | + |
| 70 | +``` |
| 71 | +load("@rules_python//python/api:libraries.bzl", "libraries") |
| 72 | +load("@rules_python//python/api:attr_builders.bzl", "attrb") |
| 73 | +
|
| 74 | +def _has_snakes_impl(ctx, base): |
| 75 | + providers = base(ctx) |
| 76 | +
|
| 77 | + out = ctx.actions.declare_file(ctx.label.name + "_snakes.check") |
| 78 | + ctx.actions.run( |
| 79 | + inputs = ctx.files.srcs, |
| 80 | + outputs = [out], |
| 81 | + executable = ctx.attr._checker[DefaultInfo].files_to_run, |
| 82 | + args = [out.path] + [f.path for f in ctx.files.srcs], |
| 83 | + ) |
| 84 | + prior_ogi = None |
| 85 | + for i, p in enumerate(providers): |
| 86 | + if type(p) == "OutputGroupInfo": |
| 87 | + prior_ogi = (i, p) |
| 88 | + break |
| 89 | + if prior_ogi: |
| 90 | + groups = {k: getattr(prior_ogi[1], k) for k in dir(prior_ogi)} |
| 91 | + if "_validation" in groups: |
| 92 | + groups["_validation"] = depset([out], transitive=groups["_validation"]) |
| 93 | + else: |
| 94 | + groups["_validation"] = depset([out]) |
| 95 | + providers[prior_ogi[0]] = OutputGroupInfo(**groups) |
| 96 | + else: |
| 97 | + providers.append(OutputGroupInfo(_validation=depset([out]))) |
| 98 | + return providers |
| 99 | +
|
| 100 | +def create_has_snakes_rule(): |
| 101 | + r = libraries.py_library_builder() |
| 102 | + base_impl = r.implementation() |
| 103 | + r.set_implementation(lambda ctx: _has_snakes_impl(ctx, base_impl)) |
| 104 | + r.attrs["_checker"] = attrb.Label( |
| 105 | + default="//:checker", |
| 106 | + executable = True, |
| 107 | + ) |
| 108 | + return r.build() |
| 109 | +has_snakes_library = create_has_snakes_rule() |
| 110 | +``` |
| 111 | + |
| 112 | +## Example: adding transitions |
| 113 | + |
| 114 | +In this example, we derive from `py_binary` to force building for a particular |
| 115 | +platform. We do this by: |
| 116 | + |
| 117 | +* Adding an additional output to the rule's cfg |
| 118 | +* Calling the base transition function |
| 119 | +* Returning the new transition outputs |
| 120 | + |
| 121 | +```starlark |
| 122 | + |
| 123 | +load("@rules_python//python/api:executables.bzl", "executables") |
| 124 | + |
| 125 | +def _force_linux_impl(settings, attr, base_impl): |
| 126 | + settings = base_impl(settings, attr) |
| 127 | + settings["//command_line_option:platforms"] = ["//my/platforms:linux"] |
| 128 | + return settings |
| 129 | + |
| 130 | +def create_rule(): |
| 131 | + r = executables.py_binary_rule_builder() |
| 132 | + base_impl = r.cfg.implementation() |
| 133 | + r.cfg.set_implementation( |
| 134 | + lambda settings, attr: _force_linux_impl(settings, attr, base_impl) |
| 135 | + ) |
| 136 | + r.cfg.add_output("//command_line_option:platforms") |
| 137 | + return r.build() |
| 138 | + |
| 139 | +py_linux_binary = create_linux_binary_rule() |
| 140 | +``` |
| 141 | + |
| 142 | +Users can then use `py_linux_binary` the same as a regular py_binary. It will |
| 143 | +act as if `--platforms=//my/platforms:linux` was specified when building it. |
0 commit comments