4.8.6 Dynamic Aspect Propagation

extra

Static attr_aspects declares once, at definition time, which dependency edges an aspect will follow. That decision is fine when every visited target answers the same question — "walk deps, walk exports" — but it cannot adapt when the right edge depends on the rule kind, the aspect's own parameters, or what the target actually looks like. Bazel's aspect API has an experimental answer to that: attr_aspects and toolchains_aspects may be written as functions instead of lists, and a separate propagation_predicate can short-circuit propagation entirely.1 Each of those functions receives a propagation_ctx, the small introspection surface Bazel exposes during propagation.2

Treat this as an extension of the basic aspect mechanics from 4.8.2 Aspect Implementation Basics. The shadow-graph idea from 4.8.1 Aspects — Cross-Cutting Graph Traversal is unchanged — the aspect still walks the rule dependency graph, and the propagated edges still become the implementation's view of ctx.rule.attr.<name>. What changes is where the choice of edge happens.

The Three Function-Form Knobs

The aspect() constructor accepts three propagation-time hooks that take a function instead of a static value:1

  • attr_aspects = <function> — returns the list of attribute names to follow for this specific target. Common static values are ["deps"], ["deps", "exports"], or ["*"]. The function form lets the aspect pick a different list per target.
  • toolchains_aspects = <function> — returns the list of toolchain types the aspect should propagate into. Static lists pin a fixed set of toolchain types up front. The function form lets that set depend on which rule is being visited.
  • propagation_predicate = <function> — returns a boolean. When it returns False for a target, the aspect skips that target entirely and does not even consider its outgoing edges. Static aspects approximate this with required_providers. The predicate gives a finer, code-driven decision.

All three are explicitly marked Experimental in the aspect parameter reference.1 That is not a stylistic note: experimental Starlark APIs are allowed to change shape across Bazel releases, and the rule of thumb at the rule-author level is to wrap them behind a single internal helper so a future API churn is a one-file edit.

What propagation_ctx Hands You

Every propagation function receives a propagation_ctx with two fields, attr and rule.2 The propagation hook is consulted first while Bazel builds the aspect's shadow graph. Bazel then analyzes each resulting aspect application with the implementation function and its different ctx. Both steps are part of analysis, not a post-analysis overlay. propagation_ctx is intentionally smaller because it is consulted before the relevant dependency targets are analyzed.

propagation_ctx.attr is a struct of the aspect's public parameters — the same attrs defined on the aspect, restricted to non-private entries, with the values supplied by the rule (for rule-propagated aspects) or by --aspects_parameters (for command-line aspects).2 This is how a single aspect can carry user-supplied configuration into its propagation decision: a predicate that takes propagation_ctx as its only argument can read propagation_ctx.attr.mode and propagate differently for "strict" versus "observe".

propagation_ctx.rule describes the target whose edges Bazel is about to consider. Its members are documented in the propagation-context API:2

  • rule.label — the target's label.
  • rule.qualified_kind — the rule kind broken into file_label (the .bzl file that defined the rule, or None for native rules) and rule_name (the rule kind, like "java_library").
  • rule.attr — a struct of the target's attributes. Each attribute exposes value and is_tool. Crucially, dependency attributes are surfaced as labels, not as analyzed Target objects, because analysis has not happened yet for this target's deps.2

That last point is the practical constraint of propagation-time logic: you can branch on attribute shapes (the rule kind, a string flag, a list of labels), but you cannot read providers, inspect unchosen select() branches, or call into the toolchain context. Anything that needs analyzed values still has to live in the aspect's implementation function.

When Function-Form Earns Its Cost

Static attr_aspects should remain the default. The function forms add a small per-edge call into Starlark that runs as Bazel constructs the shadow graph,3 and they hide the propagation list behind code that future readers must trace. Reach for them when a static spelling actually loses information:

  • Rule-kind-aware traversal. An aspect that walks deps for java_library but exports for an interface-only target can branch on propagation_ctx.rule.qualified_kind.rule_name instead of either over-propagating (["deps", "exports"] everywhere, costing extra shadow-graph nodes) or splitting the aspect into two.
  • Parameter-driven scope. A linting aspect with a mode parameter — "library_only" versus "library_and_tests" — can return a different attribute list per mode without duplicating the aspect definition.
  • Skipping whole subtrees. A propagation_predicate that returns False on genrule or on opaque third-party targets keeps the shadow graph small. The same effect is sometimes possible with required_providers, but the predicate composes more cleanly when the discriminator is a rule-kind check rather than a provider check.1

If none of these apply, leave the static list in place. The function-form variants exist for cases where the correct propagation set genuinely depends on the visited target.

Because the three function-form hooks are Experimental — and propagation_ctx exists specifically to feed them — the live API reference is more reliable than any secondary write-up while the surface is still settling. The aspect entry in the .bzl globals reference covers the parameter shapes and the Experimental markers,1 and the propagation_ctx builtin documents the members the propagation function is allowed to inspect.2 Re-check both before adopting these hooks in production code.

key takeaway

Dynamic aspect propagation does not change what an aspect is — it changes when the propagation decision is made. Static attr_aspects decides at aspect-definition time. The function form, toolchains_aspects as a function, and propagation_predicate decide per visited target, using only what propagation_ctx exposes: the aspect's own parameters and the unanalyzed shape of the target. Treat all three as Experimental, prefer the static spelling whenever it captures the real intent, and reach for the function form only when the propagation set legitimately varies per target.

Check your understanding · 4 questions

1.Which of these are exposed by propagation_ctx to a propagation function?

Select all that apply

2.Which aspect() parameter, when given a function, lets the aspect skip a target entirely so its outgoing edges are not even considered?

Select one answer

3.True or false: choosing between static and function-form propagation.

Choose True or False for each sentence

Function-form attr_aspects should be the default. The static list form is mostly historical.
Function-form attr_aspects is worth reaching for when the correct propagation set genuinely depends on the visited target's rule kind or the aspect's own parameters.
All three function-form hooks (attr_aspects, toolchains_aspects, propagation_predicate) are marked Experimental in the aspect() parameter reference.
A propagation_predicate can read providers from the target to decide whether to propagate.

4.When is function-form toolchains_aspects the most relevant hook?

Select one answer

0 of 4 answered

Footnotes

  1. .bzl filesaspect() parameter reference: attr_aspects and toolchains_aspects may be a function, and propagation_predicate is an Experimental boolean function. 1 2 3 4 5

  2. propagation_ctx — context object passed to propagation_predicate, attr_aspects, and toolchains_aspects functions. Exposes attr (aspect parameters) and rule (label, qualified_kind, attribute values represented as labels for deps). 1 2 3 4 5 6

  3. Aspects — shadow-graph construction: only the edges named in the propagation set are followed, and the implementation function runs on each node of the resulting shadow graph.