In the last article I provided a short introduction to the concept of aspects in Bazel. To keep the series going, today I will go over the somewhat lesser-known feature aspect_hints.
What is aspect_hints?
In simple terms, it is an implicit attribute available on every rule that is meant to be consumed by an attached aspect, not by the rule implementation itself. This allows us to convey “hints” to aspects in a lightweight manner.
For example, we may have an aspect that we attach to swift_library to report unused deps, but for whatever reason we need a way to tell the aspect to ignore a specific dependency.
unused_swift_deps aspect
Below is my dummy implementation of an aspect that finds and reports unused Swift deps. It is not a real implementation, but it is enough to showcase the potential of aspect_hints.
So in my aspects.bzl I have the following:
load("@rules_swift//swift:providers.bzl", "SwiftInfo")
UnusedSwiftDepsInfo = provider(fields = ["report_files"])
def _unused_swift_deps(target, ctx):
labels = []
if hasattr(ctx.rule.attr, "deps"):
for dep in ctx.rule.attr.deps:
if SwiftInfo in dep:
for module in dep[SwiftInfo].direct_modules:
labels.append(module.name)
labels.append(str(dep.label))
out = ctx.actions.declare_file("unused_deps_" + ctx.label.name + ".txt")
ctx.actions.write(
output = out,
content = "\n".join(labels),
)
transitive_files = []
for dep in ctx.rule.attr.deps:
if UnusedSwiftDepsInfo in dep:
transitive_files.append(dep[UnusedSwiftDepsInfo].report_files)
all_files = depset(direct = [out], transitive = transitive_files)
return [
UnusedSwiftDepsInfo(report_files = all_files),
DefaultInfo(files = all_files),
]
unused_swift_deps_aspect = aspect(
implementation = _unused_swift_deps,
attr_aspects = ["deps"],
)
All of the concepts in this code are explained in my previous article on aspects, so I won’t go over them again.
Writing an aspect hint
As mentioned above, now we would like the ability to instruct this aspect to ignore certain labels. The way to achieve that will feel familiar:
- Create a provider
- Make an implementation function
- Create a rule
First we create a provider to be able to easily give a hint to our aspect:
UnusedSwiftDepsHintInfo = provider(fields = ["ignore_deps"])
Then an implementation function:
def _ignore_unused_swift_deps_hint_impl(ctx):
return [UnusedSwiftDepsHintInfo(ignore_deps = ctx.attr.ignore_deps)]
Finally, a rule with the single job of filling the UnusedSwiftDepsHintInfo provider:
ignore_unused_swift_deps_hint = rule(
implementation = _ignore_unused_swift_deps_hint_impl,
attrs = {
"ignore_deps": attr.label_list(mandatory = True),
},
)
Reading aspect_hints from the aspect implementation function
Earlier I stated that aspect_hints is an implicit rule attribute that can be read from an attached aspect. Let’s go over how to do that.
In my aspect for reporting unused Swift deps, it is simply a matter of reading the attribute like any other, checking if it carries the desired provider, and then acting upon it.
Because aspect_hints is a list of labels, I will iterate over it, check if it carries UnusedSwiftDepsHintInfo, and skip writing the ignored labels to the report file:
ignored_deps = []
for aspect_hint in getattr(ctx.rule.attr, "aspect_hints", []):
if UnusedSwiftDepsHintInfo in aspect_hint:
for ignored_dep in aspect_hint[UnusedSwiftDepsHintInfo].ignore_deps:
ignored_deps.append(ignored_dep.label)
Now that we collected labels to ignore, we just need to make sure to actually skip them when reporting:
if dep.label in ignored_deps:
print("Ignoring {} dep".format(str(dep.label)))
continue
And that’s it.
Applying it to the swift_library
Here is how my BUILD.bazel looks when the newly created aspect hint is applied:
load("@rules_swift//swift:swift_library.bzl", "swift_library")
load("//:aspects.bzl", "ignore_unused_swift_deps_hint")
ignore_unused_swift_deps_hint(
name = "ignore_deps_hint",
ignore_deps = [":lib2"],
)
swift_library(
name = "lib",
srcs = ["main.swift"],
aspect_hints = [":ignore_deps_hint"],
deps = [":lib2"],
)
swift_library(
name = "lib2",
srcs = ["main.swift"],
deps = [":lib3"],
)
swift_library(
name = "lib3",
srcs = ["main.swift"],
module_name = "CustomModuleName",
)
And if we build:
bazel build :lib --aspects aspects.bzl%unused_swift_deps_aspect
We see that our lib2 dep is being ignored:
DEBUG: /Users/adincebic/developer.noindex/applying-aspects/aspects.bzl:30:30: Ignoring [@@](https://micro.blog/@)//:lib2 dep
INFO: Analyzed target //:lib (1 packages loaded, 5 targets configured, 5 aspect applications).
INFO: Found 1 target...
INFO: Elapsed time: 0.110s, Critical Path: 0.00s
INFO: 2 processes: 9 action cache hit, 2 internal.
INFO: Build completed successfully, 2 total actions
And of course, if we inspect bazel-bin/unused_deps_lib.txt, the file is empty.
Wrapping up
aspect_hints is a relatively simple idea that allows us to achieve powerful things. I kept the example intentionally simple, but feel free to explore widely used rulesets that rely on aspect_hints, like rules_swift for interoperability with C and other languages.