2. Makefile 基础规则:语法、依赖与 Tab 缩进
好,咱们正式开始写 Makefile。这一章是整个课程的地基,你把它吃透了,后面多目录、多目标、条件编译什么的,学起来会非常顺。
Makefile 的规则,说白了就三样东西:目标(target)、依赖(prerequisites)、命令(recipe)。我见过不少新手,上来就抄网上的模板,结果编译报错“missing separator”,一脸懵。其实只要搞懂这三者的关系,再记住一个关键点——Tab 缩进,就没什么难的。
2.1 规则的基本语法
一条 Makefile 规则长这样:
目标: 依赖1 依赖2 ...
<Tab>命令1
<Tab>命令2
注意,<Tab> 不是空格,是实实在在的 Tab 键。我当年第一次写 Makefile,用空格缩进,结果 make 直接报错,查了半天才发现问题。嗯,这个坑几乎每个人都会踩一次。
规则的含义很简单:如果“依赖”比“目标”新,或者“目标”不存在,就执行“命令”来更新“目标”。这就是 Make 的核心逻辑——增量构建。
核心逻辑:Make 通过比较文件和依赖的时间戳,决定哪些需要重新编译。只有发生变化的文件才会触发重新构建,这就是为什么大型项目用 Make 能节省大量时间。
2.2 目标与依赖的写法
目标可以是最终的可执行文件,也可以是中间文件(比如 .o 文件),甚至可以是“伪目标”(比如 clean、all)。依赖可以是文件,也可以是其他目标。
咱们看个最简单的例子:
hello: hello.o
gcc -o hello hello.o
hello.o: hello.c
gcc -c hello.c -o hello.o
clean:
rm -f hello hello.o
这里 hello 是最终目标,它依赖 hello.o;hello.o 又依赖 hello.c。你执行 make 时,Make 会先检查 hello.o 是否需要更新,如果需要,就编译 hello.c;然后再链接生成 hello。
我个人习惯把最终目标写在最前面,这样执行 make 时默认构建它。至于 clean,它是个伪目标——不生成文件,只执行清理命令。
小技巧:伪目标最好用 .PHONY 声明一下,避免目录下恰好有个叫 clean 的文件导致 Make 误判。写法是:.PHONY: clean。
2.3 命令行的 Tab 缩进要求
这是 Makefile 最“矫情”的地方。命令必须以 Tab 字符开头,不能是空格。如果你用空格缩进,Make 会报:
Makefile:3: *** missing separator. Stop.
为什么会这样?因为 Make 用 Tab 来区分“这是命令”和“这是其他内容”。空格在 Make 眼里只是普通文本,不是缩进标记。
我曾经在一个项目里,同事用编辑器自动把 Tab 替换成了 4 个空格,结果整个 Makefile 全崩了。排查了半小时才发现是缩进问题。从那以后,我养成了一个习惯:写 Makefile 前,先确认编辑器设置。
| 编辑器 | 设置方法 |
|---|---|
| Vim | :set noexpandtab 或 :set tabstop=4 |
| VS Code | 设置 "editor.insertSpaces": false |
| Sublime Text | 设置 "translate_tabs_to_spaces": false |
| CLion / IDEA | 设置 Use tab character 为 true |
警告:不要在 Makefile 里混用 Tab 和空格。一旦混用,Make 会报“missing separator”或“recipe commences before first target”。如果你从网页复制代码,记得检查缩进——网页经常把 Tab 转成空格。
2.4 规则的执行流程
咱们用一张图来理解 Make 是怎么执行规则的:
你看,Make 会递归检查依赖链。如果 hello.o 比 hello.c 旧,就重新编译;如果 hello 比 hello.o 旧,就重新链接。只有真正需要更新的目标才会执行命令,这就是增量构建的精髓。
2.5 多目标与多依赖
一个规则可以有多个目标,也可以有多个依赖。比如:
main.o utils.o: config.h
# 这里不会同时编译两个 .o 文件,只是声明依赖关系
这条规则的意思是:main.o 和 utils.o 都依赖 config.h。如果 config.h 变了,这两个 .o 文件都会被标记为“需要更新”。但注意,这条规则没有命令,它只是声明依赖关系。真正的编译命令在其他规则里。
我建议你把依赖声明和编译命令分开写,这样更清晰:
main.o: main.c config.h
gcc -c main.c -o main.o
utils.o: utils.c config.h
gcc -c utils.c -o utils.o
这样每个 .o 文件都有自己的编译命令,依赖关系一目了然。
2.6 常见错误与避坑指南
- missing separator:命令前用了空格而不是 Tab。检查编辑器设置,确保 Tab 键输出的是 Tab 字符。
- No rule to make target:依赖文件不存在,或者目标拼写错误。检查文件名和路径。
- Circular dependency:目标依赖了自己,或者形成了依赖环。Make 会报错并停止。
- Nothing to be done:所有依赖都比目标新,没有需要更新的内容。这不是错误,是正常行为。
调试技巧:执行 make -d 可以打印详细的调试信息,包括 Make 检查了哪些文件、比较了哪些时间戳。如果构建行为不符合预期,用这个命令排查非常有效。
2.7 本章小结
这一章的内容不多,但很重要。你只要记住:
- 规则 = 目标 + 依赖 + 命令
- 命令必须以 Tab 开头
- Make 通过时间戳判断是否需要重新构建