When writing a Bazel module extension, we sometimes need a way to let the user pass some values to configure things, download binaries from a specified URL, etc. A Bazel module extension itself does not support attributes in the same way repository rules and normal rules do. Instead, it offers another API called tag_class, which lets us define attributes for tags exposed by a module extension.
A single module extension can have multiple tag classes. During extension evaluation, we can iterate over the modules that use our extension and inspect the tags of the class we are interested in.
Downloading a binary
For the purposes of this simplistic article, we will define a very crude repository rule to download any sort of binary. Then we will wire it up through a module extension, which will allow the user to specify the URL and SHA-256 value.
Our repository rule will look like this:
def _binary_download_impl(ctx):
ctx.download(
url = ctx.attr.url,
sha256 = ctx.attr.sha256,
output = ctx.attr.file_name,
)
ctx.file(
"BUILD.bazel",
'exports_files(["%s"], visibility = ["//visibility:public"])' % ctx.attr.file_name,
)
binary_download = repository_rule(
implementation = _binary_download_impl,
attrs = {
"url": attr.string(mandatory = True),
"sha256": attr.string(mandatory = True),
"file_name": attr.string(mandatory = True),
},
)
Then we need to make a module extension with a tag class:
binary = tag_class(
attrs = {
"name": attr.string(mandatory = True),
"url": attr.string(mandatory = True),
"sha256": attr.string(mandatory = True),
},
)
def _impl(module_ctx):
for module in module_ctx.modules:
for binary_tag in module.tags.binary:
binary_download(
name = binary_tag.name,
url = binary_tag.url,
sha256 = binary_tag.sha256,
file_name = binary_tag.name,
)
binary_download_extension = module_extension(
implementation = _impl,
tag_classes = {
"binary": binary,
},
)
There are two loops here that are worth pointing out.
module_ctx.modules contains the modules participating in our extension. Each module can then have zero or more binary tags, which we access through module.tags.binary.
For every binary tag we find, we call our repository rule and forward the values from the tag to it.
Finally, the user can configure the extension from their MODULE.bazel:
binary_download = use_extension(
"//:extensions.bzl",
"binary_download_extension",
)
binary_download.binary(
name = "some_binary",
url = "https://example.com/some_binary",
sha256 = "...",
)
use_repo(binary_download, "some_binary")
And that’s it.
binary_download.binary(...) creates a tag containing the values we defined in our tag_class. When Bazel evaluates the module extension, _impl receives those tags through module_ctx, and our extension can use them to create repositories.
In this case, the flow is essentially:
MODULE.bazel
|
| binary(name, url, sha256)
v
tag_class
|
v
module extension
|
| binary_download(...)
v
repository rule
|
v
downloaded binary
Conclusion
This stuff is reasonably well covered in Bazel’s official docs, but I felt the need to try and simplify it further. It is also true that I haven’t come across anything more interesting in the past week. So, looking forward to the next one.