GNU Make 四十八年不过时:发展历程、核心用法与现代坐标系
一篇带你从零到工业级的 Make 入门。文中所有示例均在 GNU Make 4.3 下实际运行验证。
一、它为什么诞生(发展历程)
- 1977,贝尔实验室的 Stuart Feldman 在一个周五下午修了个 bug,周一发现程序还是老行为——因为忘记重编译。他把"依赖该谁管"这个反人类的问题写成了工具,就是 make。
- 1988,Richard Stallman 重写成 GNU Make,后来进入 POSIX 标准。
- 同时代还分裂出几个近亲:BSD make(pmake)、微软 nmake;后来的 CMake 严格说只是生成器,最终产物往往还是 Makefile。
- 心智模型从没变过:把构建描述成一张依赖图(DAG),哪个文件比它的依赖旧,就重做谁。这是声明式的胜利——你描述"是什么",不描述"怎么跑"。
一句话总结它的历史地位:CI/CD 概念出现之前几十年,增量构建问题的第一个解,至今没被淘汰。
二、核心心智模型(三条就够)
目标(target): 依赖(prerequisites)
配方(recipe) ← 必须是 TAB 缩进
- 你说目标和依赖,make 自己算"要不要做"——比较修改时间
- 依赖本身也可以是目标 → 自动递归成一张图
- 配方每行是独立的一个 shell(上一行的
cd到下一行就丢了)
三、五个渐进例子(全部实测)
① 最原始形态
app: main.c
gcc -o app main.c
make → 只在 main.c 比 app 新时才编译。你已经解决 1977 年那个周五下午的 bug 了。
② 变量 + 自动变量
CC := gcc
CFLAGS := -O2 -Wall
app: main.o utils.o
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
五个自动变量背下来,够用 90%:
| 符号 | 含义 | 记法 |
|---|---|---|
$@ |
目标 | at = 结果 |
$< |
第一个依赖 | 输入 |
$^ |
全部依赖(去重) | 全家 |
$? |
比目标新的依赖 | 增量 |
③ 模式规则 + 通配(生产形态)
TARGET := app
SRCS := $(wildcard *.c) # 自动收集源文件
OBJS := $(SRCS:.c=.o) # 替换后缀
$(TARGET): $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
实测输出:
gcc -O2 -Wall -c main.c -o main.o
gcc -o app main.o
hello make
再往目录里加一个 utils.c,Makefile 一个字不用改。
④ 伪目标(.PHONY 是保命符)
.PHONY: clean all test
all: app
test: app
./app --self-test
clean:
rm -f app $(OBJS)
不写 .PHONY,目录里若碰巧有个叫 clean 的文件,make clean 永远不执行——新手第一大坑。
⑤ 头文件依赖自动追踪(进阶)
C 项目真正的痛点:main.c 包含 utils.h,改了 .h 要不要重编?手写依赖必漏。让编译器自己生成:
CFLAGS += -MMD -MP
-include $(OBJS:.o=.d)
.d 文件由 gcc 顺手生成,make 每次读它补全依赖图。这一手学会,你的 Makefile 就是工业级。
增量验证(touch main.c 后再 make):
gcc -O2 -Wall -c main.c -o main.o
gcc -o app main.o
只重编动的那个文件。
四、应用场景(不止编译)
| 场景 | 典型写法 |
|---|---|
| C/C++/内核 | Linux 内核、PostgreSQL 都是 make |
| 文档流水线 | report.pdf: report.md → pandoc |
| 数据 DAG | result.csv: raw.csv parse.py → 触发重算 |
| Docker | image: Dockerfile src/ → docker build(改了才 build) |
| 日常自动化 | 项目里 make dev / make deploy / make lint,比一坨 shell 脚本有序 |
判断标准:只要有"输入文件 → 输出文件"的映射,make 就比裸脚本强;纯"跑个动作"的场景它反而不舒服。
五、现代坐标系(2026 年该怎么选)
- CMake:跨平台 C++ 生态标配,负责生成构建文件
- Ninja:只做执行器,快,被 CMake/Chromium 采用
- Bazel / Buck:Google 系,解决"正确性"(内容哈希、远程缓存、多语言 monorepo),重
- just / task:现代任务运行器——如果只是要
make dev这种入口,语法更干净,但没有时间戳语义(每次全跑) - GNU Make:零依赖、无处不在、增量构建。脚本类任务用 just,真有文件依赖用 make
六、三个坑
- 配方必须是 TAB——空格报
missing separator,编辑器里肉眼看不出 - 每行独立 shell——
cd dir && make要写在一行,分行等于白 cd $要写成$$——配方里用 shell 变量$$FILE
七、作业
- 给自己的项目写个 Makefile:
lint、test、build三个 .PHONY 目标 - 写个
paper.pdf: paper.md的 pandoc 规则,感受"改了才编译" - 把 ⑤ 的
-MMD依赖追踪抄进 ③,改一个 .h 文件,观察它重编了谁