GNU Make 四十八年不过时:发展历程、核心用法与现代坐标系

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 缩进  
  1. 你说目标和依赖,make 自己算"要不要做"——比较修改时间
  2. 依赖本身也可以是目标 → 自动递归成一张图
  3. 配方每行是独立的一个 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

六、三个坑

  1. 配方必须是 TAB——空格报 missing separator,编辑器里肉眼看不出
  2. 每行独立 shell——cd dir && make 要写在一行,分行等于白 cd
  3. $ 要写成 $$——配方里用 shell 变量 $$FILE

七、作业

  1. 给自己的项目写个 Makefile:lint、test、build 三个 .PHONY 目标
  2. 写个 paper.pdf: paper.md 的 pandoc 规则,感受"改了才编译"
  3. 把 ⑤ 的 -MMD 依赖追踪抄进 ③,改一个 .h 文件,观察它重编了谁