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基本语法 规则 (Rule) 目标 (Target) 依赖 (Dependency) 命令 (Command) 伪目标 (.PHONY) 多目标 依赖关系链 自动依赖生成 Tab键要求 Shell独立性 注释:用 # 开头,解释“为什么”

这张图把Makefile的基本语法拆成了三个部分:目标、依赖、命令。再加上注释这个辅助元素,就构成了你写Makefile需要的全部基础。

记住:规则是骨架,依赖是灵魂,命令是血肉,注释是说明书。把这四个东西搞明白,你已经能写出80%的Makefile了。

下一节我们会深入变量的使用——那才是让Makefile变得灵活的关键。不过别急,先把今天的内容消化掉。找个简单的C项目,试着写一个Makefile,亲手踩一踩Tab键的坑,比看十遍教程都管用。


可可精品资料库