2、规则与目标:深入理解 Makefile 的规则结构
Makefile 的核心,说白了就是一条规则。你写一百行、一千行的 Makefile,拆开来看,全是规则的堆叠。我刚开始学的时候,总觉得这东西很玄乎,后来发现——嗯,其实就是告诉 make:「你要做什么,用什么做,怎么做。」
2.1 规则的基本三要素
一条完整的 Makefile 规则,长这样:
目标: 依赖
命令
三部分缺一不可(虽然命令可以空着,但冒号必须要有)。我习惯把这三部分叫做「目标、依赖、命令」,记起来很简单。
| 要素 | 含义 | 举个栗子 |
|---|---|---|
| 目标(target) | 你要生成的文件,或者你要执行的动作名 | main.o、clean |
| 依赖(prerequisites) | 生成目标之前,必须先有的文件或目标 | main.c、defs.h |
| 命令(recipe) | 如何从依赖生成目标的具体 shell 指令 | gcc -c main.c |
关键理解:Make 会检查「目标」和「依赖」的时间戳。如果依赖比目标新,或者目标不存在,就执行命令。否则跳过。这就是增量编译的根基。
2.2 依赖链:Make 是怎么知道该编译什么的?
你想想看,一个项目几十个源文件,难道每次改一个 .c 文件,就要全部重新编译?当然不是。Make 通过依赖链,只编译受影响的部分。
举个例子:
app: main.o utils.o
gcc -o app main.o utils.o
main.o: main.c defs.h
gcc -c main.c
utils.o: utils.c defs.h
gcc -c utils.c
这时候如果你改了 defs.h,Make 会发现 main.o 和 utils.o 都依赖它,两个 .o 都比 .h 旧,于是重新编译两个 .o,再链接成 app。如果你只改了 main.c,那就只编译 main.o,utils.o 不动。
我的经验:依赖写全了,编译速度能快好几倍。我见过有人把所有 .c 文件都列在一个规则里,改一个文件就全量编译——那还不如写个 shell 脚本呢。
2.3 伪目标 .PHONY:别把「动作」当成「文件」
这里有个坑,我当年踩过。你写一个 clean 目标:
clean:
rm -f *.o app
看起来没问题对吧?但如果你的目录下恰好有一个叫 clean 的文件……那 Make 就会认为 clean 这个目标已经是最新的了,什么都不执行。
为什么会这样?因为 Make 默认把目标当成文件名来处理。它检查到 clean 文件存在,而且没有依赖,就认为「嗯,这个目标已经完成了,跳过」。
解决办法就是用 .PHONY 声明伪目标:
.PHONY: clean
clean:
rm -f *.o app
告诉 Make:「clean 不是一个文件,别拿时间戳来比,每次都给我执行命令。」
| 场景 | 不加 .PHONY | 加 .PHONY |
|---|---|---|
| 目录下有同名文件 | 跳过,不执行 | 正常执行 |
| 目录下无同名文件 | 正常执行 | 正常执行 |
| 多次调用 | 只执行一次 | 每次都执行 |
我曾经……在一个项目里忘了加 .PHONY,结果 clean 目标死活不执行。查了半天,发现 build 目录下有个叫 clean 的临时文件。从那以后,我所有「动作类」目标(clean、all、test、install)一律加上 .PHONY。
2.4 多目标与自动变量
有时候多个目标共享同一套依赖和命令。比如你要生成 a.o 和 b.o,规则可以写成:
a.o b.o: defs.h
echo "编译中..."
但这样写,命令里的目标名怎么区分?这时候就要用自动变量了:
$@—— 当前目标名$<—— 第一个依赖$^—— 所有依赖(去重)
看个实际例子:
a.o b.o: defs.h
gcc -c $< -o $@
当 Make 处理 a.o 时,$@ 就是 a.o,$< 是 defs.h。处理 b.o 时同理。这样一条规则就能搞定多个目标。
我建议:刚开始用自动变量时,可以在命令里加一行 @echo 目标: $@, 依赖: $^ 来调试,看看 Make 到底传了什么值进去。
2.5 规则结构全景图
下面这张图,把规则的核心结构、依赖链、以及 .PHONY 的位置都串起来了:
2.6 实战中的几个小习惯
最后分享几个我自己的习惯,不一定适合所有人,但你可以参考:
- 第一个目标写成 all —— 默认执行 all,里面列出所有要构建的最终目标。这样别人执行
make就知道要编译什么。 - 每个「动作」目标都加 .PHONY —— clean、install、test、distclean,一个不落。省得以后踩坑。
- 依赖写细一点 —— 别偷懒只写 .c 文件,头文件也列上。虽然手动维护麻烦,但可以后面用自动生成依赖的工具(gcc -MM)来搞定。
- 命令前面加 @ —— 比如
@echo "编译完成",这样不会把命令本身打印出来,输出更干净。
一句话总结:规则就是 Makefile 的细胞。目标、依赖、命令三要素搞清楚了,.PHONY 用对了,依赖链理清了,你的 Makefile 就成功了一半。