Build recipes

The.make recipe reference

Hypermake's recipe language keeps Make's rule anatomy — target: prerequisites plus tab-indented commands — and drops the macro language. Graph facts live in rule headers; behavior lives in commands; discovery lives in dedicated producer targets. Every rule on this page is real: the first two build the page you are reading.

Rule anatomy

A recipe is a path-local .make file: the file sits at the path that explains its target (src://rendered/public/{path}.html.make owns rendered/public/{path}.html). This site's page recipe:

This page's recipe src/rendered/public/{path}.html.make
rendered/public/{path}.html: rendered/public/{path}.html.d | rendered/public/{path|dirname}/
	lzr render-page --root {rootQ} --in src/{path}.html --template {path}.html --components-config config.json --components src/_components --out "$@" --minify
		
  • The rule header is one line: target: prerequisites, with a space after the colon (a:b is an invalid header). Bare target: declares no prerequisites. Backslash line continuations are not supported — the header stays on one line.
  • One target per header. A header like a b: c does not create two targets; it creates one unbuildable target named "a b". One artifact, one rule.
  • Command lines are tab-indented and run through bash from the project root. A rule with no command lines is a parse error — the whole file is rejected and hypermake doctor reports recipe.parse_failed.
  • A blank line ends the rule. # comment lines are skipped anywhere, including between commands.
  • GNU command prefixes are accepted: - ignores a failing command; @ and + are stripped.
  • Prerequisites after | are order-only: they gate existence without forcing rebuilds, and order-only directories (trailing /) are created before the commands run.

Named captures, not %

Patterns use named captures such as {path} or {asset}. GNU's % is not a wildcard here — it matches literally, and the linter says so. The captured name binds everywhere in the rule: prerequisites, order-only entries, and commands.

Target-derived prerequisite src/rendered/public/assets/{asset}.css.make
rendered/public/assets/{asset}.css: src/assets/{asset}.css | rendered/public/assets/
	cp "$<" "$@"
		

The target's own captures select the source: adding src/assets/print.css publishes rendered/public/assets/print.css with no new rule. Some capture names are typed:

Typed captures

An unmatched shape means the recipe simply does not match that target.

CaptureMatches
{width}, {height}, {dpi}, {quality}, {fps}digits only
{date}YYYY-MM-DD
{dim}300x250-style dimensions
{uuid} / {uuidir}RFC-4122 UUID / two hex chars
{path} (and any name containing "Path")may span / segments
everything elseone path segment (no /)

A capture repeated in one pattern must bind the same text each time. When several recipes match one target, the highest specificity score wins: literal characters and typed captures raise it, captures lower it — so a colocated literal recipe usually specializes a broad pattern, but specificity is computed, never assumed. hypermake match TARGET --json discloses the chosen recipe, its score, and the bound captures; two recipes producing the same target shape at equal precedence are a hard load error ("declares duplicate target family").

Prerequisites are matcher surfaces

A prerequisite is not just a file that already exists — it declares where change can come from. Three shapes, one doctrine:

Static, target-derived, discovery .make
static           src/site.css
target-derived   src/assets/{asset}.css
discovery        src/**/*.css
		

A prerequisite containing glob characters (*, ?, [...], recursive **) becomes a discovery matcher: it is fingerprinted in target state as hypermake-matcher://src/**/*.css — the matched file list plus each file's fingerprint — rather than a concrete edge. A newly created file that matches the glob invalidates the target, so the graph notices files that did not exist when the rule was written. Matcher prerequisites are not passed to $< or $^.

Automatic and template variables

Variables available in commands

Make's automatic variables are shell-safe here: values compose correctly with quoting, including suffixed forms such as $@.tmp inside double quotes. Only these three exist.

VariableValue
$@the target, as an absolute resolved path
$<the first concrete prerequisite
$^all concrete prerequisites, shell-quoted (order-only and matchers excluded)
{target}, {targetDir}, {targetBase}project-relative target path, its directory, and the path minus extension
{root}, {rootQ}the project root, plain and shell-quoted
{capture}every named capture from the pattern, usable in prerequisites and commands; {capture|dirname} and {capture|basename} take the value's directory or base name

Target flags

Non-file semantics are declared per target, above the rule they modify, in the same file:

A value-carrying producer .make
.VIRTUAL: cache/words.d
.DYNAMIC: cache/words.d

cache/words.d: src/*.txt
	printf 'cache/words.json: src/hello.txt\n'
		

Flag semantics

Flags attach when the rule header is read — declare them before the rule.

DeclarationMeaning
.VIRTUAL: targetNo file is required or written; the command's output is captured into the state store as the target's value (hypermake value TARGET shows it). Replaces .PHONY.
.DYNAMIC: targetRe-evaluated when the target is directly requested. A consumer visiting it transitively does not refresh it.
.ALWAYS: targetRe-evaluated on every graph visit, including transitive ones.
.MULTI-WRITER: targetSanctions a deliberate co-writer that would otherwise be an overlapping-writer error (below).
~target: prereqsOn-demand: buildable directly, excluded from aggregate traversal (below).

On-demand targets with ~

Excluded from aggregates, buildable directly .make
~cache/report/summary.pdf: cache/report/summary.pdf.d
	generate-report --deps "$<" --out "$@"
		

Prefixing the target with ~ marks it on-demand: an aggregate such as hypermake public that reaches it through a prerequisite edge will not build it, while hypermake cache/report/summary.pdf builds it as the requested root. This is how pages link to delivery artifacts — ZIPs, PDFs, exports — without every linked artifact joining the default build.

Dynamic dependencies:.d producers

Discovery never hides in a command body or an include. A target ending in .d is a dependency manifest: a .VIRTUAL + .DYNAMIC target, owned by its own *.d.make file, whose captured value is Make-syntax edges. This site discovers every page's reads that way:

This page's edge producer src/rendered/public/{path}.html.d.make
.VIRTUAL: rendered/public/{path}.html.d
.DYNAMIC: rendered/public/{path}.html.d

rendered/public/{path}.html.d: src://{path}.html src/_components/*.html config.json
	hypermake d-manifest --target rendered/public/{path}.html --lzr-page --in src/{path}.html --template {path}.html --components-config config.json --components src/_components
		

The consumer lists the .d target as a normal prerequisite (see the first panel on this page). Expansion is consumer-scoped: the manifest's emitted rule must name the consuming target by its plain project-relative path — a manifest that names anything else fails the build with ".d manifest has no rule for target …". Emitted manifests may use backslash line continuations, unlike authored .make files. The producer's own rule still declares the matcher surface the edges can come from (src://{path}.html, the components glob, config.json), so the graph knows where future edges originate before they exist.

Inspect the discovered edges for this page shell
hypermake value rendered/public/docs/make-recipes.html.d
hypermake explain rendered/public/docs/make-recipes.html
		

Legacy escape hatches exist and lint: a body line @always maps to .ALWAYS, and @dynamic-prereq CMD runs an inline discovery command. Doctrine keeps graph discovery in .d.make producers, where the edges are inspectable state, not side effects. There is also a declarative JSON recipe form (a native-spec recipe source in hypermake.json) with fields beyond .make, such as digest targets and external-tool actions.

One artifact, one writer

Two recipes that can write the same output make the result depend on scheduling order, so overlap is an error, not a convention. Equal-precedence duplicates fail at load time. A recipe targeting DIR/.synced declares itself the sole materializer of DIR/** (the sync idiom: remove and repopulate the tree); any other recipe rooted under that tree raises diagnostics.overlapping_writer until it is either removed or sanctioned with .MULTI-WRITER. Locality is checked too: hypermake doctor flags files carrying more than one rule (recipe.multiple_targets_per_make, reason recipe.locality) — one target family should occupy one file.

Protocol addresses in rules

Targets and prerequisites may be spelled as protocol addresses; they resolve through the project's protocols mapping, and automatic variables hand commands the resolved filesystem paths. These are equivalent under this project's mapping of public to rendered/public:

Two spellings of the same rule header .make
# protocol form
public://assets/{asset}.css: src://assets/{asset}.css | rendered/public/assets/
# plain-path form (what this site's recipe authors)
rendered/public/assets/{asset}.css: src/assets/{asset}.css | rendered/public/assets/
		

cache:// outputs work the same way: a recipe may target derived state under cache://, and both request spellings — the plain path or the protocol address — reach the same rule. One caveat from above: inside a materialized .d manifest, spell the consuming target as its plain path.

Vocabulary, quoted from the system

These definitions are quoted from the binary-owned glossary at build time (public://_hypermake/site/concepts-recipes.json, materialized from hypermake explain concept://<term>), so this page cannot drift from the system that implements it.

concept://recipe Recipe

A recipe owns a target pattern and defines how matching targets become fresh. Recipes live in path-local.make files (one target family per file); the rule header owns graph facts (target, prerequisites, order-only directories) and the tab-indented commands own the action. Patterns use named captures such as {path}, never %.

concept://recipe-doctrine Recipe Doctrine

Recipes are path-local doctrine: graph facts belong in rule headers, not shell prefixes. Use {path}-style captures instead of % wildcards,.VIRTUAL instead of.PHONY,.DYNAMIC for direct-request refresh,.ALWAYS for direct and transitive re-evaluation, and.d.make producer targets instead of include or $(shell) discovery. Make variables and conditionals are not supported. See README.md, Recipe Doctrine.

concept://prerequisite Prerequisite

Prerequisites are matcher surfaces on a rule header: concrete files, target-derived paths, or discovery matchers such as src/**/*.scss. Order-only prerequisites after | (typically directories) gate existence without forcing rebuilds. Dynamic prerequisite lists are graph artifacts produced by.d.make targets, not hidden include side effects.

concept://d-make .d.make Dynamic Dependency Producers

A.d.make file owns a.VIRTUAL and.DYNAMIC.d target whose command computes the dynamic prerequisite manifest for another target. The consuming recipe lists the.d target as a normal prerequisite, making dynamic edges inspectable graph artifacts (see them with value TARGET.d) instead of hidden scans.

concept://virtual .VIRTUAL

.VIRTUAL declares that a target is not a real file: its identity and value live in the state store rather than on disk. Use it (usually with.DYNAMIC) for manifest and discovery targets. It replaces GNU Make's.PHONY, which Hypermake does not support.

concept://dynamic .DYNAMIC

.DYNAMIC marks a target to be refreshed when that target is directly requested (or a forced build reaches it). A transitive consumer does not by itself refresh the target. Typical use is an inspectable.d manifest that callers deliberately request before consuming its value.

concept://recipe-commands Recipe Commands

Recipe commands are the action side of a rule. Hypermake-owned internal actions and captured-output recipes write outputs atomically through same-directory temp files; arbitrary shell redirection remains shell-owned and must not be described as atomic. Autovars such as $@ and $

Verify this yourself shell
hypermake match rendered/public/docs/make-recipes.html --json
hypermake explain rendered/public/docs/make-recipes.html
hypermake value rendered/public/docs/make-recipes.html.d
hypermake doctor
hypermake explain concept://recipe-doctrine