第一个Makefile:目标、依赖、规则的基本语法
说实话,我刚开始学Makefile的时候,觉得这东西挺玄乎的。不就是个编译脚本吗?用得着这么复杂?直到我在一个嵌入式项目里,手动敲了上百次gcc命令,还经常漏编译某个文件...嗯,从那以后,我老老实实把Makefile学透了。
今天咱们就从最基础的开始。你只要会写C语言,会敲命令行,这章的内容你肯定能拿下。
Makefile的核心三要素
一个Makefile,说白了就三样东西:目标(target)、依赖(prerequisites)、规则(recipe)。我习惯把它们叫做「做什么、靠什么、怎么做」。
核心公式:
目标: 依赖
规则(命令)
举个例子你就明白了。假设你有一个C程序,就一个文件 hello.c:
# 最简单的Makefile
hello: hello.c
gcc -o hello hello.c
这里:
- hello 是目标——我们要生成的可执行文件
- hello.c 是依赖——编译需要这个源文件
- gcc -o hello hello.c 是规则——具体怎么编译
注意!规则前面的那个Tab键,不是空格。我当年就因为这个Tab键,排查了半小时。Makefile对缩进极其敏感,必须用Tab,不能用4个空格代替。这是新手最容易踩的坑。
⚠️ 避坑指南:我曾经在团队里见过一个同事,把Tab键设置成了自动替换为空格。结果整个Makefile跑不起来,他愣是没发现是缩进问题。所以,请确保你的编辑器对Makefile文件使用真实的Tab字符。
多文件编译的Makefile
实际项目中,不可能只有一个源文件。咱们来个稍微复杂点的例子:
# 多文件编译
app: main.o utils.o
gcc -o app main.o utils.o
main.o: main.c
gcc -c main.c
utils.o: utils.c utils.h
gcc -c utils.c
clean:
rm -f *.o app
这个Makefile里,我定义了4个目标:
- app:最终的可执行文件,依赖两个.o文件
- main.o:编译main.c得到的目标文件
- utils.o:编译utils.c得到的目标文件
- clean:清理编译产物,这个目标没有依赖
你可能会问:为什么要先编译成.o文件,再链接?直接一步到位不行吗?
当然可以。但你想一下:如果项目有100个源文件,你只改了其中1个。用上面的写法,Makefile只会重新编译那个被修改的文件,然后重新链接。如果一步到位,每次都得全部重新编译。我在一个嵌入式Linux项目里,全量编译要40分钟。用了这种增量编译方式,每次改代码后只需要10秒。这差距,你品品。
Makefile的执行逻辑
咱们用一张图来理解Makefile是怎么工作的:
这张图展示了Makefile的核心逻辑:
- 执行
make命令后,Make工具会找当前目录下的Makefile文件 - 它默认执行第一个目标(也就是最上面的那个)
- 检查目标的依赖是否存在。如果不存在,就去执行生成那个依赖的规则
- 如果依赖已经存在,比较依赖和目标的时间戳。如果依赖比目标新,就重新执行规则
- 如果目标比依赖新,就什么都不做——这就是增量编译的原理
💡 个人经验:我习惯把最终目标放在最上面,把clean、test这类辅助目标放在下面。这样执行 make 时默认编译主程序,想清理就执行 make clean。这个习惯让我少敲了很多命令。
实战:写一个完整的Makefile
咱们来写一个真正能用的。假设项目结构是这样的:
project/
├── main.c
├── utils.c
├── utils.h
└── Makefile
完整的Makefile:
# 最终目标
app: main.o utils.o
gcc -o app main.o utils.o
# 编译main.c
main.o: main.c
gcc -c main.c -o main.o
# 编译utils.c
utils.o: utils.c utils.h
gcc -c utils.c -o utils.o
# 清理
clean:
rm -f *.o app
# 伪目标声明(防止有同名文件)
.PHONY: clean
这里有个小细节:.PHONY: clean。为什么要加这个?
假设你目录下恰好有一个叫 clean 的文件。没有 .PHONY 声明的话,Make会认为 clean 这个目标已经是最新的了,就不执行里面的命令。加了 .PHONY,Make就知道这是个伪目标,每次都执行。我在一个项目里就因为这个吃了亏——目录里有个clean日志文件,结果 make clean 永远不干活。
常用命令速查
| 命令 | 作用 | 说明 |
|---|---|---|
make |
编译第一个目标 | 默认行为,相当于 make app |
make clean |
执行清理 | 删除编译中间文件 |
make -n |
预览模式 | 只显示要执行的命令,不真正执行 |
make -B |
强制重新编译 | 忽略时间戳,全部重新构建 |
调试小技巧:当你拿不准Makefile会执行哪些命令时,用 make -n 预览一下。这招帮我排查过无数次问题。特别是当你改了依赖关系,不确定Make会不会按预期重新编译的时候,先预览再执行,稳得很。
好了,第一个Makefile的内容就这些。说白了就是三要素:目标、依赖、规则。你只要记住这个结构,剩下的就是往里面填具体的编译命令。下一章咱们会聊Makefile里的变量,那才是真正让Makefile变得好用的东西。