Understanding Make

August 1, 2026 · visitors

make shows up in almost every embedded and systems project I've touched — Yocto recipes, kernel builds, U-Boot, small firmware repos. It's also one of those tools people use for years by imitation, copying a Makefile from the last project without ever quite pinning down the model underneath it. Here's that model, laid out directly.

What is Make?

Make is a build automation tool: it reads a file (a Makefile) describing a set of targets, the prerequisites each target depends on, and the recipe (shell commands) needed to produce that target from its prerequisites. Given that description, make figures out which targets are out of date and runs only the recipes needed to bring them up to date — nothing more.

main: main.o utils.o
	gcc -o main main.o utils.o

main.o: main.c
	gcc -c main.c

Run make, and it builds main.o only if main.c is newer than main.o (or main.o doesn't exist yet), then links main only if main.o or utils.o changed. Nothing is rebuilt unnecessarily.

Who is it for?

Make was written by Stuart Feldman at Bell Labs in April 1976. By his own account, the trigger was a colleague — Steve Johnson, author of yacc — losing a morning debugging a program that was, in fact, correct; the binary simply hadn't been relinked after a source change. Feldman received the 2003 ACM Software System Award for it. The tool that came out of that afternoon — track dependencies once, let the machine figure out what needs redoing — became one of the most widely copied ideas in software tooling. GNU Make, the version almost everyone uses today, is a free reimplementation from the GNU Project (1) (5) (6).

In practice, it's for anyone assembling a non-trivial artifact — a compiled binary, a document, a set of generated files — from smaller pieces, where rebuilding everything from scratch on every change would be wasteful or slow. That covers C/C++ projects, kernel and bootloader builds, LaTeX documents, and plenty of ad-hoc automation that has nothing to do with compiling code at all.

When should you reach for it?

Make earns its keep when three things are true at once:

For a handful of files with no interdependencies, a shell script is often simpler and more honest. Make starts paying off once "did this actually need rebuilding?" becomes a question worth asking automatically instead of by hand.

Where does it sit in the toolchain?

Make usually sits one layer above the compiler and one layer below whatever a human runs. In a typical C project: source files → (compiler, driven by Make) → object files → (linker, driven by Make) → final binary. Make itself doesn't compile or link anything — it invokes the tools that do, as shell commands in each recipe.

It also tends to sit underneath higher-level build-configuration tools. CMake, Autotools, and Yocto's BitBake, for instance, don't replace Make so much as generate Makefiles (or drive an equivalent) as their actual execution backend. So even projects that never show you a Makefile directly are often running one somewhere in the pipeline.

Why does it work the way it does?

The core design choice — comparing file modification times to decide what's stale — is what makes Make fast on large trees: checking a timestamp is cheap, so a rebuild of a huge project after a one-line change can skip almost everything and still be correct, provided the dependency graph given to it is accurate.

That last clause is the source of almost every "Make is broken" complaint people run into, and it's worth sitting with: Make is only ever as correct as the graph it's told about. If a .c file #includes a header that isn't listed as a prerequisite of the corresponding .o file, editing that header won't trigger a rebuild — not because Make is confused, but because nothing ever told it that dependency existed. This is why real-world Makefiles almost always pair with a compiler-generated dependency step (-MMD -MP with GCC/Clang) rather than hand-written header lists — the compiler already knows the true include graph, so it's the only reliable source for it (4).

How does it actually work?

Targets, prerequisites, recipes. The basic unit is:

target: prerequisite1 prerequisite2
	recipe line 1
	recipe line 2

Recipe lines must be indented with a tab, not spaces — a famous historical wart that still trips people up today.

Automatic variables cut down on repetition:

%.o: %.c
	gcc -c $< -o $@

$@ is the target, $< is the first prerequisite — this pattern rule alone can build every .o from its matching .c file without one line per file.

.PHONY targets mark names that don't correspond to real files — clean, all, test — so Make doesn't get confused if a file with that name happens to exist, and always runs the recipe rather than checking timestamps: (2)

.PHONY: clean
clean:
	rm -f *.o main

Variables avoid repeating flags everywhere and make cross-project reuse easier:

CC = gcc
CFLAGS = -Wall -O2

main.o: main.c
	$(CC) $(CFLAGS) -c main.c

Automatic dependency generation closes the gap described above:

CFLAGS = -Wall -O2 -MMD -MP
-include $(wildcard *.d)

%.o: %.c
	$(CC) $(CFLAGS) -c $<

-MMD makes the compiler emit a .d file per object, listing every header actually pulled in during that compile. -include folds those files back into the Makefile so the dependency graph stays accurate as headers change — without anyone maintaining it by hand (3).

Closing thought

Most of what looks like Make's quirks — the timestamp comparisons, the insistence on tabs, the way a missing dependency fails silently instead of loudly — falls out of one design decision made in 1976: keep the model simple and the check cheap, and trust the Makefile to describe reality accurately. Once that's the lens, most "Make is being weird" moments turn out to be "the graph I gave it was incomplete" — which is a much easier thing to fix.

References

  1. Feldman, S. I. (1979). Make — A Program for Maintaining Computer Programs. Software: Practice and Experience, 9(4), 255–265. — Feldman's own paper introducing Make, including the original motivation.
  2. GNU Make Manual — the complete, authoritative reference for GNU Make's syntax and semantics.
  3. GNU Make Manual: Automatic Prerequisites — the -MMD/-include pattern in the maintainers' own words.
  4. GCC: Preprocessor Options (-M family) — what -MMD and -MP do under the hood.
  5. Make (software) — Wikipedia — general history and lineage of Make implementations (BSD Make, GNU Make, NMAKE, etc.).
  6. Stuart Feldman — Wikipedia — background on Feldman's work at Bell Labs and the 2003 ACM Software System Award.

Comments