| # Extending the rules |
| |
| :::{important} |
| **This is public, but volatile, functionality.** |
| |
| Extending and customizing the rules is supported functionality, but with weaker |
| backwards compatibility guarantees, and is not fully subject to the normal |
| backwards compatibility procedures and policies. It's simply not feasible to |
| support every possible customization with strong backwards compatibility |
| guarantees. |
| ::: |
| |
| Because of the rich ecosystem of tools and variety of use cases, APIs are |
| provided to make it easy to create custom rules using the existing rules as a |
| basis. This allows implementing behaviors that aren't possible using |
| wrapper macros around the core rules, and can make certain types of changes |
| much easier and transparent to implement. |
| |
| :::{note} |
| It is not required to extend a core rule. The minimum requirement for a custom |
| rule is to return the appropriate provider (e.g. {bzl:obj}`PyInfo` etc). |
| Extending the core rules is most useful when you want all or most of the |
| behavior of a core rule. |
| ::: |
| |
| Follow or comment on https://github.com/bazel-contrib/rules_python/issues/1647 |
| for the development of APIs to support custom derived rules. |
| |
| ## Creating custom rules |
| |
| Custom rules can be created using the core rules as a basis by using their rule |
| builder APIs. |
| |
| * [`//python/apis:executables.bzl`](#python-apis-executables-bzl): builders for |
| executables. |
| * [`//python/apis:libraries.bzl`](#python-apis-libraries-bzl): builders for |
| libraries. |
| |
| These builders create {bzl:obj}`ruleb.Rule` objects, which are thin |
| wrappers around the keyword arguments eventually passed to the `rule()` |
| function. These builder APIs give access to the _entire_ rule definition and |
| allow arbitrary modifications. |
| |
| This level of control is powerful but also volatile. A rule definition |
| contains many details that _must_ change as the implementation changes. What |
| is more or less likely to change isn't known in advance, but some general |
| rules of thumb are: |
| |
| * Additive behavior to public attributes will be less prone to breaking. |
| * Internal attributes that directly support a public attribute are likely |
| reliable. |
| * Internal attributes that support an action are more likely to change. |
| * Rule toolchains are moderately stable (toolchains are mostly internal to |
| how a rule works, but custom toolchains are supported). |
| |
| ## Example: validating a source file |
| |
| In this example, we derive a custom rule from `py_library` that verifies source |
| code contains the word "snakes". It does this by: |
| |
| * Adding an implicit dependency on a checker program |
| * Calling the base implementation function |
| * Running the checker on the srcs files |
| * Adding the result to the `_validation` output group (a special output |
| group for validation behaviors). |
| |
| To users, they can use `has_snakes_library` the same as `py_library`. The same |
| is true for other targets that might consume the rule. |
| |
| ``` |
| load("@rules_python//python/api:libraries.bzl", "libraries") |
| load("@rules_python//python/api:attr_builders.bzl", "attrb") |
| |
| def _has_snakes_impl(ctx, base): |
| providers = base(ctx) |
| |
| out = ctx.actions.declare_file(ctx.label.name + "_snakes.check") |
| ctx.actions.run( |
| inputs = ctx.files.srcs, |
| outputs = [out], |
| executable = ctx.attr._checker[DefaultInfo].files_to_run, |
| args = [out.path] + [f.path for f in ctx.files.srcs], |
| ) |
| prior_ogi = None |
| for i, p in enumerate(providers): |
| if type(p) == "OutputGroupInfo": |
| prior_ogi = (i, p) |
| break |
| if prior_ogi: |
| groups = {k: getattr(prior_ogi[1], k) for k in dir(prior_ogi)} |
| if "_validation" in groups: |
| groups["_validation"] = depset([out], transitive=groups["_validation"]) |
| else: |
| groups["_validation"] = depset([out]) |
| providers[prior_ogi[0]] = OutputGroupInfo(**groups) |
| else: |
| providers.append(OutputGroupInfo(_validation=depset([out]))) |
| return providers |
| |
| def create_has_snakes_rule(): |
| r = libraries.py_library_builder() |
| base_impl = r.implementation() |
| r.set_implementation(lambda ctx: _has_snakes_impl(ctx, base_impl)) |
| r.attrs["_checker"] = attrb.Label( |
| default="//:checker", |
| executable = True, |
| ) |
| return r.build() |
| has_snakes_library = create_has_snakes_rule() |
| ``` |
| |
| ## Example: adding transitions |
| |
| In this example, we derive a custom rule from `py_binary` to force building for a particular |
| platform. We do this by: |
| |
| * Adding an additional output to the rule's cfg |
| * Calling the base transition function |
| * Returning the new transition outputs |
| |
| ```starlark |
| |
| load("@rules_python//python/api:executables.bzl", "executables") |
| |
| def _force_linux_impl(settings, attr, base_impl): |
| settings = base_impl(settings, attr) |
| settings["//command_line_option:platforms"] = ["//my/platforms:linux"] |
| return settings |
| |
| def create_rule(): |
| r = executables.py_binary_rule_builder() |
| base_impl = r.cfg.implementation() |
| r.cfg.set_implementation( |
| lambda settings, attr: _force_linux_impl(settings, attr, base_impl) |
| ) |
| r.cfg.add_output("//command_line_option:platforms") |
| return r.build() |
| |
| py_linux_binary = create_rule() |
| ``` |
| |
| Users can then use `py_linux_binary` the same as a regular `py_binary`. It will |
| act as if `--platforms=//my/platforms:linux` was specified when building it. |