Templating
Netsuke renders MiniJinja inside YAML values. The structure stays as data; templating fills in strings,
expands entries with foreach, and gates them with when — all before the build graph exists.
Why Jinja inside YAML
Netsuke validates YAML before rendering templates. That keeps the manifest legible, keeps the structure explicit, and still leaves room for repetition, conditions, and reusable helpers. The goal is not to hide the build graph — it is to keep repeated YAML short enough to tolerate.
YAML first
Structure is checked before templating renders commands. Structural mistakes fail early, with YAML line numbers.
Entries, not branches
foreach and when select manifest entries at plan time. Build-time branching belongs in recipe commands, not the manifest.
Reusable helpers
Declared macros, filters, and the standard library cover common path, file, and environment tasks.
YAML foundations
A Netsukefile is a YAML mapping. Netsuke rejects duplicate keys, unknown fields, and missing
required fields; netsuke_version and targets are required. Quote version numbers
and any value that begins with {{ — an unquoted { has meaning in YAML.
Use | for a multi-line string that preserves line breaks, or >- to fold lines
into one string.
netsuke_version: "1.0.0"
vars:
greeting: Hello
recipients:
- Netsuke
- Ninja
targets:
- name: greeting.txt
command: >-
printf '%s\n'
"{{ greeting }}, {{ recipients | first }}!"
> {{ outs }}
defaults:
- greeting.txt
How rendering works
MiniJinja renders string fields. Put an expression inside {{ ... }}, read values
declared under vars by name, pass a value through a filter with |, call a
function with parentheses, and apply a test with is.
Rules that keep manifests honest
- Undefined values are errors, not silent empty strings.
- Only
{{ ins }}and{{ outs }}are automatically quoted for the host shell; other rendered values are not. - Structural statements such as
{% for %}cannot reshape the YAML document. Useforeach,when, and declared macros instead.
Order of operations
- Parse and validate the YAML document.
- Evaluate
foreachandwhento select and clone entries. - Deserialize the typed manifest and render every string field.
- Build the graph and hand a static plan to Ninja.
The control keys run before typed deserialization, so they shape which entries exist — nothing about them survives into the build.
Repeating with foreach
foreach takes a direct Jinja expression — no {{ ... }} braces — that
evaluates to a sequence: a list declared under vars, or a discovery call such as
glob('src/*.c'). Netsuke clones the entry once per element, exposing item (the
element) and index (its position) to every string field and to when.
It works on targets and on top-level actions alike.
vars:
designs:
- hero
- logo
targets:
- foreach: designs
name: "build/{{ item }}.png"
rule: rasterise
sources: "design/svg/{{ item }}.svg"
- foreach: glob('pages/*.md')
name: "site/{{ item | basename | with_suffix('.html') }}"
rule: page
sources: "{{ item }}"
Glob over source files only. Templates expand before anything is built, so a glob over generated outputs sees an empty directory on a clean checkout.
Selecting with when
when also takes a direct expression. If it evaluates false, the entry simply does not exist in
the plan. On a foreach entry it runs once per clone with item and
index in scope, so a single template can both expand and filter.
Complementary branches pair naturally with
command_available(...).
targets:
- when: command_available('magick')
name: "build/hero.png"
rule: magick_convert
sources: "design/hero.svg"
- when: not command_available('magick')
name: "build/hero.png"
rule: inkscape_convert
sources: "design/hero.svg"
when selects manifest entries at plan time; it is not build-time branching. A condition that must be re-evaluated during the build belongs in the recipe command or script.
Macros
Declared macros are the manifest's unit of reuse: a signature naming the macro and its
parameters, and a body template rendered wherever the macro is called. Because they are
declared as data rather than written as {% macro %} statements, the YAML document stays a
document.
macros:
- signature: "obj_name(src)"
body: |
{{ src | basename | with_suffix('.o') }}
targets:
- foreach: glob('src/*.c')
name: "build/{{ obj_name(item) }}"
rule: "compile"
sources: "{{ item }}"
Common Patterns
Recipes
Platform-specific sources with `when`
Use when on repeated entries to include targets only when the current variables match the platform you want.
targets:
- when: target_os == 'linux'
name: "sys/linux.o"
rule: "compile"
sources: "src/linux_utils.c"
- when: target_os == 'macos'
name: "sys/macos.o"
rule: "compile"
sources: "src/macos_utils.c"
Keep conditions coarse-grained. Avoid selecting on individual compiler versions unless absolutely necessary.