Netsukefile is a YAML mapping. Unknown fields and duplicate mapping keys are
errors. netsuke_version and targets are required; all other top-level
collections are optional.
The following complete example shows every top-level section:
netsuke_version: "1.0.0"
vars:
greeting: Hello
macros:
- signature: "message(name)"
body: "{{ greeting }}, {{ name }}!"
rules:
- name: write_message
command: "echo '{{ message('Netsuke') }}' > {{ outs }}"
description: "Writing greeting"
actions:
- name: greet
command: "echo '{{ message('builder') }}'"
targets:
- name: greeting.txt
rule: write_message
defaults:
- greeting.txt
The top-level fields are:
netsuke_version: required semantic version for the manifest schema.vars: global strings, numbers, booleans, or lists available to Jinja.macros: named Jinja macro definitions registered before other fields render.rules: reusable recipes referenced by targets or actions.actions: implicitly phony operations, such astestorlint.targets: required list of file-producing or logical build nodes. An empty list is valid for an action-only manifest.defaults: target or action names used whenbuildreceives no explicit targets.
defaults entries are literal names in beta3; Jinja expressions are not
rendered in this field.
vars keys named env or glob are rejected because those names identify
built-in template helpers (see
Discover files with glob and
Select optional tools). Rather than silently
shadowing the helper, the manifest fails to parse and the error names the
offending key.
Rules and recipes
Rules must provide exactly one executable recipe. Actions and targets that
perform work must also provide exactly one recipe, but may omit it when a
non-empty deps list is their complete operation. This dependency-only
aggregate form is preferred over a no-op command such as command: ":":
command: one shell command, or an ordered list of commands.script: a multi-line script interpreted by the selected recipe shell.rule: the name of another rule to use.
Rules may also provide description, text used for Ninja's progress display.
Targets and actions may also provide description, but with a different
purpose: a target or action description is discovery metadata surfaced by
netsuke help targets (see
Generate and inspect artefacts). It does not
affect Ninja progress output, which stays driven by the referenced rule's
description.
A command list runs its entries in declaration order and stops at the first
non-zero exit, so entries share the fail-fast behaviour of a handwritten &&
chain. The command field is a StringOrList: a scalar remains one shell
command, while a YAML sequence is rendered and lowered one entry at a time.
This applies equally to rules, direct targets, and actions. Each entry sees the
same Jinja context, including {{ ins }} and {{ outs }}; those two
placeholders are resolved later to the concrete target's shell-quoted input and
output paths. An empty command list is rejected when the manifest is parsed.
Windows legacy recipe contract
On Windows, v0.1.x interprets every legacy command string, command list, and
script with Windows PowerShell (powershell.exe), not with the shell
that launched netsuke. Netsuke invokes it explicitly with an encoded,
non-interactive, no-profile command before Ninja executes a recipe. A build
started from PowerShell, cmd.exe, an IDE, or Git Bash therefore uses the same
recipe interpreter. This is a Windows PowerShell contract, not a PowerShell Core
(pwsh) contract.
Scalar commands and scripts each receive a fresh PowerShell process. A command
list receives one shared process: entries run in declaration order, and each
generated entry is followed immediately by a $LASTEXITCODE check. A non-zero
status stops the list before a later entry can overwrite it. An entry remains
opaque recipe text, so multiple native commands separated by semicolons inside
one entry are not checked individually; only the status left by that entry is
observed. PowerShell terminating errors also fail the recipe. Later entries see
PowerShell variables, $env: assignments, and locations left by earlier
entries, but state does not cross action or target boundaries.
Use PowerShell syntax in the default route. $name is a PowerShell variable and
$env:NAME reads an environment variable. PowerShell parses the braced
variable name in ${VAR:-default}, but does not perform POSIX default-value
expansion. Recipe text is protected from Ninja dollar expansion, so write
ordinary PowerShell dollars rather than $$. The rendered {{ ins }} and
{{ outs }} paths use literal, single-quoted PowerShell arguments; an
apostrophe is doubled, so O'Brien.txt becomes 'O''Brien.txt'. Build and
default-target paths containing spaces are escaped for Ninja before recipe
generation, so {{ outs }} can name a whitespace-containing output. Quote
every other path and argument with PowerShell syntax; arbitrary rendered Jinja
text is not shell-quoted.
Recipes that fit within Windows' 32,766-character command-line safety limit use
the encoded powershell.exe invocation described above. Larger scalar
commands, scripts, and command lists up to 1 MiB use Ninja's rspfile and
rspfile_content bindings, so recipe text is not truncated by the Windows
command-line limit. Larger recipes are rejected before Netsuke allocates their
UTF-16LE and Base64 payload. Each Ninja edge derives a unique response-file
name from $out; Ninja creates a unique .ps1 file in the edge's working
directory containing an ASCII PowerShell bootstrap and the Base64 UTF-16LE
recipe payload. The command invokes it with powershell.exe -File "$rspfile".
The bootstrap removes its own $PSCommandPath in a finally block, including
when the recipe succeeds or fails. Query-only generation emits these bindings
but does not create files. Response-file setup failures are reported by Ninja
as execution errors.
Ninja turns a failed recipe into its own non-zero result, and netsuke returns
failure after forwarding Ninja's output. The CLI contract distinguishes success
from failure; it does not promise to return the recipe's exact child value.
To retain POSIX interpretation on Windows, explicitly select a Git Bash-compatible runtime:
choco install git --yes --no-progress
$env:PATH = "C:\Program Files\Git\bin;$env:PATH"
$env:NETSUKE_WINDOWS_SHELL = "bash"
netsuke build
MSYS2 Bash is also supported when bash.exe is on PATH. Before build or
Ninja-tool execution, Netsuke checks this selection. If bash.exe --version
cannot run, it stops with instructions to install Git for Windows or MSYS2, add
Bash to PATH, or unset NETSUKE_WINDOWS_SHELL. generate and help targets
do not execute recipes, so they do not require Bash. In CI, install Git
explicitly, prepend its bin directory to PATH, set
NETSUKE_WINDOWS_SHELL=bash, and launch Netsuke normally from a pwsh step;
do not rely on a workflow-wide shell: bash setting.
For the Unix default and the explicit Bash route, each list entry is evaluated
inside its own brace group and the groups are joined with &&. The entry is
passed to eval as a shell-quoted payload, so an inline # comment or a
trailing control operator such as & cannot consume the generated group's
closing boundary. Brace groups run in the current shell rather than a subshell:
a changed working directory, environment assignment, or shell variable can
therefore be used by later entries. A failed entry stops the chain, and the
diagnostic identifies the generated action and one-based list-entry positions,
for example netsuke command-list failure: action HASH, entry 2. These
brace-group, eval, background-job, and exec restrictions apply only to the
Unix renderer and the explicit Windows Bash compatibility route; the Windows
PowerShell route uses its native-command and error checks instead.
netsuke_version: "1.0.0"
rules:
- name: comprehensive-check
description: Run the required checks sequentially
command:
- echo "check-fmt"
- echo "lint"
- echo "test"
targets:
- name: done
rule: comprehensive-check
The same list form can be attached directly to a target. Jinja rendering and
{{ outs }} interpolation apply independently to each entry:
netsuke_version: "1.0.0"
targets:
- name: report.txt
vars:
heading: Report
command:
- "printf '{{ heading }}\\n' > {{ outs }}"
- "printf 'complete\\n' >> {{ outs }}"
Prefer a command list for a short, ordered sequence of distinct commands.
Prefer script when the logic needs multi-line structure or shell constructs
such as loops, conditionals, or variable assignment.
Legacy recipes remain shell strings in v0.1.x. The structured command blocks and argv templates proposed in RFC #573 for v0.2.0 are intended to remove this shell-selection, quoting, path, variable, and exit-semantics ambiguity. They do not change the v0.1.x contract described here.
Targets, inputs, and dependencies
A target supports these fields:
name: one output path or a list of output paths.rule,command, orscript: exactly one recipe for work that has its own execution step. An action or target with a non-emptydepslist may omit a recipe to form a dependency-only aggregate; this is preferred over a no-opcommand: ":".sources: explicit inputs. They affect freshness and become{{ ins }}.deps: implicit dependencies. They affect freshness but do not become recipe arguments. Declare them on each target; reusable rules rejectdeps. The planned rule-leveldeps_fromcontract is not implemented in beta3.dependency_order: scheduling policy for thedepslist.parallelis the default;serialruns a list with more than one dependency in declaration order.order_only_deps: ordering dependencies. Their changes do not rebuild the dependent target.vars: values that override global variables for this target. Theenvandglobrestriction above applies here too.phony: marks a logical target that does not represent a file.always: forces the recipe to run whenever the target is requested.description: an optional human-readable summary of the public operation the target performs. It is discovery metadata shown bynetsuke help targets; it never replaces a referenced rule'sdescriptionin Ninja progress output.
name, sources, deps, and order_only_deps accept either one string or a
list of strings.
Netsuke quotes paths inserted through {{ ins }} and {{ outs }}. Other Jinja
values render as ordinary command text and are not automatically shell-quoted.
The shell_escape filter described in older drafts is not implemented in beta3.
Cycle detection follows sources and deps. Order-only dependencies enforce
ordering but do not participate in cycle detection.
Run direct dependencies serially
Actions and targets both accept dependency_order. Omit it, or set it to
parallel, to retain Ninja's ordinary concurrent scheduling. Set it to
serial when the direct deps list is an ordered workflow:
netsuke_version: "1.0.0"
actions:
- name: check-fmt
command: "echo checking format"
- name: lint
command: "echo linting"
- name: test
command: "echo testing"
- name: all
dependency_order: serial
deps:
- check-fmt
- lint
- test
targets:
- name: release-notes
command: "echo preparing release notes"
- name: release
command: "./package-release"
dependency_order: serial
deps:
- check-fmt
- test
- release-notes
The all action has no recipe because its dependencies are the complete
workflow. Netsuke lowers it to a native Ninja phony node, so it has no shell
no-op of its own.
For a serial list, Netsuke starts each direct dependency only after the preceding one succeeds. If an earlier dependency fails, later dependencies in that list do not start through the serial path. Repeated or shared dependencies are still owned by the one Ninja invocation and execute at most once.
Serial ordering applies only to the direct deps list. It does not serialize
sources, order_only_deps, or unrelated work. An independently requested or
otherwise reachable later dependency can still start through that separate
path; use a dedicated aggregate action when the whole workflow must share the
same ordered entry point.
Netsuke uses Ninja's dyndep support for serial lists with two or more
dependencies, and generated builds containing staged serial ordering require
Ninja 1.10 or newer. netsuke generate, build, and clean materialize the
generated sidecars under .netsuke/dyndep in the effective working directory
before writing or invoking the generated Ninja file. The sidecars are immutable
and content-addressed. Each sidecar-capable command retains the current bundle,
then at most 32 obsolete .dd files and 1 MiB of obsolete .dd bytes. Stale
.tmp files are removed while the exclusive sidecar-directory lease is held.
build and generate prune after materialization; clean prunes only after
successful ninja -t clean, and does not prune when clean fails.
An older arbitrary manifest written with generate --output may lose its
referenced sidecars after a later command. Regenerate that manifest before
using it if retention has removed any of its sidecars. The paths
.netsuke/serial and .netsuke/dyndep must not occur in any user graph path,
including outputs, inputs, implicit dependencies, and order-only dependencies;
they are reserved for Netsuke-generated gates and sidecars.
When migrating an existing manifest, see the v0.1.0 migration guide for the opt-in syntax, Ninja version requirement, and generated-state reservation.