Rules & Targets
In Netsuke, a Rule defines how something is built, while a Target defines what is built. Understanding the relationship between these two concepts is key to mastering the build graph.
The Build Graph
Netsuke constructs a Directed Acyclic Graph (DAG) where nodes are targets and edges are dependencies. When you run netsuke build app, the system:
- Parses the manifest for the requested target.
- Identifies all transitive dependencies.
- Constructs the action graph (commands to run).
- Executes actions in parallel, topologically sorted.
Defining reusable rules
CoreA Netsuke rule is a reusable command or script template. Targets can reference the rule by name, add their own inputs, and keep duplicated shell out of the manifest.
rules:
- name: "compile"
command: "gcc -c {{ ins }} -o {{ outs }}"
description: "Compiling {{ outs }}"
- name: "link"
command: "gcc {{ ins }} -o {{ outs }}"
targets:
- name: "app"
rule: "link"
sources: ["main.o", "utils.o"]
Rule name
Targets refer to a rule by name. Keep names short and literal.
Recipe field
A rule carries exactly one recipe: command or script. A command may be one shell string or an ordered list of commands chained fail-fast with &&. Rules never declare deps; dependencies belong on the target or action.
Target wiring
Targets supply the concrete sources, outputs, and optional per-target variables.
Target fields
Netsuke target references are plain names and paths. The important distinction is between normal dependencies, order-only dependencies, and phony or always-run targets.
| Field | Example | Meaning |
|---|---|---|
| sources | ["main.o", "utils.o"] | Build inputs whose changes should trigger a rebuild. |
| deps | ["build"] | Implicit dependencies that must be built first and do trigger rebuilds, but never appear as recipe arguments. A target or action with a non-empty deps list may omit its recipe and become a dependency-only aggregate. |
| vars | { opt: "-O2" } | Values that override global vars for this target only. env and glob are rejected as keys here too. |
| order_only_deps | ["build-dir"] | Dependencies that must run first but do not trigger rebuilds when they change. |
| dependency_order | serial | Scheduling policy for the deps list. parallel (the default) keeps Ninja's ordinary concurrent scheduling; serial runs the direct dependencies in declaration order. |
| description | "Package a release" | Discovery metadata shown by netsuke help targets. On a rule, description instead drives Ninja's progress display. |
| phony / always | true / false | Control whether a target is logical only or should run every time. |
CLI Tip: Querying Targets
Use netsuke help targets to list the catalogue of targets and actions with their descriptions, and netsuke graph or netsuke generate to inspect the plan without running a full build.
$ netsuke graph --output build.dot
Serial dependency ordering
Since v0.1.0-beta2
Targets and actions accept dependency_order. Omit it, or set it to
parallel, to keep Ninja's ordinary concurrent scheduling. Set it to
serial when the direct deps list is an ordered
workflow: each dependency starts only after the preceding one succeeds, and if an earlier dependency fails,
later ones in the list do not start through the serial path.
actions:
- name: check-fmt
command: "make check-fmt"
- name: lint
command: "make lint"
- name: test
command: "make test"
- name: all
dependency_order: serial
deps:
- check-fmt
- lint
- test
The all action above has no recipe: its dependencies are the whole workflow, so Netsuke lowers it to a native Ninja phony node with no shell no-op of its own. Any action or target with a non-empty deps list may omit rule, command, and script this way. Prefer it to the old command: ":" idiom.
What serial ordering guarantees
- Direct
depsrun in declaration order, each starting only after the previous one succeeds. - Repeated or shared dependencies still execute at most once in the Ninja invocation.
- A failed dependency stops later entries from starting through the serial path.
What to watch for
- Only the direct
depslist is serialized — notsources,order_only_deps, or unrelated work. A dependency reachable through another path can still start there; use a dedicated aggregate action when the whole workflow must share one ordered entry point. - Serial lists with two or more dependencies use Ninja's
dyndepsupport and require Ninja 1.10 or newer. - Netsuke materializes content-addressed sidecars under
.netsuke/dyndep; the paths.netsuke/serialand.netsuke/dyndepare reserved and must not appear in the build graph.
Outputs & Determinism
Deterministic Output
Netsuke generates the same static plan from the same manifest and environment. Deterministic manifests make rebuild decisions easier to explain and verify.
- Consistent across machines
- Bit-for-bit identical builds
- Byte-for-byte stable
build.ninjaoutput
Validation boundaries
Schema validation, deterministic plan generation, quoted {{ ins }} and {{ outs }} markers, and a documented recipe-shell contract are the safety story. Build and default-target paths reject $, spaces, colons, |, and control characters because Ninja cannot represent them unambiguously.
Clean and incremental builds
Netsuke does not execute the build itself. It compiles the manifest into a static plan and hands that plan to Ninja, which decides what actually needs to run. How granular the rules and targets are therefore determines how much work an incremental build can skip.
Clean build
With no outputs on disk, every edge in the plan is out of date, so Ninja runs every command once. This is the upper bound on build work: the whole graph, in dependency order.
- Each target's recipe executes exactly once
- Ordering follows
sources,deps, andorder_only_deps
Incremental build
Ninja walks the same static graph and compares each edge's declared inputs against its outputs. Only the commands whose inputs changed are re-run, along with whatever depends on their results.
- Unchanged subgraphs are skipped, not re-checked command by command
-
order_only_depsconstrain ordering without forcing a rebuild - Targets marked
alwaysrun regardless of input state
Where the cost sits
All the dynamic work happens once, at generation time. Jinja expansion, foreach, when, and glob resolution are all evaluated before build.ninja is written. The plan Ninja receives contains no conditionals, no string manipulation, and no filesystem discovery, so the execution phase spends its time on dependency checking and on running commands.
Generation is deterministic: the same manifest and environment produce a byte-for-byte identical build.ninja. An unchanged manifest therefore yields an unchanged plan, and a build with nothing to do collapses to a dependency check with no recipes executed.