The Makefile for a one-binary C project, explained piece by piece.

CC     = gcc
CFLAGS = -std=gnu23 -Og -g3 -Wall -Werror -Wextra -Wwrite-strings
OUT    = out
BIN    = $(OUT)/tinybox

all: $(BIN)

# Any out/<name> from <name>.c, e.g. `make out/scratch`
$(OUT)/%: %.c Makefile | $(OUT)
	$(CC) $(CFLAGS) -o $@ $<

$(OUT):
	mkdir -p $@

tinybox: $(BIN)
scratch: $(OUT)/scratch

run: $(BIN)
	./$(BIN)

clean:
	rm -rf $(OUT)

.PHONY: all run clean tinybox scratch

Rules

target: prerequisites
	recipe
  • make rebuilds target if it doesn’t exist or any prerequisite is newer (by file modification time). That’s the whole algorithm.
  • The recipe lines must start with a tab. Spaces give *** missing separator. Stop.
  • A rule with no recipe (tinybox: $(BIN)) just says “to make this, make that”.

Variables

CC, CFLAGS are conventional names (make’s built-in rules use them too). Use with $(NAME). Override from the command line: make CFLAGS=-O2.

Automatic variables

Inside a recipe:

  • $@: the target (out/tinybox)
  • $<: the first prerequisite (tinybox.c)
  • $^: all prerequisites

The default target, and why it’s called all

Plain make builds the first target in the file, whatever its name. Calling it all is a convention (GNU Coding Standards), not a requirement.

Pattern rules

$(OUT)/%: %.c means “out/<anything> is built from <anything>.c”. One rule covers out/tinybox, out/scratch, and any future file. Some IDEs (e.g. CLion) only list named targets, which is why tinybox: and scratch: exist as aliases.

The Makefile as a prerequisite

If only tinybox.c is listed, changing CFLAGS doesn’t trigger a rebuild: make says Nothing to be done and your new warning flag silently does nothing. Listing Makefile fixes that. ($< still picks tinybox.c, since it’s first.)

Order-only prerequisites: | $(OUT)

Everything after | must exist before the recipe runs, but its timestamp is ignored. That matters for directories: a directory’s mtime changes whenever a file inside it is added or removed, so as a normal prerequisite it would make targets look out of date constantly.

.PHONY

Marks targets that are names, not files. Without it:

  • A file called clean in the directory would make make clean think it’s up to date.
  • make goes looking for built-in rules. It has one for foo from foo.c, so without .PHONY, make tinybox would build out/tinybox and then run gcc $(CFLAGS) tinybox.c out/tinybox -o tinybox, building a stray ./tinybox and feeding the binary in as an extra input. Phony targets skip the built-in rule search.

(Fun fact: with no Makefile at all, make scratch works: the built-in rule runs cc scratch.c -o scratch.)

Handy flags

  • make -n: dry run, print the commands without running them
  • make -B: rebuild everything regardless of timestamps
  • make -p: dump the database, including all built-in rules

References: info make (the GNU make manual), especially “Rule Syntax”, “Pattern Rules”, “Automatic Variables”, “Phony Targets”, “Prerequisite Types”.