Oftentimes, it is necessary to validate rule attributes to prevent misuse of a rule. While attributes can be validated as part of the main rule implementation function, there is perhaps a better way to do it.
Rule initializer
This is yet another lesser-known feature of Bazel, where we can declare an initializer function that runs when a rule is instantiated. In my opinion, this is the ideal place to validate inputs.
Take this fictitious rule, where we want to make sure that it only accepts certain modes:
def _init(name, mode = "safe", **kwargs):
if mode not in ["safe", "fast"]:
fail("mode must be 'safe' or 'fast'; got %r" % mode)
return {"mode": mode}
def _impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(out, ctx.attr.mode + "\n")
return [DefaultInfo(files = depset([out]))]
validated_file = rule(
implementation = _impl,
initializer = _init,
attrs = {
"mode": attr.string(),
},
)
Passing an unsupported mode will result in a failure during the loading phase, before Bazel even gets to analyzing the target.
This is an important distinction compared to validating attributes in the rule implementation function, which runs during the analysis phase. With an initializer, invalid inputs are caught earlier, when the rule is instantiated in a BUILD file.
Another interesting detail is that initializers can also modify attribute values by returning a dictionary containing the attributes to override. Attributes that are not returned remain unchanged. In our example, we simply return the validated mode value.
Notice that we also define the default value of mode in the initializer rather than in attr.string(). Initializers run before attribute defaults are applied, so a default specified in the initializer takes precedence. The **kwargs parameter allows us to ignore other public attributes that we don’t need to validate.
Experimental status
The initializer parameter is still marked as experimental in Bazel’s API documentation. However, this designation is outdated.
The --experimental_rule_extension_api flag has been enabled by default since Bazel 8, meaning the feature is no longer restricted to allowlisted users by default. Fabian, a trusted Bazel contributor, has also opened a PR to remove the outdated experimental designation from the documentation.
So despite what the current documentation suggests, feel free to use it.
Conclusion
While there are a couple of ways to validate inputs in a Bazel rule, I think this one deserves to be used more widely since it feels like the right tool for the job.