4.2.7 Custom Provider Declaration
Custom providers are the contract language between rules. A producing rule returns a provider instance with semantic data, and a consuming rule reads that provider from its direct dependencies during analysis.1 4.2.5 DefaultInfo & Runfiles answers "what files and runfiles does this target expose by default?". A custom provider answers the domain-specific question your ruleset needs, such as "what headers should downstream rules compile with?" or "what archives should a linker see?"2
The provider instance travels on the dependency target, not through output files.
Declare The Data Shape
Declare a provider once in a .bzl file and bind it to a global symbol. The result of provider() must be stored in a global value if rule or aspect implementations need to use it.3
LibraryInfo = provider(
doc = "Compilation data needed by downstream library consumers.",
fields = {
"headers": "depset of header Files from this target and deps.",
"archives": "depset of compiled archive Files for linking.",
},
)
The provider symbol has two jobs. First, it is a constructor for provider instances:
LibraryInfo(
headers = depset(ctx.files.hdrs),
archives = depset([archive]),
)
Second, the same symbol is the key used to retrieve that provider from a dependency target: dep[LibraryInfo].4 That is why custom providers are normally exported from a public .bzl entry point when third-party rules are expected to interoperate with them.
doc describes the provider for documentation generators, and fields restricts the allowed field names. The fields value can be a list of names or a dictionary from field name to field documentation. The dictionary form is more useful for public rulesets because it documents each piece of the contract next to the declaration.5 Field declarations do not make fields mandatory by themselves. They define which names may appear. If you need invariants, use a constructor helper or init.
Return Provider Instances
A rule returns providers from its implementation function. In a library-like rule, the implementation usually returns DefaultInfo for the files Bazel should build by default and a custom provider for data that consumers need during their own analysis.6
def _library_impl(ctx):
archive = ctx.actions.declare_file(ctx.label.name + ".a")
# Stand-in for the compiler action that would produce the archive.
ctx.actions.write(
output = archive,
content = "",
)
transitive_headers = [
dep[LibraryInfo].headers
for dep in ctx.attr.deps
]
headers = depset(
direct = ctx.files.hdrs,
transitive = transitive_headers,
)
return [
DefaultInfo(files = depset([archive])),
LibraryInfo(
headers = headers,
archives = depset(
direct = [archive],
transitive = [
dep[LibraryInfo].archives
for dep in ctx.attr.deps
],
),
),
]
This is where the earlier depset rule becomes concrete: provider fields that describe transitive data should stay as depsets, not lists copied at every node.7 A GoLibraryInfo-style provider uses this shape for a custom Go library rule: each library publishes its own archive and a depset of dependency metadata, then a binary rule flattens that data only when generating the linker configuration.8 The collection mechanics are covered in 4.1.5 depset vs list. The provider declaration is the API boundary that makes those mechanics visible to other rules.
Keep provider fields small and semantic. A provider should not be a dumping ground for every intermediate value the implementation happened to compute. Publish the facts consumers need to analyze their own actions: files, import paths, compile contexts, tool options, or transitive metadata. Internal scratch values should stay local to the implementation function.
Require Providers On Dependencies
Consumers should declare what provider contract they require on dependency attributes. For attr.label and attr.label_list, the providers parameter tells Bazel that every target in the attribute must return the required provider.9
consumer_rule = rule(
implementation = _consumer_impl,
attrs = {
"deps": attr.label_list(
providers = [LibraryInfo],
doc = "Libraries that provide compilation data.",
),
},
)
This is an analysis-time API boundary, not a runtime check. Bazel verifies the declared provider requirement while it constructs the configured target graph, before _consumer_impl can read ctx.attr.deps or any consuming action can run. A provider constraint therefore keeps an incompatible target out of the consumer's implementation instead of turning a contract mismatch into a later indexing error.
For example, this BUILD call deliberately passes a file-only target where the consumer requires LibraryInfo:
filegroup(
name = "plain_files",
srcs = ["note.txt"],
)
consumer_rule(
name = "broken_consumer",
deps = [":plain_files"],
)
Analysis stops at the deps attribute. The first diagnostic names the consuming target and attribute, then says that //pkg:plain_files is missing mandatory provider LibraryInfo. Start there: replace the dependency with a target that returns the same LibraryInfo symbol, or change the public contract if file-only inputs are actually valid. Do not add a defensive hasattr() inside _consumer_impl. That code is not the boundary Bazel is reporting, and diagnostic wording and source locations vary by Bazel version.9
Once the constraint has passed, label attributes are resolved to Target objects during analysis, so the implementation can read the provider directly:
def _consumer_impl(ctx):
archives = depset(transitive = [
dep[LibraryInfo].archives
for dep in ctx.attr.deps
])
The providers parameter has list-of-lists semantics. providers = [A, B] means each dependency must return both A and B. providers = [[A], [B]] means each dependency may return either A or B.10 For new code, prefer one clear provider contract when you can. The either/or form is useful for compatibility windows, but it makes the consumer responsible for handling multiple shapes.
Use init For Invariants
The plain provider constructor checks field names, but it does not know your ruleset's invariants. If every provider instance must normalize data, reject an empty field, or route construction through a public factory, pass an init callback to provider().11
def _library_info_init(*, headers, archives):
if not archives:
fail("LibraryInfo requires at least one archive")
return {
"headers": headers,
"archives": archives,
}
LibraryInfo, _new_library_info = provider(
doc = "Compilation data needed by downstream library consumers.",
fields = {
"headers": "depset of header Files from this target and deps.",
"archives": "depset of compiled archive Files for linking.",
},
init = _library_info_init,
)
When init is present, calling LibraryInfo(...) forwards the arguments to the callback. The callback must return a dictionary whose keys are provider field names. Bazel then creates the provider instance from that dictionary.12 In this form, provider() returns two values: the public provider symbol and a raw constructor that bypasses init.13 The official docs recommend binding that raw constructor to an underscore-prefixed name so ordinary user code cannot load it directly.14
Use init for provider-wide invariants, not for target-specific validation that belongs in the rule implementation. If the rule needs to validate user attributes, fail near the start of _impl. If every instance of LibraryInfo must have normalized headers no matter which rule created it, put that invariant in init.
Keep The Boundary Modern
Old Starlark rules returned struct values and exposed provider-like data as fields on Target objects. Bazel still documents a migration path for legacy providers, but the legacy style is strongly discouraged and should not be used in new code.15 Modern declared providers avoid name clashes and hide data behind the provider symbol: code can read dep[LibraryInfo] only if it loaded the same provider symbol.16
That symbol-based access is the reason provider design becomes an interface design question. Once another rule depends on LibraryInfo.headers, that field name and meaning are part of the public API. Renaming it or changing its type is more like changing a function signature than refactoring a local variable. The next chapter, 4.2.8 Provider-as-Interface Pattern, focuses on that composability pattern. Here the practical rule is simpler: declare the provider deliberately, document its fields, require it on dependency attributes, and return only data you are willing to support.
The mini-ruleset's complete
GlyphInfo declaration
documents each field, while
glyph_library
constructs and returns the provider from direct and transitive data.
Custom providers are typed analysis-phase messages. Declare the provider symbol globally, return provider instances from producing rules, require the provider on dependency attributes, and read it with dep[ProviderName].
Use fields to document and restrict the data shape. Use init only when provider instances need shared construction rules that should hold no matter which rule creates them.
Check your understanding · 4 questions
1.What is the dual role of a custom provider symbol such as LibraryInfo?
Select one answer
2.Which statements describe good custom provider design?
Select all that apply
3.True or false: provider requirements and initialization.
Choose True or False for each sentence
providers = [A, B] means each dependency must provide both A and B.providers = [[A], [B]] means each dependency may provide either A or B.fields argument makes every listed provider field mandatory.init, provider() returns the provider symbol and a raw constructor.4.A consumer_rule has deps = attr.label_list(providers = [LibraryInfo]), but its BUILD call passes a filegroup that does not return LibraryInfo. What should you inspect first?
Select one answer
Footnotes
-
Rules — providers as pieces of information exposed by a rule to dependent rules. ↩
-
Rules — examples of rules returning both
DefaultInfoand language-specific provider data such asCcInfo. ↩ -
.bzl files —
provider()defines a provider symbol that must be stored in a global value. ↩ -
Provider — provider values are both constructors and keys for target indexing. ↩
-
.bzl files —
doc,fields, and allowed field forms forprovider(). ↩ -
Rules — implementation functions return a list of provider objects including
DefaultInfoand custom providers. ↩ -
Writing Bazel rules: library rule, depsets, providers — providers and depsets used together to pass dependency information between rules. ↩
-
Writing Bazel rules: library rule, depsets, providers —
GoLibraryInfocarries direct library metadata and a depset of dependency metadata. ↩ -
Rules — dependency attributes should specify which providers their dependencies must provide. ↩1 ↩2
-
attr —
providersparameter list-of-lists semantics for label and label-list attributes. ↩ -
Rules — custom initialization of providers for preprocessing, validation, and cleaner construction APIs. ↩
-
Rules —
initcallback invocation and dictionary return contract. ↩ -
.bzl files —
provider(init = ...)returns the provider symbol and a raw constructor. ↩ -
Rules — raw constructors are typically bound to underscore-prefixed private names. ↩
-
Rules — legacy
structproviders are strongly discouraged and supported mainly for migration. ↩ -
Rules — modern providers avoid name clashes and support data hiding through symbol-based access. ↩