4.8.2 Aspect Implementation Basics
An aspect is declared with aspect() and an implementation function — the surface is parallel to 4.2.1 Rule Function, but, during analysis, its result attaches to existing dependency edges instead of defining a new target kind.1 The aspect declaration says how the shadow graph from 4.8.1 Aspects — Cross-Cutting Graph Traversal is built and what each visited node may produce: providers, actions, and named output groups.2
The aspect() Declaration
aspect() is a global factory. Its return value must be stored in a global symbol in a .bzl file so Bazel and users can refer to it by <file label>%<global name>.3 The smallest useful declaration names the implementation function and the edges to follow:
print_aspect = aspect(
implementation = _print_aspect_impl,
attr_aspects = ["deps"],
required_providers = [CcInfo],
)
The most important parameters at this level are:4
| Parameter | What it decides |
|---|---|
implementation | The Starlark function Bazel runs for every visited target.5 |
attr_aspects | Which attributes of each visited rule the aspect follows next.6 |
required_providers | Which targets are eligible — the aspect only stops on targets whose rule advertises one of these provider sets.7 |
attrs | Aspect attributes: private label deps for tools and public string parameters.8 |
provides | Providers the implementation promises to return. Required when another aspect's required_aspect_providers reads them.9 |
Other parameters are useful but secondary for a first aspect. toolchains, exec_groups, and toolchains_aspects connect the aspect to platform-aware tooling. requires, required_aspect_providers, and provides chain aspects together. propagation_predicate and the function form of attr_aspects enable runtime propagation decisions — those belong in 4.8.6 Dynamic Aspect Propagation.10 apply_to_generating_rules redirects propagation from an output file back to the rule that generates it.11
The Implementation Function
An aspect implementation has exactly two parameters: the target it is being applied to and ctx, the analysis context.12 The shape is (Target, ctx) → [Provider].13
def _print_aspect_impl(target, ctx):
if hasattr(ctx.rule.attr, "srcs"):
for src in ctx.rule.attr.srcs:
for f in src.files.to_list():
print(f.path)
return []
Two things separate this from a rule implementation:
targetis the visited rule's configured target. The aspect reads providers from it with the usualtarget[SomeInfo]indexing.14ctx.ruleexposes the visited rule's own metadata, even though the aspect didn't declare those attributes.ctx.rule.attris the attribute struct,ctx.rule.kindis the rule class name ("cc_library","py_binary", …), andctx.rule.executable/ctx.rule.file/ctx.rule.files/ctx.rule.toolchainsmirror the rule's analysis-time accessors.15ctx.rule.kindis the standard hook for rule-type-specific logic, such as "only emit a Makefile rule forcc_binary."16
ctx still gives access to the aspect's own attrs through ctx.attr and to action APIs through ctx.actions, exactly like a rule context.17 An aspect can therefore call ctx.actions.run(), declare files, and emit OutputGroupInfo the same way a rule does.18
Propagation Edges
attr_aspects is the list of attribute names the aspect follows. Common values are ["deps"] and ["deps", "exports"]. The wildcard ["*"] propagates along every attribute of every visited rule.19 Bazel rewrites the visited rule's view of those attributes for the duration of the aspect: if a target X has Y and Z in deps, then ctx.rule.attr.deps for A(X) is [A(Y), A(Z)] — Target objects that already carry the aspect's providers.20 This is the mechanism behind transitive collection patterns.
def _file_count_aspect_impl(target, ctx):
count = 0
if hasattr(ctx.rule.attr, "srcs"):
for src in ctx.rule.attr.srcs:
for f in src.files.to_list():
if ctx.attr.extension == "*" or ctx.attr.extension == f.extension:
count = count + 1
for dep in ctx.rule.attr.deps:
count = count + dep[FileCountInfo].count
return [FileCountInfo(count = count)]
The aspect reads the dep's FileCountInfo after applying itself to that dep — the recursion is implicit in the propagation list.21
required_providers narrows traversal to targets whose rules advertise certain providers. The value is a list of provider lists with OR-of-ANDs semantics: [[FooInfo], [BarInfo], [BazInfo, QuxInfo]] matches a rule that provides FooInfo, or BarInfo, or both BazInfo and QuxInfo.22 Use it to keep the shadow graph focused — a Java-only aspect should not stop at cc_library nodes.23 The wildcard ["*"] propagation combined with no required_providers is the most aggressive shape and can cause subtle problems: cc_shared_library's linking aspect uses "*" so it can reach object files reachable through any attribute, and a custom rule that incidentally returns CcInfo ends up considered for linking by accident.24
Aspect Attrs
Aspects can declare their own attributes, but the rules differ from regular rules.25
-
Private attributes (names starting with
_) must have defaults and must be of typelabelorlabel_list. They are how an aspect carries the tools and configuration files it needs without forcing every visited rule to expose them:26attrs = { "_protoc": attr.label( default = Label("//tools:protoc"), executable = True, cfg = "exec", ), } -
Public attributes are parameters. In the current API reference, explicit public aspect attrs must be
stringattrs and use thevaluesrestriction so Bazel can validate parameter values against the consuming rule.27 For command-line aspects, parameter values come from--aspects_parametersand thevaluesrestriction may be relaxed.28
That public-parameter restriction is what makes the aspect-factory pattern common in production rulesets: a top-level Starlark function captures richer configuration, such as a label, and returns an aspect() whose private attr defaults to that value.29
def stylelint_aspect(config):
return aspect(
implementation = _stylelint_impl,
attr_aspects = ["deps"],
attrs = {
"_config": attr.label(default = config),
},
)
That is how rules_lint lets each consumer wire their own tool config without exposing a public label-typed aspect parameter.30
Returning Providers
An aspect implementation returns a list of providers, just like a rule.31 The provider set for the aspect application A(X) is the union of the rule's providers on X and the aspect's own providers.32 Two rules differ from regular rule authoring:
- An aspect must not return
DefaultInfo. Default outputs belong to the rule. It is also an error if the rule and the aspect both return the same provider type — with two documented exceptions.33 OutputGroupInfofrom rules and aspects is merged, as long as the rule and aspect declare different group names.InstrumentedFilesInfofrom the aspect supersedes the rule's, used for coverage instrumentation.34
OutputGroupInfo is therefore the standard way an aspect surfaces files to users and tools — IDE drivers, linters, and SBOM aspects use named groups so a single bazel build can request only the aspect's artifacts.35
return [
FileCountInfo(count = count),
OutputGroupInfo(file_count_report = depset([report])),
]
Aspect-generated actions should also use stable mnemonic values. Mnemonics show up in profiles, bazel aquery, and CI logs alongside actions from regular rules.36 For multi-action augmentation patterns — adding instrumentation or coverage to an existing rule's action without reconstructing its full input list — see 4.8.7 shadowed_action — Action Composition.
Two Ways To Invoke
An aspect runs only when something asks for it. There are two invocation modes, called command-line aspects and rule-propagated aspects in the official docs.37
Command-line — pass --aspects and a label-style spec on a normal bazel build:38
bazel build //pkg:app \
--aspects=//tools/lint:linters.bzl%ruff_aspect \
--output_groups=rules_lint_human,rules_lint_machine
The spec format is <.bzl file label>%<aspect global name>. Command-line aspects are how IDE plugins, lint drivers, and SBOM generators run aspects across an existing graph without changing any BUILD files.39
Rule-propagated — a custom rule attaches the aspect to one of its label attributes:40
file_count_rule = rule(
implementation = _file_count_rule_impl,
attrs = {
"deps": attr.label_list(aspects = [file_count_aspect]),
"extension": attr.string(default = "*"),
},
)
Here, building any file_count_rule target runs file_count_aspect on the targets in deps and on their transitive deps (because attr_aspects = ["deps"]).41 Aspect parameters are taken from the requesting rule's attribute of the same name and type — that is why the rule has a matching extension attr with a default value, and why public string parameters need a values restriction on the aspect side.42
The choice between the two is a deployment question, not an implementation one. Both run the same implementation function over the same shadow graph. They differ only in how the request reaches the graph. Lint workflows usually start as command-line aspects because they don't want to touch rule sets. Once that quality gate matures, the team may decide to fold it into the rule contract instead — that is the comparison developed in 4.8.3 Validation Actions vs Aspects.
The mini-ruleset assembles the core pieces in
glyph_metadata_aspect.bzl:
an implementation that registers an output, propagation over two attributes,
required_providers, and an OutputGroupInfo result. Request it with
bazel build //examples/basic:metadata --aspects=//tools/aspects:glyph_metadata_aspect.bzl%glyph_metadata_aspect --output_groups=glyph_metadata.
An aspect is an analysis-time overlay that follows selected dependency edges, not a new target kind.
aspect() declares a parallel function over an existing graph: implementation plus attr_aspects decide where it runs, required_providers decides what it stops on, attrs give it tools and parameters, and the returned providers and output groups are how its results escape. Public params are string-valued. Private label attrs carry tools and richer config. Never return DefaultInfo. Merge with rules through OutputGroupInfo. Pick command-line invocation when you can't change rule sets, rule-propagated when the aspect is part of a target's contract.
Check your understanding · 4 questions
1.An aspect's implementation function is called with (target, ctx). What does ctx.rule give the aspect that ctx alone does not?
Select one answer
2.Which of the following are valid for an aspect's attrs?
Select all that apply
3.True or false: provider rules for aspects
Choose True or False for each sentence
DefaultInfo to add files to the visited target's default outputs.OutputGroupInfo with different group names, Bazel merges the providers.ctx.actions.run().4.Match each aspect() parameter to what it controls.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
attr_aspectsrequired_providersattrsimplementationFootnotes
-
Aspects — aspects are similar to rules in that they have an implementation function that generates actions and returns providers, but they propagate along edges in a shadow graph instead of defining new target kinds. ↩
-
Aspects — typical use cases: IDE project info, code generation, linting, coverage, metadata collection. ↩
-
.bzl files —
aspect()creates an aspect value that must be stored in a global value. Reference format<file label>%<aspect name>for--aspects. ↩ -
.bzl files — full parameter list for
aspect(). ↩ -
.bzl files —
implementationis a Starlark function with two parameters:Targetandctx, evaluated during analysis for each application of the aspect. ↩ -
.bzl files —
attr_aspectsis a list of attribute names, or"*", that the aspect propagates along. ↩ -
.bzl files —
required_providerslimits propagation to targets whose rules advertise the listed providers, with OR-of-ANDs semantics on lists of lists. ↩ -
.bzl files —
attrsmaps attribute name toattr.*object. Implicit_attrs must belabel/label_listwith defaults. Explicit attrs must bestringand usevalues. ↩ -
.bzl files —
providesis required when another aspect'srequired_aspect_providersdepends on them. ↩ -
.bzl files —
toolchains,toolchains_aspects,exec_groups,propagation_predicate, and function-formattr_aspectsare documented as the dynamic and toolchain extension points. ↩ -
.bzl files —
apply_to_generating_rulespropagates from an output file to its generating rule. ↩ -
Aspects — implementation function takes
targetandctx. Returns providers, may generate actions. ↩ -
Bazel's Take on (Cc) Shared Libraries — aspect is essentially a function
(Target, Context) → [Provider]walking the build tree and enriching it. ↩ -
Aspects — the implementation can examine providers provided by the target via the
targetargument. ↩ -
rule_attributes —
ctx.ruleexposesattr,executable,file,files,kind,toolchains,exec_groups, andvarof the rule the aspect is applied to. ↩ -
rule_attributes —
kindis the rule class name, e.g.cc_library. ↩ -
Aspects — parameters and private attributes are passed in the attributes of
ctx. ↩ -
Aspects — aspect implementation functions are like rule implementations: they return providers and can generate actions. ↩
-
Aspects —
attr_aspectspropagation list."*"propagates along all attributes. ↩ -
Aspects — values of attributes along which the aspect propagates are replaced with the results of applying the aspect to them. ↩
-
Aspects — file_count_aspect example accumulates
FileCountInfo.countfromctx.rule.attr.depsafter the aspect has been applied to them. ↩ -
.bzl files —
required_providersis a list of provider lists. Matches a target if it provides all entries of at least one inner list. ↩ -
Aspects —
required_providerskeeps traversal focused on targets that advertise the listed providers, e.g. onlyCcInfo-carrying targets. ↩ -
Bazel's Take on (Cc) Shared Libraries — the linking aspect propagates via
"*"to every edge and looks at every target providingCcInfo. Custom rules that incidentally provideCcInfoget pulled into linking decisions. ↩ -
Aspects —
attrsdefines a set of attributes for an aspect. Type and visibility rules differ from rules. ↩ -
Aspects — private label/label_list attributes can specify dependencies on tools or libraries.
_protocexample withexecutable = True, cfg = "exec". ↩ -
.bzl files — current
aspect()API reference: explicit public aspect attrs must bestringattrs and use thevaluesrestriction. Private attrs must belabel/label_listwith defaults. ↩ -
Aspects — command-line aspects pass parameter values via
--aspects_parametersand thevaluesrestriction may be omitted. ↩ -
Rules_lint: Formatting and Linting All Languages — aspect factory pattern: a function returns an aspect whose
_configprivate label attr is the captured config label. Needed because aspects only allow string parameters publicly and need label types via underscore prefix. ↩ -
Rules_lint: Formatting and Linting All Languages — each linter ships a
*_aspect(config = ...)factory used in userlinters.bzl. ↩ -
Aspects — aspect implementation returns a struct/list of providers that are accessible to its dependencies. ↩
-
Aspects — provider set for
A(X)is the union of the rule's providers on X and the aspect's providers. Rule providers are frozen and cannot be modified by the aspect. ↩ -
Aspects — it is an error if a target and the aspect both provide a provider of the same type, except for
OutputGroupInfoandInstrumentedFilesInfo. Aspect implementations may never returnDefaultInfo. ↩ -
Aspects —
OutputGroupInfois merged when rule and aspect declare different output groups.InstrumentedFilesInfois taken from the aspect. ↩ -
Rules_lint: Formatting and Linting All Languages — aspect output groups such as human-readable and machine-readable lint reports decouple lint execution from consumption mode (build failure, test, report). ↩
-
actions —
mnemonicmakes actions identifiable in profiles, logs, andaquery. Same API applies whether the action is registered from a rule or an aspect. ↩ -
Aspects — distinction between rule-propagated aspects and command-line aspects. ↩
-
Aspects — command-line invocation:
bazel build //tgt --aspects file.bzl%aspect_name. Spec format<extension file label>%<aspect top-level name>. ↩ -
Rules_lint: Formatting and Linting All Languages — aspect visits existing
*_libraryrules without modifying them. Designed so no rule set modification is needed. ↩ -
attr —
aspectsparameter onattr.label/attr.label_listattaches aspects to dependencies of the attribute. ↩ -
Aspects —
file_count_ruleinvokesfile_count_aspectviaattr.label_list(aspects = [...]). Aspect is evaluated for the rule and all targets reachable viadeps. ↩ -
Aspects — for rule-propagated aspects, parameter values come from the requesting rule's attribute of the same name and type. ↩