GNU Make
Idiomatic Makefile patterns for C/C++ projects: phony targets, pattern rules, automatic dependency generation, and incremental-build hygiene. Grounded against GNU Make 4.4.
Contract
| Field | Bound contract |
|---|---|
| Trigger | The task writes or debugs a Makefile, sets up pattern rules or automatic dependency generation, manages CFLAGS/LDFLAGS, converts ad-hoc compile commands into a Makefile, or diagnoses incremental build failures. |
| Authority | Reversible local: writes only Makefile/*.mk files and local build outputs; rollback is version control plus make clean. No remote mutation. |
| Side effect | make runs the compiler and writes objects and binaries into the tree or build directory. |
| Done | make builds the target incrementally (a second run reports nothing to do) and make clean removes the outputs. |
Inputs
- Source layout (required): where
.c/.cppfiles and headers live. - Compiler and flags (required if not inferrable):
CC,CFLAGS,LDFLAGS,LDLIBS. - Build variants (optional): debug/release split, install prefix.
Procedure
- Write the minimal correct Makefile. Recipes start with a tab, not spaces. Done when:
makebuilds andmake cleanremoves outputs.
CC := gcc
CFLAGS := -std=c23 -Wall -Wextra -g -O2
LDFLAGS :=
LDLIBS :=
SRCS := $(wildcard src/*.c)
OBJS := $(SRCS:src/%.c=build/%.o)
TARGET := build/prog
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c -o $@ $<
build:
mkdir -p build
clean:
rm -rf build
Automatic variables: $@ is the target, $< the first prerequisite, $^ all prerequisites deduplicated, $* the pattern stem, $(@D) the target's directory part.
- Add automatic dependency generation. Without it, editing a header rebuilds nothing.
-MMDwrites a.dfile per object;-MPadds phony targets for headers so deleting one does not break the build. Done when: touching a header rebuilds exactly its dependents.
DEPFLAGS = -MMD -MP
DEPS := $(OBJS:.o=.d)
build/%.o: src/%.c | build
$(CC) $(CFLAGS) $(DEPFLAGS) -MF $(@:.o=.d) -c -o $@ $<
-include $(DEPS) # leading - ignores missing .d files on first build
- Use pattern rules for repeated transforms. Done when: one rule covers each source-to-output shape.
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c -o $@ $<
%.s: %.c
$(CC) $(CFLAGS) -S -o $@ $<
- Add build variants and quiet output. Done when:
make BUILD=debugswitches flags andmake V=1shows full commands.
BUILD ?= release
ifeq ($(BUILD),debug)
CFLAGS += -g -Og -DDEBUG
else
CFLAGS += -O2 -DNDEBUG
endif
Q := $(if $(V),,@)
build/%.o: src/%.c | build
@echo " CC $<"
$(Q)$(CC) $(CFLAGS) -c -o $@ $<
- Add an install target with
DESTDIRandPREFIX. Done when:make install DESTDIR=/tmp/stagestages the tree.
PREFIX ?= /usr/local
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin
install -m 0755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/
- Structure multi-directory projects with included
.mkfiles, not recursive make. Recursive make fragments the dependency graph and breaks parallelism. Done when: one top-level Makefile sees every prerequisite.
# Makefile
include lib/module.mk
include src/app.mk
# lib/module.mk
LIB_SRCS := $(wildcard lib/*.c)
LIB_OBJS := $(LIB_SRCS:lib/%.c=build/lib_%.o)
OBJS += $(LIB_OBJS)
build/lib_%.o: lib/%.c
$(CC) $(CFLAGS) -c -o $@ $<
- Diagnose failures against this table. Done when: the error maps to a cause and a fix.
| Error | Cause | Fix |
|---|---|---|
No rule to make target 'foo.o' |
Missing source or rule | Check the source path and pattern rule |
Nothing to be done for 'all' |
Targets up to date | Touch a source or run make clean |
Circular dependency dropped |
Target depends on itself | Trace the prerequisite chain |
missing separator |
Spaces where a tab belongs | Recipes need a literal tab |
multiple target patterns |
Bad pattern rule syntax | Check % placement |
| Rebuilds everything | Wrong timestamps or missing .PHONY |
Check date; mark all and clean phony |
| Header edit rebuilds nothing | No dependency tracking | Add -MMD -MP and -include $(DEPS) |
Useful flags: make -j"$(nproc)" parallel, make -O synchronizes output, make -n dry run, make -B force rebuild, make -k keep going, make -p print the rule database, make --warn-undefined-variables catch typos.
For the full variable, function, and special-target reference see references/cheatsheet.md.
Failure and recovery
makedoes nothing after a header edit: dependency files are missing; add-MMD -MPand-include $(DEPS).- Parallel build races: a missing order-only prerequisite or an undeclared generated header; declare the real dependency, never serialize to hide it.
missing separator: convert the recipe's leading spaces to a tab.- Variable expands empty:
=defers expansion to use time,:=expands at definition; pick:=unless late expansion is the point.::=is the POSIX immediate form, available in GNU Make 4.4 and later. - Recursive make is already present: include the child
.mkfiles from the top Makefile instead of$(MAKE) -C.
Output
A Makefile that builds incrementally, tracks header dependencies, supports clean and install, and survives make -j. For debugging tasks, the diagnosed cause and the minimal fix.