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.inputsmay be files, declared outputs of dependencies, or grafted Trees;outputsare 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 (whatzut buildmaterializes).struct(field = value, ...)— an ad-hoc provider. The prelude’srust_library, for example, returnsstruct(files=..., crate_name=..., rlib=..., rlibs=..., dylibs=...)so a downstreamrust_binarycan wire--externand 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.