2、Makefile基本语法:规则的结构、目标与依赖、命令的书写、Tab键的陷阱、注释的写法
好,咱们正式开始动手写Makefile了。说实话,很多初学者一上来就被Makefile的语法吓住了——觉得它长得怪,缩进还特别严格。其实你把它拆开看,核心就一个东西:规则。
Makefile里的一切,都是围绕规则展开的。你只要把规则搞懂了,剩下的就是查查函数、记记变量的事。
2.1 规则的结构:目标、依赖、命令
一条规则长什么样?我直接给你看最标准的模板:
目标: 依赖1 依赖2 依赖3
命令1
命令2
就这么简单。目标通常是你要生成的文件名,比如 main.o 或者 app。依赖是生成目标所需要的原材料。命令则是怎么把原材料加工成目标。
举个例子:
main.o: main.c
gcc -c main.c -o main.o
这里 main.o 是目标,main.c 是依赖,gcc -c main.c -o main.o 是命令。意思就是:如果 main.c 比 main.o 新,就执行这条命令重新编译。
核心理解:Makefile的本质是一个“依赖关系图”。make工具会从最终目标开始,递归地检查每个依赖是否需要更新。只有依赖比目标新时,才会执行命令。
我在项目中遇到过一种情况:有人把目标写成了 clean,但当前目录下恰好有一个叫 clean 的文件。结果每次执行 make clean,make都告诉你“clean是最新的”,就是不执行删除操作。这就是典型的“伪目标”问题,后面我们会专门讲。
2.2 目标与依赖:谁依赖谁,搞清楚
依赖关系是Makefile的灵魂。你想想看,一个项目少则几十个文件,多则上千个。如果每次改一个头文件就要全部重新编译,那效率得多低?
Makefile通过依赖关系,只重新编译那些真正受影响的部分。比如:
app: main.o utils.o
gcc main.o utils.o -o app
main.o: main.c defs.h
gcc -c main.c -o main.o
utils.o: utils.c defs.h
gcc -c utils.c -o utils.o
这里 app 依赖 main.o 和 utils.o。而每个 .o 文件又依赖对应的 .c 文件和头文件 defs.h。
如果你只改了 main.c,make会发现 main.o 比 main.c 旧,于是重新编译 main.o,然后重新链接 app。但 utils.o 不会动——因为它没有变化。
个人习惯:我写依赖时,会把头文件也列进去。虽然麻烦一点,但能避免很多“改了头文件但没重新编译”的坑。后来我学会了用 gcc -MM 自动生成依赖,省了不少事。
2.3 命令的书写:shell命令的延续
命令部分就是普通的shell命令。你可以写 gcc、rm、cp、echo,甚至写一个 python script.py 都行。Makefile不限制你用什么工具,它只负责在需要的时候执行这些命令。
有一点要注意:每条命令都在独立的shell进程中执行。这意味着:
target:
cd src
gcc -c main.c
这段代码不会按你想象的方式工作。因为 cd src 在第一个shell里执行,然后shell退出了。第二条命令 gcc -c main.c 在新的shell里执行,目录还是原来的目录。
正确的写法是:
target:
cd src && gcc -c main.c
或者用反斜杠换行:
target:
cd src; \
gcc -c main.c
嗯,这里要注意:反斜杠后面不能有空格或注释,否则换行会失效。我刚开始用Makefile时就被这个坑过,调试了半天才发现是反斜杠后面多了个空格。
2.4 Tab键的陷阱:一个空格引发的血案
这是Makefile里最著名的坑,没有之一。
Makefile规定:命令必须以Tab键开头。不能用空格代替,哪怕你用了8个空格也不行。
如果你用空格代替了Tab,make会报错:
Makefile:3: *** missing separator. Stop.
这个错误信息很模糊,初学者看到往往一头雾水。说白了就是:make在第三行没找到Tab键,只看到了空格。
我曾经帮同事排查过一个编译问题,他的Makefile看起来完全没问题,但就是报这个错。我打开文件一看,好家伙,整篇Makefile都是用空格缩进的。他说“我明明按了Tab啊”,结果发现是他的编辑器把Tab自动转换成了空格。
怎么避免?我建议你:
- 在编辑器里设置“保留Tab键”,不要自动转换
- 用
cat -A Makefile查看,Tab会显示为^I,空格就是空格 - 如果实在搞不定,可以用
sed -i 's/^ /\t/' Makefile把开头的4个空格替换成Tab
2.5 注释的写法:井号开头的学问
Makefile的注释用 # 开头,从 # 到行尾的内容都会被忽略。
# 这是注释
# 编译主程序
main.o: main.c defs.h
gcc -c main.c -o main.o # 这里也可以加注释
注释可以放在行首,也可以放在行尾。但要注意:命令行的注释不能跨行。如果你用反斜杠换行,注释只对当前行有效。
另外,变量赋值时注释要小心:
CFLAGS = -Wall -O2 # 开启警告和优化
这样写没问题。但如果你写成:
CFLAGS = -Wall -O2 # 开启警告和优化
注意 # 前面有个空格,这会导致 CFLAGS 的值变成 -Wall -O2 后面跟一个空格。虽然大多数情况下不影响使用,但严格来说是不对的。
我的习惯:注释尽量写清楚“为什么这么做”,而不是“做了什么”。比如 # 去掉调试符号以减小体积 就比 # strip 有用得多。半年后你回来看自己的Makefile,会感谢当初多写的那几行注释。
2.6 本章知识体系
下面这张图帮你梳理了Makefile基本语法的核心脉络:
这张图把Makefile的基本语法拆成了三个部分:目标、依赖、命令。再加上注释这个辅助元素,就构成了你写Makefile需要的全部基础。
记住:规则是骨架,依赖是灵魂,命令是血肉,注释是说明书。把这四个东西搞明白,你已经能写出80%的Makefile了。
下一节我们会深入变量的使用——那才是让Makefile变得灵活的关键。不过别急,先把今天的内容消化掉。找个简单的C项目,试着写一个Makefile,亲手踩一踩Tab键的坑,比看十遍教程都管用。
可可精品资料库