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.
The global symbol is created with repository_rule(...). Callers instantiate repositories from that API.
This loading-time filesystem API is not the analysis ctx from normal rules.
target loaded from BUILD.bazel
files exposed to dependents
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.
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
repository_ctx.ctx and can register actions.Footnotes
-
Repository Rules — repository definition, attributes, implementation function, and loading-phase fetching model. ↩1 ↩2
-
.bzl files —
repository_rule()signature and parameter rules forimplementation,attrs, implicitname, private attrs,local,configure, and related flags. ↩1 ↩2 ↩3 -
Writing Bazel rules: repository rules —
hello_repo,use_repo_rule(),go_download,download_and_extract(), private template attr, and generated BUILD file examples. ↩1 ↩2 ↩3 -
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+wgetanti-pattern. ↩1 ↩2 -
repository_ctx — API reference for
attr,download(),download_and_extract(),execute(),file(),path(),report_progress(), andtemplate(). ↩1 ↩2 ↩3 -
exec_result — return fields from
repository_ctx.execute():return_code,stdout, andstderr. ↩ -
Bazel.build - Binary package management — custom
cipd_packagerepository rule usingfile(),path(),execute(), andexec_result.stderr. ↩ -
path — path object operations available to repository rules and module extensions. ↩
-
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. ↩
-
Module extensions — extensions collect module tags and create repositories by calling repository rules. ↩