Make & Makefiles Cheat Sheet
GNU Make syntax for defining build targets, dependencies, variables, and phony tasks used to automate builds and workflows.
Basic Syntax
Targets, prerequisites, and recipes.
# target: prerequisites#\trecipe (must be indented with a TAB, not spaces)app: main.o utils.o gcc -o app main.o utils.omain.o: main.c gcc -c main.cutils.o: utils.c gcc -c utils.cclean: rm -f *.o app
Phony Targets & Variables
Common conventions for non-file targets.
.PHONY: build test cleanCC := gccCFLAGS := -Wall -O2SRC := $(wildcard *.c)OBJ := $(SRC:.c=.o)build: $(OBJ) $(CC) $(CFLAGS) -o app $(OBJ)test: build ./run_tests.shclean: rm -f $(OBJ) app
Automatic Variables
Special variables available inside recipes.
- $@- The name of the current target
- $<- The first prerequisite
- $^- All prerequisites, space-separated, duplicates removed
- $?- Prerequisites newer than the target
- %.o: %.c- Pattern rule: builds any .o from a matching .c file
- .DEFAULT_GOAL- Overrides which target runs when 'make' is called with no arguments
Real-World Example
A typical Node/Docker project Makefile.
.PHONY: install build docker up downinstall: npm cibuild: install npm run builddocker: docker build -t myapp:latest .up: docker compose up -ddown: docker compose down
Built-in Functions & Text Manipulation
Make's function calls for transforming variables at expansion time.
SRCS := $(shell find src -name '*.c')HDRS := $(wildcard include/*.h)# String substitutionOBJS := $(patsubst src/%.c,build/%.o,$(SRCS))# FilteringTESTS := $(filter %_test.c,$(SRCS))NONTESTS := $(filter-out %_test.c,$(SRCS))# Conditional assignment (only if unset)CC ?= gcc# String opsUPPER := $(shell echo $(NAME) | tr a-z A-Z)JOINED := $(subst /,_,$(SRCS))$(info Building $(words $(SRCS)) source files)
Conditionals, Includes & Recursion
Composing multi-directory builds and environment-aware logic.
ifeq ($(OS),Windows_NT) RM := del /Qelse RM := rm -fendififdef DEBUG CFLAGS += -g -O0else CFLAGS += -O2endif# Pull in generated dependency files (auto header tracking)-include $(OBJS:.o=.d)# Recurse into subdirectoriesSUBDIRS := lib app.PHONY: $(SUBDIRS)$(SUBDIRS): $(MAKE) -C $@all: $(SUBDIRS)
Automatic Header Dependency Tracking
Generate .d files with the compiler so editing a header rebuilds every .o that includes it.
CFLAGS += -MMD -MPOBJS := $(SRCS:.c=.o)DEPS := $(OBJS:.o=.d)%.o: %.c $(CC) $(CFLAGS) -c $< -o $@-include $(DEPS)clean: rm -f $(OBJS) $(DEPS)
Gotchas & Semantics
Behaviors that trip up people who learned Makefiles by copy-paste.
- = vs := vs ?= vs +=- `=` is recursively (lazily) expanded on every use; `:=` is expanded once immediately; `?=` sets only if unset; `+=` appends, inheriting the parent's flavor
- Order-only prerequisites- `target: normal-deps | order-only-deps` — order-only deps (e.g. a directory) trigger creation but never force a rebuild by themselves
- .SECONDARY / .PRECIOUS- Prevent Make from deleting intermediate files it auto-generated during a chained pattern-rule build
- Parallel builds (-j)- `make -j8` runs independent targets concurrently; unsafe unless prerequisites are declared correctly, since Make assumes the DAG is complete
- Recipe shell per line- Each recipe line runs in its own subshell by default; use a trailing `\` or `.ONESHELL:` to keep `cd`/variable state across lines
- $(MAKE) vs plain recursive call- Always invoke sub-makes via `$(MAKE)`, never a literal `make`, so `-j` and `-n` flags propagate correctly (the '+' recipe prefix also works)
.ONESHELL and eval Metaprogramming
Advanced directives for shell-like recipes and generating rules programmatically.
.ONESHELL:SHELL := /bin/bash.SHELLFLAGS := -eu -o pipefail -cdeploy: cd build tar czf ../release.tar.gz . scp ../release.tar.gz user@host:/srv/releases/# Generate a rule per service using eval + callSERVICES := api worker webdefine BUILD_RULEbuild-$(1): docker build -t myorg/$(1):latest ./$(1)endef$(foreach svc,$(SERVICES),$(eval $(call BUILD_RULE,$(svc))))build-all: $(addprefix build-,$(SERVICES))
Always declare non-file targets like 'clean' and 'test' under .PHONY — without it, Make will skip the recipe if a file literally named 'clean' ever exists in the directory.