Make Cheatsheet
Recursive Make
Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
$(MAKE) and -C
A recursive make is one makefile invoking another as a sub-process. Always call
it as $(MAKE), never as a literal make, so that the sub-make inherits the
same binary, the flags, and the job server.
SUBDIRS := liba libb .PHONY: all $(SUBDIRS) all: $(SUBDIRS) $(SUBDIRS): $(MAKE) -C $@ libb: liba # ordering between sub-builds
make -C liba make[1]: Entering directory '/tmp/rec/liba' make[1]: Leaving directory '/tmp/rec/liba' make -C libb
- -C dir chdir's before reading the makefile, so relative paths inside the
sub-makefile stay relative to that directory. $(CURDIR) is the sub-make's
own directory.
- MAKELEVEL is 0 at the top and increments per level. Guard top-level-only
logic with ifeq ($(MAKELEVEL),0).
- make[1]: Entering directory lines come free. Silence them with
--no-print-directory, but keep them in CI, because a compiler error's
relative path is meaningless without them.
The + recipe prefix and the job server
-j is implemented with a job server that sub-makes must join, otherwise each
one starts its own pool and you get N × N parallel jobs.
$(SUBDIRS): +$(MAKE) -C $@
- Any recipe line that literally contains $(MAKE) or ${MAKE} is already
treated as recursive, and runs even under -n, -t, and -q.
- The + prefix is what you need when the sub-make is hidden behind a wrapper,
a shell script, or a variable that does not spell out $(MAKE).
- MAKEFLAGS carries the job server to children. Inspecting it in a sub-make
under -j2 shows something like
-j2 --jobserver-auth=fifo:/tmp/GMfifo13195.
- GNU make 4.4 switched from a pipe pair to a named FIFO, which fixed most of
the historical warning: jobserver unavailable: using -j1 noise. On older
versions that warning means a recipe launched a make that make did not
recognize as recursive, and + is the fix.
Never write -j inside a recipe's $(MAKE) call. Pass it once on the outer
command line and let it propagate.
Passing variables down
Variables do not automatically reach a sub-make. Three ways to send them, in increasing bluntness.
export PREFIX := /opt/thing # this one variable, to recipes and sub-makes export # every variable, from this point on .EXPORT_ALL_VARIABLES: # every variable, whole file unexport CFLAGS # opt one back out
# or on the sub-make command line, which the sub-makefile cannot override
$(SUBDIRS):
$(MAKE) -C $@ CFLAGS='$(CFLAGS)'| Method | Sub-make sees it as | Sub-makefile can override |
|---|---|---|
export VAR | environment | yes |
$(MAKE) VAR=x | command line | no, unless override |
MAKEFLAGS += VAR=x | command line, to every level | no |
| nothing | not set | — |
Two traps. First, an exported variable becomes an environment variable for
every recipe, so exporting something named PATH or SHELL changes how
commands run. Second, command-line variables propagate to all deeper levels
automatically, which makes a deep override impossible to shake off.
Variables make always propagates: MAKEFLAGS, MFLAGS, MAKELEVEL, MAKE,
and CURDIR. MAKEFILE_LIST and .DEFAULT_GOAL are per-invocation and do not.
The -C versus cd distinction
# Wrong: a new shell per line, so the cd is gone by line two
bad:
cd liba
$(MAKE)
# Works, but loses the job server and the directory banner
ok:
cd liba && $(MAKE)
# Correct
good:
$(MAKE) -C libaWhy non-recursive is usually better
Each sub-make sees only its own directory, and that single fact causes every recursive-make problem. This is the argument of Peter Miller's 1997 paper Recursive Make Considered Harmful, and it still holds.
| Problem | Why it happens |
|---|---|
| Wrong build order | The top level guesses dependencies between subdirectories |
| Under-parallelized | libb waits for all of liba, not just the parts it needs |
| Repeated work | Each level re-reads makefiles and re-runs $(shell) probes |
| Incorrect incremental builds | A cross-directory header edge no sub-make can see |
-j fragility | A missed + or a wrapper script breaks the job server |
| Slow no-op builds | N sub-makes to decide there is nothing to do |
The non-recursive alternative: one make process reads included fragments from
every directory and builds one complete dependency graph.
# top-level Makefile MODULES := liba libb app include $(patsubst %,%/module.mk,$(MODULES)) .PHONY: all all: $(ALL_TARGETS)
# liba/module.mk , paths relative to the top level liba_SRC := $(wildcard liba/*.c) liba_OBJ := $(liba_SRC:.c=.o) ALL_TARGETS += liba/liba.a liba/liba.a: $(liba_OBJ) $(AR) rcs $@ $^
The rules: every path is written relative to the top level, every variable is
prefixed with the module name to avoid collisions, and fragments append to shared
lists with += rather than assigning.
When recursion is still the right call:
- The sub-directory is a genuinely separate project with its own build system.
- You are driving a different tool entirely (
cargo,go build,cmake). - The tree is vendored third-party code you do not want to model.
In those cases mark the target .PHONY, prefix the line with +, and accept
that make cannot reason about what is inside.
A safe recursive skeleton
MAKEFLAGS += --no-print-directory
SUBDIRS := liba libb app
export PREFIX ?= /usr/local
.PHONY: all clean $(SUBDIRS)
all: $(SUBDIRS)
$(SUBDIRS):
+$(MAKE) -C $@ $(TARGET)
app: liba libb
clean:
+$(MAKE) -C liba clean
+$(MAKE) -C libb clean
+$(MAKE) -C app cleanmake -j8 # parallel across and within subdirectories make TARGET=test # forward a goal to every subdirectory make -k # keep going, build every subdirectory that can
$(TARGET) forwarding beats writing one pass-through target per goal, and it
stays correct when a subdirectory does not define the goal only if you add a
catch-all there.