Coming from GNU make
Hypermake keeps what made Make durable — rule headers, prerequisites, tab-indented commands, automatic variables — and removes the macro language layered on top. There are no variables, no conditionals, no includes, no $(shell). Each removed idiom has a graph-native replacement, and the parser teaches it to you per line rather than failing silently.
What carries over unchanged
target: prerequisitesrule headers with tab-indented command lines.- Order-only prerequisites after
|— and order-only directories (trailing/) are created for you. - Automatic variables
$@,$<,$^— here expanded shell-safely to resolved absolute paths. Other automatics ($*,$?,$%) do not exist. - Command prefixes
-(ignore failure),@and+(accepted, stripped). #comments; blank line ends a rule.
The migration table
GNU idiom to Hypermake shape
The teaching column quotes hypermake doctor's per-line lint (recipe.gnu_make_idiom findings), anchored on concept://recipe-doctrine.
| GNU make | Hypermake | Why / teaching |
|---|---|---|
%.css: %.scss | {name}.css: {name}.scss | % wildcards are not patterns in Hypermake (they match literally); use named captures such as {path} or {name}. Captures also type ({width} matches digits) and bind in commands. |
.PHONY: build | .VIRTUAL: target | GNU special targets are not supported. .VIRTUAL means no file is required and the command output becomes the target's captured value; add .DYNAMIC (refresh on direct request) or .ALWAYS (refresh on every visit) for re-run semantics. |
.SILENT, .SUFFIXES, .DELETE_ON_ERROR, .ONESHELL, … | — | The whole special-target family lints as special-target; the behaviors are either defaults or graph facts declared per target. |
CC:= gcc, SRCS += … | — | Make variables are not supported; graph facts live in rule headers, paths in capture variables such as {path}. Recipe-side values come from captures and template variables ({root}, {targetDir}, …). |
ifeq/ifdef/define/export | — | Conditionals, defines, and exports are not supported; graph facts live in rule headers, behavior in commands. Variant outputs are separate targets with their own recipes. |
include deps.mk | .d.make producer targets | include has no Hypermake equivalent; dynamic edges are .d.make graph targets consumed as prerequisites — inspectable with hypermake value TARGET.d, never globally included. |
$(shell find …) | .d.make producer targets | $(shell …) is not supported; discovery belongs in a .d.make dynamic dependency producer with its own declared matcher surface. |
$(wildcard src/*.css) | src/*.css in the prerequisite list | Globs are legal prerequisites directly: they become fingerprinted discovery matchers, and a newly created matching file invalidates the target. |
a.min.js b.min.js: build.sh | one rule per target, one file per rule | Multi-target rules are not supported — that header parses as one unbuildable target named "a.min.js b.min.js". Separately, hypermake doctor flags files containing several rules (recipe.multiple_targets_per_make): locality doctrine wants one target family per .make file. |
deps: a \ (continuation lines) | one-line rule headers | Backslash continuations are not supported in authored .make files — the \ becomes a literal prerequisite. (Materialized .d manifests may use them.) |
VPATH / vpath | protocol addresses | Search paths are implicit state; Hypermake replaces them with explicit protocol mappings — src://, cache://, public:// addresses are valid targets and prerequisites. |
make all aggregates | aggregate targets + ~ on-demand | An aggregate is an ordinary rule listing its prerequisites (this site's public). Targets prefixed ~ are excluded from aggregate traversal and built only when directly requested — reachability is a projection choice, not a side effect of linking. |
The linter teaches at the moment of the mistake
Drop a legacy Makefile into a recipe root and hypermake doctor answers with per-line translations instead of a parse mystery. This transcript is from exactly that experiment — a three-rule GNU Makefile in a scratch project:
warning recipe.gnu_make_idiom 1x
Recipe file uses GNU Make idioms: src/gnu/legacy.make
edit src/gnu/legacy.make
line 1: variable-assignment -> Make variables are not supported; graph facts live in rule headers, paths in capture variables such as {path}
line 2: special-target -> GNU special targets are not supported; use .VIRTUAL for non-file targets and .DYNAMIC for refresh-on-request targets
percent-pattern -> % wildcards are not patterns in Hypermake (they match literally); use named captures such as {path} or {campaign}
reason recipe.gnu_make_idiom
Files that fail outright (a rule without commands, a header without the colon-space) surface as recipe.parse_failed with the offending file named; dropped lines carry invalid_rule_header or skipped_dot_target findings, and the legacy body directive @always lints as legacy_always. A .make file is never silently ignored.
hypermake explain concept://recipe-doctrine hypermake doctor hypermake match rendered/public/docs/coming-from-make.html --json hypermake scaffold recipe cache/example/first.txt