Make Cheatsheet

Parallelism

Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

-j, and choosing a number

-j N runs up to N recipes at once. -j with no number means unlimited, which will fork-bomb a large tree.

make -j8
make -j$(nproc)                      # Linux
make -j$(sysctl -n hw.ncpu)          # macOS and BSD
make -j$(getconf _NPROCESSORS_ONLN)  # portable
make -j$(nproc) -l$(nproc)           # cap by load average too
FlagEffect
-j Nup to N jobs
-junlimited jobs, avoid
-l N --load-average=Ndo not start a new job while load ≥ N
-O --output-sync=targetbuffer each target's output, print it whole
-O line -O recurse -O noneother output-sync granularities
-kkeep going after a failure, build everything that still can
-iignore all errors, rarely what you want

Set a default in the makefile with MAKEFLAGS += -j8, but be aware that it applies to every invocation including make clean, and that a -j from the command line overrides it.

-j only helps when the dependency graph is wide. If make -j16 runs one job at a time, the graph is a chain, and no flag fixes that. Look for a .PHONY target that everything depends on, or a sub-make serialization.

Interleaved output, and -O

Parallel recipes write to the same terminal, so lines from different jobs interleave and a compiler error can end up split across two other jobs' output.

$ make -j4
b start
c done
b done
a done
$ make -j4 -O
c done
b start
b done
a done

-O (short for -Otarget) buffers a target's entire output and flushes it as one block when the target finishes, which is what you want in CI. Costs: nothing appears until a target completes, so a long compile looks hung, and a recipe that prompts for input will hang for real.

What breaks under -j

Serial makefiles hide bugs that -j exposes immediately. Every one of these is a missing dependency edge.

SymptomCause
"No such file or directory" for a file another rule createsmissing prerequisite, works serially by luck of ordering
Two jobs writing the same filetwo rules produce one output, or a shared temp name
mkdir: File existsseveral rules mkdir the same directory
Random failures that vanish on rerunan implicit ordering the graph does not express
A generated header compiled before it existsthe header is not a prerequisite of the objects

Fixes, in order of preference:

# 1. Declare the real edge
build/main.o: generated/version.h

# 2. Order-only prerequisite for a directory
build/%.o: src/%.c | build
build:
	mkdir -p $@

# 3. mkdir -p is idempotent, so this is safe in a parallel recipe
build/%.o: src/%.c
	@mkdir -p $(@D)

# 4. Unique temp names, never a fixed one
%.o: %.c
	$(CC) -c -o $@.tmp$$$$ $< && mv $@.tmp$$$$ $@

# 5. A rule that really does produce several files at once, GNU 4.3+
parser.c parser.h &: parser.y
	bison -d -o parser.c $<

Before &: existed, the portable form for a multi-output rule was a sentinel file that both outputs depend on, which is why old makefiles have .stamp targets.

.NOTPARALLEL and .WAIT

.NOTPARALLEL forces the entire makefile to run serially, whatever -j says.

.NOTPARALLEL:
$ make -f np.mk -j4
a done
b start
b done
c done

GNU make 4.4 lets .NOTPARALLEL take prerequisites, so only those targets' prerequisites are serialized. It is a blunt instrument, and reaching for it usually means an unexpressed dependency. Fix the graph instead.

.WAIT, also GNU 4.4, is the surgical version. Put it in a prerequisite list and everything before it finishes before anything after it starts.

all: schema .WAIT migrate .WAIT seed
$ make -f w.mk -j4
a done
b done
c done

.WAIT is a real target name, so older makes see it as a missing file and fail. Guard it on $(MAKE_VERSION) if the makefile must run on make 4.3 or older.

Recursive make under -j

The job server is what keeps N sub-makes from each starting N jobs.

- Prefix any sub-make invocation with + when the line does not literally contain $(MAKE), so make marks it as recursive and passes the job server along in MAKEFLAGS. - Never put -j inside a recipe. Pass it once on the outer command line. - warning: jobserver unavailable: using -j1 on make 4.3 and older means a sub-make missed the handoff. GNU make 4.4's FIFO-based job server removed most of these. - Sub-directory targets must be .PHONY, or a directory's timestamp will make make skip the whole sub-build.

.PHONY: $(SUBDIRS)
$(SUBDIRS):
	+$(MAKE) -C $@

Measuring whether it helped

time make -j1 && make clean
time make -j8

make -j8 --debug=j 2>&1 | grep -c 'Putting child'   # jobs actually launched
make -n | wc -l                                     # recipe lines in the plan
make -d 2>&1 | grep 'Must remake target'            # what is being rebuilt

Two structural checks worth running once on any slow build:

# a serial no-op build should be fast. If it is not, the graph is the problem
time make                        # after a full build, expect under a second

# find the long pole: --trace names each target right before its recipe
make clean && make -j1 --trace

Practical ceilings: link steps are memory-bound and often the real limit, so -j$(nproc) with 2 GB per link will swap. Compilation scales close to linearly up to core count, and beyond core count -j only adds contention. On CI containers nproc reports the host's cores rather than the cgroup quota, so pin -j to the runner's advertised CPU count instead.