A modern, declarative build system compiler. YAML + Jinja in, Ninja out. Nothing more. Nothing less.
Netsuke is a friendly build system that compiles structured manifests into
a Ninja build graph. It’s not a shell-script runner, a meta-task framework, or
a domain-specific CI layer. It’s make, if make hadn’t been invented in 1977.
- Declarative: Targets, rules, and dependencies described explicitly.
- Dynamic when needed: Jinja templating for loops, macros, conditionals, file globbing.
- Static where required: Always compiles to a reproducible, fully static dependency graph.
- Unopinionated: No magic for C, Rust, Python, JavaScript, or any other blessed language.
- Safe: All variable interpolation is securely shell-escaped by default.
- Fast: Builds executed by Ninja, the fastest graph executor we know of.
netsuke_version: "1.0"
vars:
cc: clang
cflags: -Wall -Werror
rules:
- name: compile
command: "{{ cc }} {{ cflags }} -c {{ ins }} -o {{ outs }}"
- name: link
command: "{{ cc }} {{ cflags }} {{ ins }} -o {{ outs }}"
targets:
- foreach: glob('src/*.c')
name: "build/{{ item | basename | with_suffix('.o') }}"
rule: compile
sources: "{{ item }}"
- name: app
rule: link
sources: "{{ glob('src/*.c') | map('basename') | map('with_suffix', '.o') }}"Yes, it’s just YAML. Yes, that’s a Jinja foreach. No, you don’t need to
define .PHONY or remember what $@ means. This is 2025. You deserve better.
Rules are reusable command templates. Each one has exactly one of:
command:- a single shell stringscript:- a multi-line block- (or) can be declared inline on a target
rules:
- name: rasterise
script: |
inkscape --export-png={{ ins }} {{ outs }}Targets are things you want to build.
- name: build/logo.png
rule: rasterise
sources: assets/logo.svgTargets can also define:
deps: explicit dependenciesorder_only_deps: e.g.mkdir -p buildvars: per-target variables
You may also use command: or script: instead of referencing a rule.
Phony targets behave like Make’s .PHONY:
- name: clean
phony: true
always: true
command: rm -rf buildFor cleaner structure, you may also define phony targets under an actions:
block:
actions:
- name: test
command: pytestAll actions are treated as { phony: true, always: false } by default.
Netsuke uses MiniJinja to render your manifest before parsing.
You can:
-
Glob files:
{{ glob('src/**/*.c') }} -
Read environment vars:
{{ env('CC') }} -
Use filters:
{{ path | basename | with_suffix('.o') }} -
Define reusable macros:
macros: - signature: "shout(msg)" body: | echo "{{ msg | upper }}"
Templating happens before parsing, so any valid output must be valid YAML.
Shell commands are automatically escaped. Interpolation into command: or
script: will never yield a command injection vulnerability unless you
explicitly ask for | raw.
command: "echo {{ dangerous_value }}" # Safe
command: "echo {{ dangerous_value | raw }}" # Unsafe (your problem now)netsuke [build] [target1 target2 ...]
netsuke clean
netsuke graph
netsuke manifest FILEnetsukealone builds thedefaults:targets from your manifestnetsuke graphemits a Graphviz.dotof the build DAGnetsuke cleanrunsninja -t cleannetsuke manifest FILEwrites the Ninja manifest toFILEwithout invoking Ninja
You can also pass:
--fileto use an alternate manifest--directoryto run in a different working dir-j Nto control parallelism (passed through to Ninja)-v,--verboseto enable verbose logging
Release builds include a netsuke.1 manual page generated from the Clap
definitions, providing the same flags and subcommands documented via --help.
Manual page generation honours SOURCE_DATE_EPOCH for reproducible dates. If
the value is invalid, a warning is emitted and the date falls back to
1970-01-01. If SOURCE_DATE_EPOCH is unset, the date deterministically falls
back to 1970-01-01 without a warning. The published crate does not include
this file; packagers can source it from release artefacts under
target/generated-man/<target>/<profile>/.
Netsuke is under active development. It’s not finished, but it’s buildable, usable, and increasingly delightful.
Coming soon:
graph --htmlfor interactive DAGs- Extensible plugin system for filters/functions
- Toolchain presets (
cargo,node, etc.)
A netsuke is a small carved object used to fasten things securely to a belt. It’s not the sword. It’s not the pouch. It’s the thing that connects them.
That’s what this is: a tidy connector between your intent and the tool that gets it done.
Netsuke is distributed under the ISC licence. You don't need a legal thesis to use a build tool.