So far I went through writing the aspect, but I haven’t shown how to run actions from it to do something useful. Remember: we can do very little during Starlark evaluation (Bazel’s analysis phase). If we want to read files, inspect sources, or produce results, the work has to happen in the execution phase by running actions.
NOTE: Before continuing, it helps to read my earlier articles on Bazel aspects, since I’m going to assume that background.
ctx.actions
Every aspect and rule gets a ctx object, which gives us access to ctx.actions — including run(). Sticking with the “unused Swift deps” example, here’s what invoking a tool looks like:
ctx.actions.run(
inputs = [input] + source_files,
outputs = [out],
executable = ctx.executable._tool,
arguments = [arguments],
)
This snippet captures the essentials of a Bazel action:
- Inputs: all files the tool may read
- Outputs: files the tool is expected to generate
- Executable: the tool binary itself
- Arguments: the tool’s command-line arguments
Bazel uses these declarations to compute action keys, decide when work must be re-executed, and make remote/local caching correct.
Inputs
Here, inputs are:
- a JSON file that describes what we want to be analyzed
- the Swift source files to scan
First, we build the JSON payload:
payload = {
"targetLabel": str(target.label),
"sources": [f.path for f in source_files],
"deps": deps_to_analyze,
}
encoded_json = json.encode(payload)
input = ctx.actions.declare_file(ctx.label.name + "_input.json")
Like I described in my first article, declaring the file is not enough — we also need an action to write it:
ctx.actions.write(
output = input,
content = encoded_json,
)
Outputs, arguments and tool
In this case the tool produces a single JSON output that contains the unused dependency results:
out = ctx.actions.declare_file(ctx.label.name + "_unused_deps.json")
For arguments, instead of building a plain list, Bazel provides ctx.actions.args():
arguments = ctx.actions.args()
arguments.add(input)
arguments.add("--output")
arguments.add(out)
One reasonable question is: why bother with Args instead of a list?
Because command lines can get huge in real builds (compilers and linkers are the classic examples). Args lets Bazel represent and expand arguments efficiently without paying the cost of building enormous Starlark lists.
arguments = [arguments]
There is way more information about this topic on the official bazel docs page.
Running the tool
To run a tool from an aspect (or a rule), the two common approaches are:
- Private attribute
- Toolchains
Here I use a private attribute for simplicity (I’ve covered toolchains in a previous post).
We add a private attribute by prefixing it with _:
unused_swift_deps_aspect = aspect(
implementation = _unused_swift_deps_impl,
attrs = {
"_tool": attr.label(
default = "//tools/FindUnusedSwiftDeps",
executable = True,
cfg = "exec",
),
},
attr_aspects = ["deps"],
)
Then we give it a default label (the tool target), mark it executable, and set cfg = "exec" so it runs on the execution platform.
Inside the implementation, you reference it as:
tool = ctx.executable._tool
At that point, you can wire it into ctx.actions.run(...) and Bazel will execute it as part of the build.
NOTE: I intentionally omited the code for the tool itself to keep the focus on starlark.
Closing thoughts
At this point it is clear how concept of aspects in bazel can be powerful. I probably won’t be writing more about aspects directly but may share some of what I wrote and found useful.