Author a manifest

Updated Sep 02, 2026

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 as test or lint.
  • 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 when build receives 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, or script: exactly one recipe for work that has its own execution step. An action or target with a non-empty deps list may omit a recipe to form a dependency-only aggregate; this is preferred over a no-op command: ":".
  • 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 reject deps. The planned rule-level deps_from contract is not implemented in beta3.
  • dependency_order: scheduling policy for the deps list. parallel is the default; serial runs 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. The env and glob restriction 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 by netsuke help targets; it never replaces a referenced rule's description in 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.