4.9.4 Repository Rule API

repository_rule() is the rule-author API for making an external repository appear in Bazel's world. A normal rule produces providers and actions for a target during analysis. A repository rule runs during loading, receives a repository_ctx, and fills a repository directory with files, directories, symlinks, downloaded archives, or generated BUILD files.1 That is why the previous item, 4.9.3 When to Write Custom Repository Rules, is about deciding whether you need a custom fetcher at all. This article is about the shape of that fetcher once the answer is yes.

Repository rule API separates schema from loading-time fetching
Callers pass declared attributes. During loading, repository_ctx fetches or writes files; then Bazel can load targets from the generated repository.
Schema
Callers see declared inputs
attrs plus implicit name
public url, sha256
implicit name
private _build_tpl

The global symbol is created with repository_rule(...). Callers instantiate repositories from that API.

repository_ctx
The implementation fetches during loading
fetch, write, inspect
read attrs rctx.attr.*
fetch bytes download
write files file/template
host edge execute/path
no actions no providers no toolchains

This loading-time filesystem API is not the analysis ctx from normal rules.

Repository
Bazel later loads packages from the tree
BUILD files plus labels
@archive/
BUILD.bazel
include/
lib.a
@archive//:lib

target loaded from BUILD.bazel

@archive//:headers

files exposed to dependents

Keep custom repo rules small. Attrs describe inputs. repository_ctx materializes files. The generated repository is the only thing analysis consumes.

The Smallest Useful Repository Rule

A repository rule has two main pieces: an implementation function and an attribute schema. The implementation function takes exactly one parameter, conventionally named repository_ctx or ctx. The schema describes the attributes a caller may pass when instantiating the repository rule.2

def _hello_repo_impl(ctx):
    ctx.file("hello.txt", ctx.attr.message)
    ctx.file("BUILD.bazel", 'exports_files(["hello.txt"])')

hello_repo = repository_rule(
    implementation = _hello_repo_impl,
    attrs = {
        "message": attr.string(mandatory = True),
    },
)

The caller does not call this from a BUILD file. In module mode, a root module can bind the rule with use_repo_rule() and then instantiate an external repository by passing a name and the declared attributes.3

hello_repo = use_repo_rule("//:repo.bzl", "hello_repo")

hello_repo(
    name = "hello",
    message = "Hello, world!",
)

After the repository is fetched, Bazel treats the generated directory like another repository: it has packages, BUILD files, and labels such as @hello//:hello.txt.4 The important mental model is that the implementation function must create enough of that filesystem tree for Bazel to load packages from it.

For a complete repository rule rather than a parallel sketch, inspect the mini-ruleset's glyph_module_repo. Its implementation validates attrs, writes a source file and BUILD.bazel, and exports the resulting repository_rule() symbol from one compact file.

Attributes Are The Repo Rule's Public API

The attrs dictionary plays the same design role as it does for normal rules: it declares the inputs the rule author wants callers to provide. Inside the implementation, those values are available under repository_ctx.attr.1

def _archive_repo_impl(ctx):
    ctx.download_and_extract(
        url = ctx.attr.url,
        sha256 = ctx.attr.sha256,
        strip_prefix = ctx.attr.strip_prefix,
    )
    ctx.file("BUILD.bazel", ctx.attr.build_file_content)

archive_repo = repository_rule(
    implementation = _archive_repo_impl,
    attrs = {
        "url": attr.string(mandatory = True),
        "sha256": attr.string(mandatory = True),
        "strip_prefix": attr.string(default = ""),
        "build_file_content": attr.string(mandatory = True),
    },
)

The name attribute is implicit, so you do not declare it in attrs.2 Be careful when reading names inside the implementation: repository_ctx.attr.name and repository_ctx.name are about Bazel's repository naming machinery, which becomes especially visible under Bzlmod. For generated labels and cross-repo references, keep the name story explicit and leave the deeper compatibility details to 4.9.5 Reproducibility Concerns.

Private attributes start with _ and must have defaults. They are useful for implementation files such as templates, patches, or helper binaries.2 A go_download repository rule, for example, uses a private _build_tpl label so the caller supplies platform facts and archive URLs, while the ruleset supplies the template used to generate the repository's BUILD file.3

repository_ctx Is Not ctx

The biggest source of confusion is that repository rule implementations look like rule implementations but run in a different phase with a different context object. repository_ctx can create files, download data, extract archives, execute host commands, resolve paths, read files, and report progress.5 It cannot register actions, return providers, request toolchains, or depend on outputs produced by ordinary targets, because those are analysis- and execution-phase concepts.

Use repository_ctx.file() when the generated file is small and the content is naturally assembled in Starlark. Use repository_ctx.template() when the generated file has a stable skeleton with a few substitutions. A go_download implementation, for example, downloads and extracts a Go distribution, then expands a BUILD template with OS and CPU-specific substitutions.3

def _go_download_impl(ctx):
    ctx.download_and_extract(
        ctx.attr.urls,
        sha256 = ctx.attr.sha256,
        strip_prefix = "go",
    )

    ctx.template(
        "BUILD.bazel",
        ctx.attr._build_tpl,
        substitutions = {
            "{goos}": ctx.attr.goos,
            "{goarch}": ctx.attr.goarch,
        },
    )

That pattern is the center of many custom repository rules: fetch or discover inputs, then write a Bazel-shaped repository around them.

Downloads Should Carry Integrity

For network artifacts, prefer repository_ctx.download() or repository_ctx.download_and_extract() over shelling out to curl or wget. These methods know about Bazel's downloader, accept mirror URLs, support checksums, and can populate or reuse the repository cache when a sha256 or integrity value is supplied.5 The private-data talk gives the practical reason: fetching in a genrule hides the network access from bazel fetch, bazel vendor, repository cache behavior, credential helpers, and downloader policy.4

4.9.2 Archive Downloads & Integrity develops the downloader and checksum contract. At the API level, the rule is simple: if your repository rule downloads bytes, model that operation through repository_ctx and expose enough attributes for the caller or ruleset to pin what was fetched.

Host Commands Are The Sharp Edge

repository_ctx.execute() runs a process on the host while the repository is being fetched. It returns an exec_result with return_code, stdout, and stderr, which lets the implementation fail with a useful message or parse command output.6 This is how real integrations sometimes bridge to ecosystem tools or installers. A CIPD example writes an .ensure file with repository_ctx.file(), runs the CIPD client with repository_ctx.execute(), and fails with stderr when the command returns non-zero.7

result = repository_ctx.execute([
    repository_ctx.path(repository_ctx.attr._cipd_client),
    "ensure",
    "-root",
    ".",
    "-ensure-file",
    ".ensure",
])
if result.return_code:
    fail(result.stderr)

Treat execute() as a boundary, not a shortcut. A host command brings in the host filesystem, host PATH, process environment, platform differences, timeouts, and tool availability. Some repository rules genuinely need that power, but each dependency should be visible in attributes, downloaded tools, watched files, or documented setup. The neighboring reproducibility articles cover the tracking details in 4.9.5 Reproducibility Concerns, 4.9.6 getenv() vs os.environ Pitfall, and 4.9.7 watch() / watch_tree() File Dependencies.

Paths And Existing Files

repository_ctx.path() converts a string, label, or path value into a path object. In a repository rule, relative strings resolve inside the repository being created. Label values resolve to source files, not generated artifacts.5 Path objects then expose operations such as basename, dirname, exists, is_dir, get_child(), readdir(), and realpath().8

This is useful for implementation inputs. A private template label can be passed to repository_ctx.template(). A helper binary label can be converted to a path and executed. A local toolchain generator may inspect a directory and copy files into the generated repository. The Testwell CTC++ integration shows the advanced version: download a tool suite, run installer commands, expand a wrapper template, inspect @local_config_cc, and copy toolchain files into a new generated repository.9

Those examples also show why path access should stay boring. If the rule reads host files, make the watched dependency story explicit in the article dedicated to 4.9.7 watch() / watch_tree() File Dependencies. If it bakes absolute host paths or command output into generated files, make the reproducibility trade-off explicit in 4.9.5 Reproducibility Concerns.

Where Module Extensions Fit

Module extensions do not replace repository rules. They usually orchestrate them. An extension reads tags from modules across the dependency graph, resolves what repositories should exist, then calls repository rules to create those repositories.10 That means a well-designed repository rule is often still the materialization unit underneath a Bzlmod extension.

The split is useful. Put per-repository filesystem work in a repository rule: download this archive, generate this BUILD file, expose this toolchain repository. Put graph-wide aggregation in a module extension: collect all Maven artifacts, all Go modules, or all requested SDK versions, then call repository rules with concrete attributes. 4.10.1 Module Extension Fundamentals develops that extension model, while the repository rule API remains the bridge from resolved dependency data to actual repository contents.

key takeaway

A repository rule is a loading-phase filesystem constructor. Its attrs describe the caller-visible inputs, its implementation receives repository_ctx, and its job is to create a repository directory that Bazel can load. Keep the rule small: fetch with download() or download_and_extract(), generate files with file() or template(), use execute() only when the host interaction is truly part of the integration, and push graph-wide Bzlmod logic into module extensions.

Check your understanding · 4 questions

1.How should a custom repository rule expose caller-controlled inputs?

Select one answer

2.Which details are specific to the repository rule API?

Select all that apply

3.When a repository rule is tempted to call repository_ctx.execute(), which cautions from the article apply?

Select all that apply

4.True or false: repository rules, module extensions, and normal rules use different implementation surfaces.

Choose True or False for each sentence

A repository rule implementation receives repository_ctx.
A normal rule implementation receives ctx and can register actions.
A module extension usually aggregates graph-wide tag data before calling repository rules.
A repository rule can depend on generated artifacts produced by ordinary build actions.
0 of 4 answered

Footnotes

  1. Repository Rules — repository definition, attributes, implementation function, and loading-phase fetching model. 1 2

  2. .bzl filesrepository_rule() signature and parameter rules for implementation, attrs, implicit name, private attrs, local, configure, and related flags. 1 2 3

  3. Writing Bazel rules: repository ruleshello_repo, use_repo_rule(), go_download, download_and_extract(), private template attr, and generated BUILD file examples. 1 2 3

  4. Fetching private data with Repo Rules and MODULE Extensions - Malte Poll, Modus Create — external label fetching, repository rule capabilities, downloader-aware fetching, and the genrule + wget anti-pattern. 1 2

  5. repository_ctx — API reference for attr, download(), download_and_extract(), execute(), file(), path(), report_progress(), and template(). 1 2 3

  6. exec_result — return fields from repository_ctx.execute(): return_code, stdout, and stderr.

  7. Bazel.build - Binary package management — custom cipd_package repository rule using file(), path(), execute(), and exec_result.stderr.

  8. path — path object operations available to repository rules and module extensions.

  9. Integrating Testwell CTC++ with Bazel — advanced repository rule that downloads, executes installer commands, expands a wrapper template, reads an existing toolchain repository, and generates a modified toolchain repository.

  10. Module extensions — extensions collect module tags and create repositories by calling repository rules.