:::{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.
Custom rules can be created using the core rules as a basis by using their rule builder APIs.
//python/apis:executables.bzl
: builders for executables.//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 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).
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()
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.