gazelle_fold 0.1.0Latest published 1d ago
MODULE.bazel
bazel_dep(name = "gazelle_fold", version = "0.1.0")
README

gazelle_fold

gazelle_fold is a small Gazelle extension for folding over a BUILD tree from leaf packages back toward the root. As it walks upward, it can build parent targets from child exports, update local targets, and enforce project-local BUILD policies beside the packages they govern.

That shape is useful in large monorepos, especially when humans and coding agents are both writing BUILD files: teams can encode local conventions once and keep generated edits aligned with the repo's own build patterns.

The basic setup is short:

# gazelle:fold import("std:folds/file_rollup.star")
# gazelle:fold import("std:rewrites/required_tags.star")
# gazelle:fold import("std:policies/forbidden_deps.star")
# gazelle:fold use("file_rollup", scope = "...", include = ["*.rs", "BUILD.bazel"], local_name = "all_sources", recursive_name = "all_sources_recursive")
# gazelle:fold use("required_tags", scope = "...", kinds = ["rust_library"], tags = ["team:runtime"])
# gazelle:fold use("forbidden_deps", scope = "app/...", kinds = ["rust_library"], deny = ["//legacy/..."])

Those imports load stock definitions from the bundled std mount. use(...) activates them for a relative package scope and supplies their parameters. Nearer activations layer over farther ones, so a child package can override only the parameter it cares about:

# inherited `kinds` stays in force; only `tags` changes here
# gazelle:fold use("required_tags", scope = ".", tags = ["team:child"])

The bundled definitions show the three things the extension can do:

file_rollup      fold child exports into ancestor targets
required_tags    modify local targets in place
forbidden_deps   reject local policy violations

file_rollup is the canonical fold example: leaf packages export local source groups, parents combine child exports with their own local files, and a full walk carries the recursive rollup all the way to the root.

For a tree like:

app/
├── BUILD.bazel
├── root.rs
└── child/
    ├── BUILD.bazel
    ├── lib.rs
    └── grandchild/
        ├── BUILD.bazel
        └── lib.rs

With one directive at the root:

# gazelle:fold import("std:folds/file_rollup.star")
# gazelle:fold use("file_rollup", scope = "...", include = ["*.rs", "BUILD.bazel"], local_name = "all_sources", recursive_name = "all_sources_recursive")

the leaf package exports its local files:

# child/grandchild/BUILD.bazel
filegroup(
    name = "all_sources",
    srcs = [
        "BUILD.bazel",
        "lib.rs",
    ],
)

filegroup(
    name = "all_sources_recursive",
    srcs = [":all_sources"],
    visibility = ["//visibility:public"],
)

its parent adds the child export to its own local files:

# child/BUILD.bazel
filegroup(
    name = "all_sources_recursive",
    srcs = [
        ":all_sources",
        "//child/grandchild:all_sources_recursive",
    ],
    visibility = ["//visibility:public"],
)

and the root receives the whole subtree:

# BUILD.bazel
filegroup(
    name = "all_sources_recursive",
    srcs = [
        ":all_sources",
        "//child:all_sources_recursive",
    ],
    visibility = ["//visibility:public"],
)

Each package contributes only its local files and the exports from its direct children. The recursive target emerges from the fold instead of one giant repo-wide declaration.

Install

After gazelle_fold is published to the Bazel Central Registry, add it beside Gazelle in your root module:

bazel_dep(name = "gazelle", version = "0.50.0")
bazel_dep(name = "gazelle_fold", version = "0.1.0")

Add the extension

load("@gazelle//:def.bzl", "gazelle_binary")

gazelle_binary(
    name = "gazelle_fold",
    languages = ["@gazelle_fold//language/fold"],
    version = 2,
)

Run normal Gazelle apply/check flows:

bazelisk run //:gazelle
bazelisk run //:gazelle_ci

The repo pins Bazel through .bazelversion, so use bazelisk for local builds and tests as well.

For a tiny repo that consumes gazelle_fold as a dependency instead of as the root module, see examples/bzlmod. CI runs that module separately so the public integration path stays honest.

Module paths

Modules resolve through a small mount table:

std:<path>    bundled gazelle_fold standard library
root:<path>   file path anchored at the repository root
<path>        relative to the importing .star file, or to the BUILD package for import(...)

Today the runtime exposes the std and root mounts. The mount table is the internal abstraction, so other named mounts can be added later without changing the module language.

Reuse or customize

Stock definitions are importable entrypoints for common fold steps and rule hooks:

std:folds/file_rollup.star
std:rewrites/required_tags.star
std:policies/forbidden_deps.star

If you want repo-specific names or defaults, load the bundled helper library from your own .star entrypoint:

load("std:lib/file_rollup.star", "file_rollup_fold")
load("std:lib/forbidden_deps.star", "forbidden_deps_policy")
load("std:lib/required_tags.star", "required_tags_rewrite")

required_tags_rewrite(
    name = "rust_required_tags",
    kinds = ["rust_library", "rust_binary", "rust_test"],
)

file_rollup_fold(
    name = "rust_files",
    local_name = "all_sources",
    recursive_name = "all_sources_recursive",
    include = ["*.rs", "BUILD.bazel"],
)

forbidden_deps_policy(
    name = "rust_forbidden_deps",
    kinds = ["rust_library", "rust_binary", "rust_test"],
    deny = ["//legacy/..."],
)

Then import the repo-owned entrypoint:

# gazelle:fold import("root:build/gazelle_fold/rust.star")
# gazelle:fold use("rust_required_tags", scope = "...", tags = ["team:runtime"])
# gazelle:fold use("rust_files", scope = "...")

Supported scopes are ".", "...", "bar", and "bar/..."; they are relative to the package containing the directive.

To skip one rule action for exactly one following target:

# gazelle:fold skip("required_tags", reason = "vendored target")
rust_library(
    name = "vendored",
)

Starlark host API

The built-in library is ordinary Starlark layered over a deliberately small host:

gazelle_fold.param(type, required = False, default = None)
gazelle_fold.fold(name, params = {}, apply = fn)
gazelle_fold.rewrite(name, params = {}, apply = fn)
gazelle_fold.policy(name, params = {}, apply = fn)

Fold callbacks receive (ctx). Rewrites and policies receive (ctx, rule). Folds can read child exports, emit local targets, and pass state upward to ancestors. Rewrites change local rules. Policies report violations and can fail the run.

rule.kind
rule.name
rule.matches_kind(patterns)
rule.list_attr(name)
rule.ensure_list_attr_contains(name, values)
rule.deps_matching(patterns)

ctx.rel
ctx.name
ctx.params
ctx.matching_files(include)
ctx.ensure_filegroup(name, srcs, public = False)
ctx.remove_filegroup(name)
ctx.child_exports(name)
ctx.export(name, label)
ctx.report_violation(message)  # policies only

params is a real definition contract: unknown names, missing required params, and wrong types are rejected instead of silently falling through.

Current limits

  • Directive comments use a tiny one-command language, not general Starlark.
  • The built-in mount table exposes std and root; user-configured mounts are not surfaced yet.
  • The host currently exposes safe string-list edits and filegroup generation, not arbitrary BUILD AST mutation.
  • required_tags only rewrites literal string-list attributes; complex select(...)-style expressions are skipped rather than guessed at.
  • forbidden_deps reports direct labels from literal deps lists and fails the Gazelle run before files are written. Patterns are absolute Bazel labels such as //legacy:old or subtree selectors such as //legacy/...; non-literal deps expressions fail closed because they cannot be validated safely.
  • Recursive rollups are deliberately conservative: if a selective Gazelle run has not covered every relevant child package, ancestor recursive outputs are left untouched instead of being rewritten from partial knowledge.

See docs/starlark-api-redesign.md for the fold design rationale, examples/ for copyable examples, and tests/apply_mvp for an end-to-end fixture.

About

Target folding for Bazel query-like aggregation in your BUILD graph.

@andyscott/gazelle_fold@andyscott
Homepage
1star
Wednesday, May 6, 2026 (1 day ago)

Languages

Go92.7%
Starlark7%
Rust0.2%
Shell0.2%

Maintainers

@andyscott

Versions