4.4.1 Rule Public API Design
A production rule is a public API in the same practical sense as a library function: downstream BUILD files call it, other rules may consume its providers, humans expect its default outputs to stay predictable, and tools may request its named output groups. The implementation can change, but the surface that users write and consumers read needs deliberate design.1
The Surface Is Larger Than attrs
The first visible part of a rule API is the attribute set declared in rule(attrs = ...). Attribute schemas decide what users may write, which files or targets are accepted, which providers dependencies must expose, and which values are documented for Stardoc or similar tooling.2 That is the rule-function contract from 4.2.1 Rule Function, but the production question is stricter: "will this still be a good call site after hundreds of targets depend on it?"
Use ordinary names when the meaning is ordinary. The official style guide recommends srcs for source files, deps for compilation dependencies, data for runtime files, and runtime_deps for runtime-only dependencies.3 A custom name is useful only when the role is genuinely custom. The goal is not cleverness. It is a BUILD file another engineer and a tool can scan quickly.
Private attributes are part of the same design. If the rule always uses a compiler, formatter, or helper binary, hide that implementation dependency behind an underscore-prefixed attr such as _compiler instead of making every user wire it by hand.4 When that private attr is executable, build it in the execution configuration with cfg = "exec" so the tool runs where the action runs, not necessarily where the final artifact runs.5 If the tool varies by platform or user setup, the stable public surface may need to move from a private attr to a toolchain in 4.6.2 Defining, Registering & Accessing Toolchains.
my_library = rule(
implementation = _my_library_impl,
attrs = {
"srcs": attr.label_list(
allow_files = [".my"],
doc = "Source files compiled by this library.",
),
"deps": attr.label_list(
providers = [MyLibraryInfo],
doc = "Libraries that expose MyLibraryInfo.",
),
"_compiler": attr.label(
default = Label("//tools/my:compiler"),
executable = True,
cfg = "exec",
),
},
doc = "Compiles MyLang sources into a reusable library.",
)
This is not just input validation. The providers = [MyLibraryInfo] line tells users which interface a dependency must support before the implementation reads dep[MyLibraryInfo] during analysis.6
Providers Are the Consumer API
Custom providers are the rule-to-rule interface. A provider carries semantic information that another rule needs during analysis: headers, archives, import paths, descriptors, source maps, or tool-specific metadata.7 That is why 4.2.7 Custom Provider Declaration treats provider fields as contract fields, not temporary structs.
Design provider fields around capabilities, not around the current implementation. A field named archives says "consumers may link these files." A field named temp_outputs says "I leaked my implementation." A GoLibraryInfo-style provider exposes a direct library metadata struct plus a depset of transitive dependency metadata. The binary rule consumes that provider to generate linker input without depending on the producing rule's internal code layout.8
If other rulesets should implement compatible producers, export the provider symbol from the public .bzl entry point. Bazel provider access is symbol-based: the provider symbol is both the constructor and the key used as dep[ProviderName].9 Keeping a provider private is therefore an intentional choice: only code that can load that symbol can participate in the interface.
Default Outputs, Output Groups, and Internals
A rule often produces more files than a normal user wants from bazel build //pkg:target. DefaultInfo.files is the ordinary build result: the depset of files Bazel builds by default when the target is requested.10 For executable and test rules, DefaultInfo(executable = ...) also names the file Bazel launches, and DefaultInfo(runfiles = ...) carries runtime files.11
Output groups are a different public surface. They publish named depsets of files that users and tools can request selectively with --output_groups, or through filegroup(output_group = "...") in a BUILD file.12 Use them for categories like debug_files, lint_reports, source_maps, or ide_info when those files should be buildable on demand but not part of the target's normal output set.
return [
DefaultInfo(files = depset([archive])),
MyLibraryInfo(
headers = headers,
archives = depset([archive]),
),
OutputGroupInfo(
debug_files = depset([debug_manifest]),
),
]
Do not use output groups as a substitute for provider contracts. OutputGroupInfo should not convey file categories to consuming rule actions. If another rule needs semantic data, define a rule-specific provider instead.13 The practical split is:
| Surface | Use it for |
|---|---|
DefaultInfo.files | Files a normal build of the target should produce. |
| Custom providers | Semantic data other rules need during analysis. |
OutputGroupInfo | Named artifact sets humans, CI, IDEs, or aspects request selectively. |
Internal File values | Intermediates used only by this rule's actions. |
Documentation Is Part of the API
Public Starlark APIs should be documented where the API is declared: put doc on rules, aspects, attributes, providers, and provider fields.14 Reusable rulesets should also expose a clear entry point such as defs.bzl, keep a top-level README that tells users what API to expect, and include examples that show supported usage.15
That layout matters because users copy what they can find. If //internal:rules.bzl is easier to load than //:defs.bzl, someone will depend on it. A stable public entry point lets you reorganize internals without forcing users to change load statements: load internal definitions into a public def.bzl and re-export the symbols users should load.16 The ruleset-level version of this concern continues in 4.11.1 Ruleset Layout & Public Entry Points.
Generated API docs are the mechanical backstop. Stardoc can extract documentation from Starlark declarations, and modern BCR documentation workflows can publish generated Starlark docs with the released module so users discover API docs next to the artifact they depend on.17 The detailed documentation item is 4.5.4 Stardoc — API Documentation, while example workspaces as executable documentation are covered in 4.5.3 Example Workspaces as Contract Tests.
Plan For Evolution Before Users Arrive
Once users depend on an attr, a provider field, an output name, or an output group string, changing it is a migration. Additive changes are easiest: add a new optional attr with a safe default, add a new provider field while keeping the old one, or add a new output group without removing the old group. Breaking changes need a compatibility plan, not just a release note.
Bazel's rule compatibility guidance frames the goal as a manageable migration: users should not be forced to upgrade a ruleset major version and a Bazel major version at the same time.18 For a production rule API, apply that same idea locally. If an old provider field must go away, keep both fields for a window, document the replacement, test both paths, and remove the old field in a release that is clearly allowed to break consumers. The broader ruleset policy belongs in 4.11.6 Rule Compatibility & Bazel Version Policy.
For a concrete facade, inspect the mini-ruleset's defs.bzl, which deliberately re-exports only its supported providers, rules, and macros.
For a production-shaped contrast, bazel_rules_hdl re-exports its public
synthesis symbols from
synthesis/defs.bzl,
while
place_and_route/build_defs.bzl
consumes a typed synthesis provider and coordinates private implementation
steps.19 Its public place_and_route target kind accepts SynthesisInfo and
orchestrates the private OpenROAD stages without exposing each one as a target
kind. Consumers still load synthesis, place-and-route, and Verilog rules from
their respective public files. The stages and attributes remain specific to
this ruleset.
A production rule API has four public faces: attrs users write, providers other rules consume, outputs users and tools request, and documentation/examples that teach the supported path.
Keep implementation tools private, provider fields semantic, default outputs small, output groups named and stable, and public .bzl entry points obvious. Most rule maintenance pain comes from treating one of those surfaces as internal after users have already built on it.
Check your understanding · 3 questions
1.Match each rule API surface to the role it should play.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
2.Which choices make a production rule API easier to evolve safely?
Select all that apply
3.True or false: designing attrs, providers, and outputs for reusable rules.
Choose True or False for each sentence
Footnotes
-
Rules - rules define attributes, actions, outputs, providers, and implementation behavior. ↩
-
Rules - attribute schemas, common attributes, provider requirements, and documentation on rule attributes. ↩
-
.bzl style guide - standard rule names and attribute conventions for
srcs,deps,data, andruntime_deps. ↩ -
Rules - private attributes and implicit dependencies for implementation tools. ↩
-
Rules - executable dependency attributes should choose
cfg = "exec"for build-time tools. ↩ -
Rules - dependency attributes can require providers that dependencies must return. ↩
-
Rules - providers expose information to rules that depend on the target. ↩
-
Writing Bazel rules: library rule, depsets, providers -
GoLibraryInfoprovider and depset-based transitive metadata example. ↩ -
Writing Bazel rules: library rule, depsets, providers - public
def.bzlexportsgo_libraryandGoLibraryInfofor compatible consumers. ↩ -
DefaultInfo -
filesfield describes default outputs built from the command line. ↩ -
DefaultInfo -
runfilesandexecutableconstructor parameters.FilesToRunProviderviafiles_to_run. ↩ -
Rules - output groups can be requested with
--output_groupsand defined withOutputGroupInfo. ↩ -
Rules - output groups should not replace rule-specific providers for consumer actions. ↩
-
.bzl style guide - document rules, aspects, attributes, providers, and provider fields with
doc. ↩ -
Deploying Rules - ruleset layout, README/API expectations,
defs.bzlentry point, tests, examples, and documentation. ↩ -
Writing Bazel rules: simple binary rule - public
def.bzlentry point re-exporting internal rule symbols. ↩ -
Bazel Starlark Docs on the Registry -
starlark_doc_extract,bzl_library,docs_url, and BCR rendering for published Starlark API docs. ↩ -
Rule Compatibility - manageable migration avoids forcing users to upgrade a ruleset major version and Bazel major version simultaneously. ↩
-
bazel_rules_hdl — open HDL and silicon-design rules —
synthesis/defs.bzldefines the public synthesis facade, whileplace_and_route/build_defs.bzldemonstrates a provider-linked rule coordinating private implementation steps. ↩