Makefile Cheatsheet - Targets, Variables & Build Rules
A Makefile is a dependency graph, not a script. Targets whose prerequisites are unchanged are not rebuilt, and that single property is why a tool born for C still fronts every npm script and most static site tooling. What matters is learning the rule syntax: how a pattern rule such as %.o matches filenames, what $@, $< and $^ expand to, and how make decides which target to build first. Two mistakes recur constantly. Recipe lines must start with a real Tab, and a missing Tab produces a missing separator error rather than anything resembling a clue. Targets like clean that produce no file must be declared .PHONY, otherwise an unrelated file named clean makes make consider the job already done. The sheet groups variable assignment, automatic variables, rules, pattern rules, builtin functions, conditionals and worked examples. Keep prerequisite lines honest, because one wrong dependency produces a build that quietly skips work and reports success.
Typical Use Case
For engineers maintaining a project where anyone can clone it and get a build. A Makefile makes build order, dependencies and feature switches reviewable instead of a command line that lives in one person memory. Covered: the five assignment forms, automatic variables, pattern rules, built-in functions and conditionals. Three things break in practice: when a recursive = expands, a recipe typed with spaces instead of a tab, and pattern rules that omit header prerequisites.
Variables & Assignment 6
CC = gccCC := gccCC ?= gccCFLAGS += -Wallexport PATH := /opt/bin:$(PATH)$(CC) or ${CC}Automatic Variables 7
$@$<$^$+$*$?$(@D) / $(@F)Rules & Targets 5
target: deps\n\trecipe.PHONY: clean installall: prog1 prog2clean:\n\trm -f *.o $(TARGET)install: target\n\tcp target /usr/local/binPattern Rules 5
%.o: %.c\n\t$(CC) -c $< -o $@%.o: %.c $(HDRS)$(OBJDIR)/%.o: $(SRCDIR)/%.cOBJS := $(SRCS:.c=.o)$(OBJS): obj/%.o: src/%.cBuilt-in Functions 8
$(wildcard *.c)$(patsubst %.c,%.o,$(SRCS))$(filter %.c %.h,$(SRCS))$(filter-out %.test,$(SRCS))$(strip $(string))$(shell date +%Y)$(subst from,to,text)$(foreach var,list,text)Conditionals 5
ifeq ($(OS),Windows_NT)\n...\nendififneq ($(CC),gcc)\n...\nendififdef DEBUG\nCFLAGS += -g\nendififndef RELEASE\n...\nelse\n...\nendif$(if $(DEBUG),-g,-O2)Practical Examples 6
SRCS := $(wildcard src/*.c)\nOBJS := $(SRCS:.c=.o)include config.mk.DEFAULT_GOAL := installvpath %.c src:src2make -j4make CFLAGS="-O2 -g"Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
CC = gcc | 递归赋值,变量在使用时才展开,后续修改会影响已经写过的地方 | CC = gcc |
CC := gcc | 简单赋值,定义时立即求值;关键配置推荐用它把值锁死 | CC := gcc |
CC ?= gcc | 仅当变量尚未定义时才赋值,是允许命令行或环境覆盖的标准写法 | CC ?= gcc |
CFLAGS += -Wall | 追加而非覆盖;但命令行传入的同名变量默认覆盖整个 Makefile 的追加结果 | CFLAGS += -Wall |
export PATH := /opt/bin:$(PATH) | 把变量导出给子进程(configure、测试脚本),是传递编译环境的唯一途径 | export PATH := /opt/bin:$(PATH) |
$@ | 当前规则的目标名,编译与链接命令里几乎必用 | $@ |
$< | 第一个依赖项。模式规则中它就是输入文件,与 -o $@ 是固定搭配 | $< |
$^ | 所有依赖去重后的有序列表,直接用于链接命令最常见 | $^ |
$+ | 所有依赖但不去重且保留顺序,用于同名单文件出现多次的场景 | $+ |
$* | 模式规则中的 stem(目标去后缀),或有后缀目标去掉目录后的文件名 | $* |
$? | 比目标更新的依赖列表,只在增量编译场景里用 | $? |
$(@D) / $(@F) | 目标所在目录 / 目标文件名,避免在规则里手工拆分路径 | $(@D) / $(@F) |
.PHONY: clean install | 声明伪目标;目录里存在同名文件夹时也不会被判定为已完成 | .PHONY: clean install |
all: prog1 prog2 | 多目标依赖会按序依次构建,适合把产物集中成一个入口 | all: prog1 prog2 |
%.o: %.c | 模式规则:任意 .c 生成同名 .o,recipe 行首必须是 Tab | %.o: %.c |
%.o: %.c $(HDRS) | 把头文件列为依赖,改头文件会触发重新编译,增量构建正确的关键 | %.o: %.c $(HDRS) |
$(OBJS): obj/%.o: src/%.c | 静态模式规则,源目录与目标目录不同名时用它替代通用模式规则 | $(OBJS): obj/%.o: src/%.c |
OBJS := $(SRCS:.c=.o) | 后缀替换,把源文件列表批量映射成目标文件列表 | OBJS := $(SRCS:.c=.o) |
$(wildcard *.c) | 展开为匹配到的文件名列表,不递归子目录,未匹配时返回空串 | $(wildcard *.c) |
$(patsubst %.c,%.o,$(SRCS)) | 按模式批量替换,比后缀替换更灵活 | $(patsubst %.c,%.o,$(SRCS)) |
$(filter %.c %.h,$(SRCS)) | 按模式筛选列表,滤出 .c 与 .h 文件 | $(filter %.c %.h,$(SRCS)) |
$(filter-out %.test,$(SRCS)) | 反选,从列表里剔除匹配项,常用于排除测试文件 | $(filter-out %.test,$(SRCS)) |
$(shell date +%Y) | 执行 shell 并捕获 stdout 作为字符串;每次展开都会真的执行一次 | $(shell date +%Y) |
$(subst from,to,text) | 按字面量做替换,不支持 % 通配 | $(subst from,to,text) |
$(foreach var,list,text) | 对列表每个元素展开一次 text,是批量生成规则的主力 | $(foreach var,list,text) |
$(strip $(string)) | 压缩多余空白并去掉首尾空格,用于拼接带条件的片段 | $(strip $(string)) |
ifeq ($(OS),Windows_NT) | 相等判断,用于跨平台分支;MSVC 与 MinGW 的路径差异几乎都靠它抹平 | ifeq ($(OS),Windows_NT) |
ifdef DEBUG / ifndef RELEASE | 判断变量是否被定义(只看定义不看值),控制 -g 之类调试开关 | ifdef DEBUG |
$(if $(DEBUG),-g,-O2) | 内联三元式,按变量是否为空选择编译选项,比整段 ifeq 更紧凑 | $(if $(DEBUG),-g,-O2) |
.DEFAULT_GOAL := install | 显式指定默认目标,否则 make 执行规则里出现的第一个目标 | .DEFAULT_GOAL := install |
vpath %.c src:src2 | 限定模式规则的搜索目录,让 src/ 与 src2/ 下的同名文件走同一套规则 | vpath %.c src:src2 |
include config.mk | 引入另一个 makefile 以拆分公共配置;前缀 - 可在缺失时忽略 | include config.mk |
make -j4 | 并行执行 4 个作业,编译时间显著缩短;规则若共写同一产物会竞争 | make -j4 |
make CFLAGS="-O2 -g" | 命令行赋值覆盖 Makefile 内部的同名变量,并使其后续 += 失效 | make CFLAGS="-O2 -g" |
Common pitfalls
Symptommake 报 "missing separator",或者 recipe 行被完全忽略。
Causerecipe 行首不是 Tab。多数编辑器把 Tab 显示为空格,复制粘贴也会把它换成空格。
Fix把该行行首换成真正的 Tab,并把编辑器设置成 Tab 显示为箭头便于辨认。
Symptom改了头文件却不重新编译,链接时出现 undefined reference。
Cause模式规则只写了 `%.o: %.c`,没把头文件列入依赖,make 不知道 .o 依赖 .h。
Fix规则改成 `%.o: %.c $(HDRS)`;更稳的做法是对 .c 跑 -MMD -MP 生成 .d 再 include 它。
Symptommake 直接报 "up to date",但可执行文件根本还没编译出来。
Cause目标名与某个同名目录重合,make 看到那个目录存在就认为目标已完成。
Fix把这类目标写进 .PHONY,或改一个不与目录重名的目标名。
Symptom在命令行改了 CC/CFLAGS,Makefile 内部的追加完全没有体现在最终命令里。
Cause命令行赋值优先级高于 Makefile 内的普通 `=` 与 `+=`,后者被整体忽略。
Fix需要叠加时把变量改成 `?=` 或 `override CFLAGS += ...`,并用 make -n 确认展开后的实际命令。
Symptom变量在某些位置展开成空串,echo 出来什么都没有。
Cause递归赋值 `=` 引用了尚未定义的变量;赋值与引用的先后顺序决定了展开结果。
Fix关键变量统一改用 := 立即赋值,并用 `$(info VAR=$(VAR))` 在解析阶段打印确认。
Symptom串行 make 正常,加 make -j4 之后偶发链接失败或产物损坏。
Cause并行下两个目标竞争同一中间产物,或某个目录的生成与使用之间没有依赖声明。
Fix为所有目录类目标写出显式依赖(如 `obj/%.o: src/%.c | obj`),并把一条配方管多个目标的规则拆成独立模式规则。
Symptom$(wildcard *.c) 匹配不到任何文件。
Causewildcard 相对 Makefile 所在目录展开且不递归子目录;文件都在 src/ 下时自然匹配为空。
Fix写明子目录路径或用 `$(shell find src -name '*.c')`,注意后者依赖 shell 环境。
SymptomMakefile 顶部某条 $(shell ...) 每跑一次 make 就重复执行多次,输出还被拆成多行破坏语法。
Cause$(shell) 在每次变量展开时都会重新执行;递归赋值里引用它会放大这个次数。
Fix改成 := 立即赋值让它只跑一次,并用 $(strip ...) 处理带换行的输出。
Troubleshooting
1只打印不执行,检查展开后的真实命令
make -n target这一步最能暴露 Tab、空变量与路径拼接问题。
2打印规则数据库,确认目标与依赖是否正确
make -p -n-p 列出内置规则与变量,-n 只打印不执行。
3并行构建观察依赖是否声明充分
make -j4串行通过而并行失败,基本可确定存在未声明依赖或共享中间文件。
4强制某目标无条件重建
make -B target-B 排除「时间戳没变所以不构建」的干扰。
5确认使用的 make 实现与版本
make --versionGNU make 与 BSD make 行为不同,脚本需先确认目标环境。
Command Examples
A standard C build with a pattern rule
CC := gcc
CFLAGS := -Wall -Wextra -O2
OBJS := $(SRCS:.c=.o)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
app: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^$< 是第一个依赖(源文件),$@ 是目标名(.o),$^ 是全部依赖(链接阶段)。用 := 而非 = 立即展开,值才不会在引用时才求值。
Calling a shell command from a Makefile
clean:
rm -f *.o
@echo "清理完成"
list:
@ls -1 *.o | wc -l命令行以 TAB 开头才是配方,空格会被当成普通文本。前缀 @ 抑制命令回显;不加 @ 会把 rm -f *.o 也打印一遍,CI 日志里很吵。
Output
清理完成
Find out why a target is being rebuilt
make -n app
make -d app 2>&1 | grep -i "newer than"-n 只打印不执行,用来预览会做什么;-d 输出完整决策过程,能看到 make 判定哪个依赖更新。排查「明明没改代码却总是重编」先看这条。
Output
gcc -Wall -O2 -c main.c -o main.o gcc -Wall -O2 -c util.c -o util.o
Common Pitfalls
- Recipe lines must begin with a TAB, not spaces — the most common Makefile mistake, usually reported as "missing separator".
- A target sharing a name with a real file is considered up to date; declare .PHONY to avoid it, especially for clean and install.
- With =, variables expand at the point of use, which frequently causes "I changed it and nothing happened" — prefer := for paths and includes.
- A hard-coded source list is not re-scanned, so make never notices a new file; collect with $(wildcard *.c) at parse time.
- Under parallel make -j, undeclared ordering dependencies between targets can produce incorrect results.
Tips
- Recipe lines MUST start with a Tab, not spaces - the most common mistake.
- The .PHONY target ignores a same-named file on disk; good for clean/install.
- $@ = target, $< = first prerequisite, $^ = all prerequisites - the three most-used automatic vars.
- := expands immediately, = defers; prefer := to avoid recursive side effects.
- include other.mk pulls in another Makefile, good for large projects.
- CLI variables override Makefile assignments; use override to prevent that.
FAQ
What is the difference between := and = for Makefile variables?
= expands recursively — any variables referenced inside are resolved at the point of use. := expands immediately, at the moment of definition. With CC = gcc, reassigning CC later still affects earlier references, whereas CC := gcc freezes the value on definition. Use := for anything involving paths or other variables, otherwise you get the classic confusion where a change appears to do nothing.
What is .PHONY and why does clean need it?
Make decides whether to run a recipe by checking whether the target file exists and is newer than its dependencies. If a file literally named clean happens to exist, make clean concludes the target is up to date and does nothing. Declaring .PHONY: clean tells make the target produces no file and must always run. Strictly, any target that does not create a file should be declared phony.
What do $@, $<, $^ and $* expand to?
$@ is the current target name, $< is the first prerequisite (used by compilers), $^ is all prerequisites deduplicated, $+ is the same but keeps duplicates, which matters when you deliberately link an object file twice. $* is what % matched in a pattern rule. The classic %.o: %.c rule with $(CC) -c $< -o $@ is built entirely from these.
Why does make skip my newly added source file?
Make resolves prerequisites by name, so a hard-coded list of src/*.o will never pick up a new file automatically. Replace the fixed list with $(wildcard src/*.c) to collect files at parse time, then derive targets with $(SRCS:.c=.o), or declare targets explicitly with a static pattern rule. Also keep variable definitions ahead of the rules that consume them.
How do I choose the parallelism for make -j?
Match it to the core count — make -j4 saturates a four-core machine. Parallelism requires accurate dependency declarations: if targets have an implicit order you did not express, parallel builds will produce wrong results. Do not oversubscribe either, since linking is memory hungry and far more jobs than cores can fail from exhaustion.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 19, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us