Core Concepts

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:

  1. Parses the manifest for the requested target.
  2. Identifies all transitive dependencies.
  3. Constructs the action graph (commands to run).
  4. Executes actions in parallel, topologically sorted.
Static graph first Rules and targets are resolved before execution begins. That is what lets Netsuke hand a concrete plan to Ninja.
app
main.o
utils.o
compile
Dependency Graph Visualization

Defining reusable rules

Core

A 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.

Netsukefile
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"]
1

Rule name

Targets refer to a rule by name. Keep names short and literal.

2

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.

3

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 help targets
$ 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.

Netsukefile
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
New in v0.1.0-beta3

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 deps run 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 deps list is serialized — not sources, 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 dyndep support and require Ninja 1.10 or newer.
  • Netsuke materializes content-addressed sidecars under .netsuke/dyndep; the paths .netsuke/serial and .netsuke/dyndep are 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.ninja output

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.

Read Security Model →

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, and order_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_deps constrain ordering without forcing a rebuild
  • Targets marked always run 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.