Skip to content

Latest commit

 

History

History
143 lines (115 loc) · 5.17 KB

extending.md

File metadata and controls

143 lines (115 loc) · 5.17 KB

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 #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.

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 is 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 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 from py_library a custom rule 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 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
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_linux_binary_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.