Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rules & providers

Rules in zut are definitions over primitives, not engine built-ins. A rule says how to turn attributes into actions (commands that produce files) and what providers (typed results) it hands to the targets that depend on it. rust_binary and friends are written this way in the prelude — and you can write your own the same way.

Anatomy of a rule

def _my_tool_impl(ctx):
    # 1. Declare outputs.
    out = ctx.actions.declare_file(ctx.attr.out)

    # 2. Register the action that produces them.
    ctx.actions.run(
        executable = "/bin/sh",
        arguments = ["-c", "my-tool " + ctx.attr.input + " > " + out.path],
        inputs = [ctx.attr.input],
        outputs = [out],
        env = {"PATH": "/usr/bin:/bin"},
    )

    # 3. Return providers for downstream targets.
    return DefaultInfo(files = [out])

my_tool = rule(
    implementation = _my_tool_impl,
    attrs = {
        "input": attrs.string(),
        "out": attrs.string(),
    },
)

A target then instantiates it:

my_tool(name = "thing", input = "data.in", out = "data.out")

ctx — the rule context

Inside an implementation function, ctx exposes:

  • ctx.attr.<name> — the target’s attribute values.
  • ctx.actions.declare_file(name) — declare an output; returns a file value with a .path.
  • ctx.actions.run(executable, arguments, inputs, outputs, env) — register an action. inputs may be files, declared outputs of dependencies, or grafted Trees; outputs are the declared files it produces.
  • ctx.toolchain.rust / ctx.toolchain.zig — the discovered host toolchain (compiler path + environment) for that language.

Actions

An action is the cacheable unit: a command, its input file set, its declared outputs, and its environment. zut hashes all of that into the action key. If the key is already in the cache (locally or remote), the action doesn’t run — its outputs are served. Inputs not declared here are unavailable at run time thanks to the sandbox, which is what makes the cache key sound.

Providers

Providers are the typed values a rule returns for its dependents to consume:

  • DefaultInfo(files = [...]) — the conventional “these are my output files” provider (what zut build materializes).
  • struct(field = value, ...) — an ad-hoc provider. The prelude’s rust_library, for example, returns struct(files=..., crate_name=..., rlib=..., rlibs=..., dylibs=...) so a downstream rust_binary can wire --extern and the transitive rlib closure.
  • provider(...) — define a named provider type for stronger contracts.

A dependent reads a provider’s fields off the dependency value it receives via its deps-style attributes — that’s how rust_binary discovers the .rlib of each rust_library it links.

Next: the built-in prelude.