Skip to content

Rules and Evaluation in Modern Bazel ​

Understanding Rules ​

Rules in Bazel define how to transform inputs into outputs. In modern Bazel, rules are more modular and composable thanks to Bzlmod.

Rule Structure ​

python
# Basic rule definition
def _my_rule_impl(ctx):
    # Rule implementation
    output = ctx.actions.declare_file(ctx.label.name + ".out")
    
    ctx.actions.run(
        outputs = [output],
        inputs = ctx.files.srcs,
        executable = ctx.executable.compiler,
        arguments = ["-o", output.path] + [f.path for f in ctx.files.srcs],
    )
    
    return [DefaultInfo(files = depset([output]))]

my_rule = rule(
    implementation = _my_rule_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "compiler": attr.label(
            executable = True,
            cfg = "exec",
        ),
    },
)

Rule Evaluation ​

1. Loading Phase ​

python
# BUILD.bazel
load("//my/rules:defs.bzl", "my_rule")

# Loading happens first
my_rule(
    name = "target",
    srcs = ["input.txt"],
    compiler = ":tool",
)

2. Analysis Phase ​

python
def _impl(ctx):
    # Analysis phase: Create action graph
    inputs = ctx.files.srcs
    output = ctx.actions.declare_file(...)
    
    # Declare the action
    ctx.actions.run(
        outputs = [output],
        inputs = inputs,
        executable = ctx.executable.tool,
    )
    
    # Return providers for other rules
    return [
        DefaultInfo(...),
        MyInfo(...),
    ]

3. Execution Phase ​

python
# Actions are executed based on the action graph
# Parallel execution when possible
cc_binary(
    name = "app",
    srcs = ["main.cc"],
    deps = [
        ":lib1",  # Built independently
        ":lib2",  # Built independently
    ],
)

Rule Types ​

1. Binary Rules ​

python
def _binary_impl(ctx):
    output = ctx.actions.declare_file(ctx.label.name)
    
    ctx.actions.run(
        outputs = [output],
        inputs = ctx.files.srcs,
        executable = ctx.executable.compiler,
        arguments = ["-o", output.path] + [f.path for f in ctx.files.srcs],
    )
    
    # Binary rules must return executable
    return [DefaultInfo(
        files = depset([output]),
        executable = output,
    )]

my_binary = rule(
    implementation = _binary_impl,
    executable = True,  # Marks as executable
    ...
)

2. Library Rules ​

python
def _library_impl(ctx):
    # Collect sources
    srcs = ctx.files.srcs
    
    # Collect dependencies
    deps = [dep[MyInfo].transitive_sources 
            for dep in ctx.attr.deps]
    
    # Create compilation action
    objects = []
    for src in srcs:
        obj = ctx.actions.declare_file(
            src.basename.replace(".c", ".o")
        )
        objects.append(obj)
        ctx.actions.run(
            outputs = [obj],
            inputs = [src],
            executable = ctx.executable.compiler,
        )
    
    # Return provider for dependents
    return [MyInfo(
        objects = objects,
        transitive_sources = depset(
            direct = srcs,
            transitive = deps,
        ),
    )]

my_library = rule(
    implementation = _library_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "deps": attr.label_list(providers = [MyInfo]),
    },
)

3. Test Rules ​

python
def _test_impl(ctx):
    # Create test executable
    test_bin = ctx.actions.declare_file(ctx.label.name)
    
    ctx.actions.run(
        outputs = [test_bin],
        inputs = ctx.files.srcs + ctx.files.data,
        executable = ctx.executable.compiler,
    )
    
    # Test rules must be executable
    return [DefaultInfo(
        files = depset([test_bin]),
        executable = test_bin,
        runfiles = ctx.runfiles(files = ctx.files.data),
    )]

my_test = rule(
    implementation = _test_impl,
    test = True,  # Marks as test rule
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "data": attr.label_list(allow_files = True),
    },
)

Rule Attributes ​

1. Basic Attributes ​

python
my_rule = rule(
    attrs = {
        "srcs": attr.label_list(
            allow_files = True,
            doc = "Source files",
        ),
        "deps": attr.label_list(
            providers = [MyInfo],
            doc = "Dependencies",
        ),
        "out": attr.output(
            doc = "Output file",
        ),
    },
)

2. Configuration Attributes ​

python
my_rule = rule(
    attrs = {
        "compiler": attr.label(
            executable = True,
            cfg = "exec",
            default = "//tools:default_compiler",
        ),
        "opts": attr.string_list(
            default = ["-O2"],
        ),
        "config": attr.label(
            providers = [ConfigInfo],
            default = "//config:default",
        ),
    },
)

3. Platform Attributes ​

python
my_rule = rule(
    attrs = {
        "target_compatible_with": attr.label_list(
            providers = [ConstraintValueInfo],
        ),
        "toolchains": attr.label_list(
            providers = [MyToolchainInfo],
        ),
    },
)

Best Practices ​

1. Rule Design ​

python
# Good: Clear, focused rule
compile_lib = rule(
    implementation = _compile_lib_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "deps": attr.label_list(providers = [LibInfo]),
    },
)

# Bad: Too many responsibilities
do_everything = rule(
    implementation = _do_everything_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "compile_opts": attr.string_list(),
        "link_opts": attr.string_list(),
        "test_args": attr.string_list(),
        # Too many unrelated attributes
    },
)

2. Provider Usage ​

python
# Good: Clear provider interface
LibInfo = provider(
    fields = ["objects", "headers", "includes"],
)

# Bad: Vague provider
Info = provider(
    fields = ["stuff"],
)

3. Action Creation ​

python
# Good: Explicit inputs and outputs
ctx.actions.run(
    outputs = [declared_output],
    inputs = ctx.files.srcs,
    executable = ctx.executable.tool,
    arguments = ["--out", declared_output.path],
)

# Bad: Hidden dependencies
ctx.actions.run_shell(
    command = "gcc $( find . -name '*.c' ) -o out",
)

Key Takeaways ​

  1. Rule Design

    • Keep rules focused
    • Use clear interfaces
    • Follow Bazel conventions
  2. Evaluation Process

    • Understand the phases
    • Handle dependencies correctly
    • Use providers effectively
  3. Best Practices

    • Write hermetic rules
    • Document attributes
    • Test rule behavior

Next Steps ​

Released under the MIT License.