4.4.6 Advanced Action Parameters
extraMost custom rules never need the advanced action parameters. Once a rule already declares clean inputs, tools, outputs, command-line data, and environment, unused_inputs_list and resource_set let you refine how Bazel treats that action at production scale: one tunes cache invalidation, the other tunes local scheduling.1 They are not substitutes for the basic action contract from 4.4.3 Action Execution Contract or the scalable command-line patterns from 4.2.3 Args & Command Lines.
unused_inputs_list: Tell Bazel What Did Not Matter
An action's declared inputs are normally part of the reuse decision: if an input changes, Bazel has to assume the output may change too. That is the right default for correctness and connects directly to the cache-key model from 2.4 Caching & Incrementality. unused_inputs_list is the escape hatch for tools that must receive a broad set of files at execution time, but can report afterward which of those files did not affect the outputs.2
The parameter points at a File containing the action's unused inputs. The official API describes it as generally one of the action's outputs, and says changes in listed files must not affect the action outputs in any way.2
def _compile_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".o")
unused = ctx.actions.declare_file(ctx.label.name + ".unused_inputs")
args = ctx.actions.args()
args.add("--out", out)
args.add("--unused-inputs", unused)
args.add_all(ctx.files.srcs)
args.add_all(ctx.files.headers)
ctx.actions.run(
executable = ctx.executable._compiler,
inputs = ctx.files.srcs + ctx.files.headers,
outputs = [out, unused],
arguments = [args],
unused_inputs_list = unused,
mnemonic = "CustomCompile",
)
return [DefaultInfo(files = depset([out]))]
This only works when the tool can tell the truth. If a header, interface file, generated metadata file, or config file appears in the unused-inputs list, the rule is promising that changing that file cannot change the declared outputs for this execution. A false unused-input report is a cache correctness bug, not a performance tweak.
The useful pattern is "available to the tool, irrelevant to the cache key." Recompilation avoidance in rules_haskell makes the complete path concrete:
- Start conservatively. A module compile action declares the full interface files of its dependencies. GHC may need those files, so omitting them would make the action contract incomplete.
- Expose a smaller change signal. The ruleset extracts the ABI hash nested in each interface file into a separate
.abifile and also declares that file as an input. Many interface-file changes leave this downstream-facing ABI signal unchanged.3 - Keep the rich input available. An ABI hash cannot replace an interface file. When an ABI change causes recompilation, GHC still needs the complete interface, so the
.hifile remains in the action's declared inputs.4 - Let the smaller signal control invalidation. The action's unused-inputs report lists the interface files. Bazel can then reuse the result when only those files change, while a changed
.abiinput invalidates the action. On that execution the full interfaces are already available.3,4
This separates two questions that an ordinary input list answers together: what might the tool need if it runs? and which change proves that it must run again? The conservative input set answers the first. The ABI files plus unused_inputs_list answer the second.
For the executable trail, the rules_haskell map connects the implementation in
haskell/private/actions/compile.bzl
to the focused
rules_haskell_tests/tests/recompilation/
regression workspace.5 These are ruleset-specific evidence for the general
unused_inputs_list contract, not a public API for downstream Haskell builds.
The same design can fit a compiler whose dependency artifact contains more data than its public interface signal, an analyzer that scans a conservative file set but emits the files that actually informed its report, or a generator that receives a catalog and can prove which entries contributed to its outputs. The smaller signal need not always be an ABI hash: it can be any deterministic, declared artifact that captures every change capable of affecting the output.
The safety boundary is exact: after accounting for the inputs that remain relevant, changing a file named in unused_inputs_list must not be able to change any declared output. The tool must derive that fact from the execution, not from a rule-author guess. Do not apply the pattern when an omitted detail can affect code generation, diagnostics, ordering, configuration, generated metadata, or any other declared output. When the tool reads hidden inputs, or when nondeterminism prevents it from reporting relevance reliably. In those cases, keep the input relevant and accept the invalidation until you can produce a complete smaller signal.
For the Haskell-specific combination of module-grained actions, ABI-based recompilation avoidance, and persistent GHC workers, continue with L7.1 rules_haskell. The reusable rule-authoring idea here does not depend on those Haskell mechanics.
resource_set: Size Local Action Scheduling
resource_set is a callback for estimating CPU and memory use when the action runs locally. Bazel calls the function with two positional arguments: an OS name string such as "osx" and the number of action inputs. The function returns a dictionary whose supported keys are "cpu", "memory" in MB, and "local_test". If the parameter is None, Bazel uses defaults of 1 CPU, 250 MB memory, and 1 local test.6
def _compiler_resources(os_name, input_count):
memory_mb = 512 + input_count * 2
if os_name == "osx":
memory_mb += 256
return {
"cpu": 1,
"memory": memory_mb,
}
def _compile_impl(ctx):
# ... declare outputs, inputs, args ...
ctx.actions.run(
executable = ctx.executable._compiler,
inputs = inputs,
outputs = [out],
arguments = [args],
mnemonic = "CustomCompile",
resource_set = _compiler_resources,
)
The callback must be a top-level function. Lambdas and nested functions are not allowed.6 Treat that as part of the same discipline used for Args.map_each: action-time callbacks should not capture arbitrary analysis-phase state.
Use resource_set when the default local estimate is materially wrong. A code generator that needs several GB of memory should not be scheduled as if it needed 250 MB. A tiny formatter over one file should not force the local scheduler to reserve more capacity than it needs. The right estimate helps Bazel avoid local oversubscription and improves utilization, but it is still an estimate, not a hard memory limit. Measure whether that estimate matters with the profiling workflows in 5.4 Performance.
This parameter is scoped to local execution. The official API says it estimates resource usage "if this action is run locally."6 6.3 Remote Execution Infrastructure explains the separate remote-execution environment and scheduling model. Do not assume resource_set will provision a larger remote worker.
Reach for these parameters only after the ordinary action contract is already solid. unused_inputs_list is for tools that can accurately report cache-irrelevant declared inputs after execution. resource_set is for actions whose local CPU or memory needs are large enough that Bazel's default estimate causes bad scheduling.
Both parameters are promises to Bazel. If the promise is wrong, the result is either incorrect cache reuse or misleading local scheduling.
Check your understanding · 3 questions
1.Which conditions make unused_inputs_list appropriate for a custom action?
Select all that apply
2.What does resource_set influence?
Select one answer
3.True or false: advanced action parameters are promises to Bazel.
Choose True or False for each sentence
resource_set is a hard memory limit enforced on the process.resource_set callback must be a top-level function.unused_inputs_list removes listed files from the action sandbox.Footnotes
-
actions —
ctx.actions.run()parameters includeunused_inputs_listandresource_set. ↩ -
actions —
unused_inputs_listis a file listing inputs unused by the action. Listed inputs must not affect outputs. ↩1 ↩2 -
Recompilation avoidance in rules_haskell — ABI-file extraction and cache-control pattern for Haskell interface files. ↩1 ↩2
-
Recompilation avoidance in rules_haskell — interface files remain action inputs and are still available when recompilation occurs. ↩1 ↩2
-
rules_haskell repository map — modern and legacy setup boundaries, GHC/package extensions, Nix/cross routes, and the private recompilation implementation paired with regression evidence ↩
-
actions —
resource_setcallback arguments, return keys, defaults, local-execution scope, and top-level function requirement. ↩1 ↩2 ↩3