Skip to content

Providers and Aspects in Modern Bazel

Understanding Providers

What Are Providers?

Providers are the mechanism for passing information between rules in Bazel. They enable structured data sharing and form the backbone of Bazel's dependency system.

Basic Provider Structure

python
# Define a provider
FooInfo = provider(
    fields = ["sources", "includes", "deps_info"],
    doc = "Information about Foo compilation",
)

# Rule implementation using provider
def _foo_library_impl(ctx):
    # Collect information
    sources = ctx.files.srcs
    includes = ctx.attr.includes
    
    # Create provider instance
    return [
        FooInfo(
            sources = sources,
            includes = includes,
            deps_info = collect_deps(ctx.attr.deps),
        ),
    ]

Common Built-in Providers

1. DefaultInfo

python
def _custom_binary_impl(ctx):
    output = ctx.actions.declare_file(ctx.label.name)
    
    return [DefaultInfo(
        files = depset([output]),           # Output files
        runfiles = ctx.runfiles(files = [output]),  # Runtime files
        executable = output,                # For binary targets
    )]

2. OutputGroupInfo

python
def _custom_library_impl(ctx):
    # Different output groups for different purposes
    return [OutputGroupInfo(
        compilation = depset([compiled_lib]),
        debug_info = depset([debug_symbols]),
        documentation = depset([generated_docs]),
    )]

3. CcInfo

python
def _custom_cc_impl(ctx):
    return [CcInfo(
        compilation_context = cc_common.create_compilation_context(
            headers = depset(ctx.files.hdrs),
            includes = depset(ctx.attr.includes),
        ),
        linking_context = cc_common.create_linking_context(...),
    )]

Provider Composition

1. Collecting from Dependencies

python
def _collect_providers(ctx):
    # Gather providers from deps
    dep_infos = [dep[FooInfo] for dep in ctx.attr.deps]
    
    # Combine information
    all_sources = depset(
        direct = ctx.files.srcs,
        transitive = [info.sources for info in dep_infos],
    )
    
    return FooInfo(
        sources = all_sources,
        includes = ctx.attr.includes,
        deps_info = dep_infos,
    )

2. Provider Chains

python
def _library_impl(ctx):
    # Get providers from dependencies
    cc_info = ctx.attr.dep[CcInfo]
    foo_info = ctx.attr.dep[FooInfo]
    
    # Create new compilation context
    new_cc_info = cc_common.merge_cc_infos(
        cc_infos = [cc_info, process_foo_info(foo_info)]
    )
    
    return [new_cc_info]

Understanding Aspects

What Are Aspects?

Aspects are a way to extend rules with additional behavior and outputs. They can traverse the dependency graph and collect or generate information.

Basic Aspect Structure

python
# Define an aspect
collect_sources = aspect(
    implementation = _collect_sources_impl,
    attr_aspects = ["deps"],
    attrs = {
        "_tool": attr.label(
            default = "//tools:collector",
            executable = True,
            cfg = "exec",
        ),
    },
)

def _collect_sources_impl(target, ctx):
    # Collect sources from this target
    srcs = target.files.to_list()
    
    # Collect from dependencies
    dep_sources = [dep[OutputGroupInfo].sources 
                  for dep in ctx.rule.attr.deps]
    
    return [OutputGroupInfo(
        sources = depset(srcs, transitive = dep_sources)
    )]

Common Aspect Use Cases

1. Code Generation

python
proto_gen = aspect(
    implementation = _proto_gen_impl,
    attr_aspects = ["deps"],
    attrs = {
        "_protoc": attr.label(
            default = "@com_google_protobuf//:protoc",
            executable = True,
            cfg = "exec",
        ),
    },
)

def _proto_gen_impl(target, ctx):
    # Generate code for each proto file
    outputs = []
    for src in target.files.to_list():
        output = ctx.actions.declare_file(
            src.basename.replace(".proto", ".pb.go")
        )
        outputs.append(output)
        ctx.actions.run(
            outputs = [output],
            inputs = [src],
            executable = ctx.executable._protoc,
            arguments = ["--go_out=" + output.path, src.path],
        )
    
    return [OutputGroupInfo(
        generated = depset(outputs)
    )]

2. Documentation Generation

python
doc_gen = aspect(
    implementation = _doc_gen_impl,
    attr_aspects = ["deps"],
    attrs = {
        "_doc_tool": attr.label(
            default = "//tools:doc_generator",
            executable = True,
            cfg = "exec",
        ),
    },
)

def _doc_gen_impl(target, ctx):
    # Generate documentation
    doc_file = ctx.actions.declare_file(
        target.label.name + ".md"
    )
    
    ctx.actions.run(
        outputs = [doc_file],
        inputs = target.files.to_list(),
        executable = ctx.executable._doc_tool,
        arguments = ["-o", doc_file.path] + 
                   [f.path for f in target.files.to_list()],
    )
    
    return [OutputGroupInfo(
        docs = depset([doc_file])
    )]

Best Practices

1. Provider Design

python
# Good: Clear, focused provider
CompileInfo = provider(
    fields = ["objects", "includes", "flags"],
    doc = "Compilation artifacts and settings",
)

# Bad: Too broad/vague
BuildInfo = provider(
    fields = ["stuff", "more_stuff"],
)

2. Aspect Performance

python
# Good: Minimal traversal
collect_headers = aspect(
    implementation = _collect_headers_impl,
    attr_aspects = ["deps"],  # Only traverse deps
)

# Bad: Excessive traversal
collect_all = aspect(
    implementation = _collect_all_impl,
    attr_aspects = ["*"],  # Traverses everything
)

3. Provider Propagation

python
def _merge_providers(ctx):
    # Good: Explicit provider handling
    dep_infos = [dep[MyInfo] for dep in ctx.attr.deps
                if MyInfo in dep]
    
    # Bad: Assuming provider exists
    dep_infos = [dep[MyInfo] for dep in ctx.attr.deps]

Key Takeaways

  1. Provider Usage

    • Use providers for structured data sharing
    • Design providers with clear purpose
    • Handle missing providers gracefully
  2. Aspect Design

    • Use aspects for cross-cutting concerns
    • Minimize dependency traversal
    • Cache results when possible
  3. Best Practices

    • Keep providers focused
    • Design for performance
    • Handle errors gracefully

Next Steps

  • Learn about Rules and Evaluation to understand how providers and aspects fit into rule implementation
  • Explore Dependencies and Actions to see how providers help share information between targets
  • Study Build Rules to see providers and aspects in action
  • Read about Toolchains to understand how providers help with tool configuration

Released under the MIT License.