Bazel’s way of attaching additional information and behavior to the build graph is called aspects. They allow us to add extra logic to rules without modifying the rule implementations themselves. Common use cases include validation actions or analysis tasks such as detecting unused dependencies. These are just a few examples; aspects enable much more complex workflows.

A note on using aspects

Earlier, I mentioned that aspects allow us to attach additional logic to rules without modifying rule code. This is true, but only in one of the two ways aspects can be used:

  • Command-line invocation – aspects are applied externally at build time. This is what we will focus on in this article.
  • Attribute attachment – aspects are attached directly to rule attributes, which requires modifying the rule definition. This approach will be covered in the next article.

Writing aspects

Like most things in Bazel, aspects are rule-like and generally follow this pattern:

  1. Write an implementation function that accepts two arguments: target and ctx
  2. Optionally execute actions
  3. Return providers
  4. Create the aspect by calling the aspect() function and passing the implementation and configuration arguments

In this example, we will write an aspect that operates on swift_library targets (specifically, anything that propagates the SwiftInfo provider). The aspect will generate a .txt file containing the names of the Swift modules that the target depends on.

Given the following target:

swift_library(
    name = "lib",
    deps = [":lib2"],
    srcs = glob(["*.swift"]),
)

When the aspect is applied to this target, it will produce a text file containing lib2.

Loading required providers

In aspects.bzl, we first load the SwiftInfo provider from rules_swift:

load("@rules_swift//swift:providers.bzl", "SwiftInfo")

Next, we define our own provider to propagate the generated report files transitively:

SwiftDepsInfo = provider(fields = ["report_files"])

Aspect implementation

Now we can write the implementation function:

def _swift_deps(target, ctx):
    module_names = []

    if hasattr(ctx.rule.attr, "deps"):
        for dep in ctx.rule.attr.deps:
            if SwiftInfo in dep:
                for module in dep[SwiftInfo].direct_modules:
                    module_names.append(module.name)

    out = ctx.actions.declare_file("swift_deps_" + ctx.label.name + ".txt")
    ctx.actions.write(
        output = out,
        content = "\n".join(module_names),
    )

    transitive_files = []
    if hasattr(ctx.rule.attr, "deps"):
        for dep in ctx.rule.attr.deps:
            if SwiftDepsInfo in dep:
                transitive_files.append(dep[SwiftDepsInfo].report_files)

    all_files = depset(direct = [out], transitive = transitive_files)
    return [
        SwiftDepsInfo(report_files = all_files),
        DefaultInfo(files = all_files),
    ]

So the code above does few simple things:

  1. Collects Swift module names from the deps attribute via the SwiftInfo provider
  2. Declares an output file and writes the module names into it
  3. Collects transitive report files so they materialize as build artifacts
  4. Returns both a custom provider (SwiftDepsInfo) and DefaultInfo

Creating the aspect

The final step is to define the aspect itself:

swift_deps_aspect = aspect(
    implementation = _swift_deps,
    attr_aspects = ["deps"],
)

Note: The attr_aspects = ["deps"] argument tells Bazel to propagate this aspect along the deps attribute. In other words, when the aspect is applied to a rule, Bazel will also apply it to every target listed in that rule’s deps.

Running the aspect

Given the following BUILD.bazel file:

load("@rules_swift//swift:swift_library.bzl", "swift_library")

swift_library(
    name = "lib",
    srcs = ["main.swift"],
    deps = [":lib2"],
)

swift_library(
    name = "lib2",
    srcs = ["main.swift"],
    deps = [":lib3"],
)

swift_library(
    name = "lib3",
    srcs = ["main.swift"],
    module_name = "CustomModule",
)

Running:

bazel build :lib --aspects aspects.bzl%swift_deps_aspect

will produce three additional text files which you can locate under bazel-bin/:

  • swift_deps_lib.txt – contains lib2
  • swift_deps_lib2.txt – contains CustomModule
  • swift_deps_lib3.txt – empty, since it has no dependencies

Why does swift_deps_lib2.txt contain CustomModule instead of lib3? Because we are explicitly extracting the Swift module name from the SwiftInfo provider. If instead we wanted the target name, we could use dep.label.name, or str(dep.label) to get the fully qualified Bazel label.

Going further

This article was a brief introduction to Bazel aspects, intended to set the foundation for the next one. In the next article, we will look at more concrete use cases and explore attaching aspects directly to rule attributes from Starlark as well as utilizing some of the other arguments on that aspect() function.