2. 第一个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 命令 查找第一个目标(默认目标) 检查依赖文件是否存在 递归生成依赖文件 比较时间戳

这张图展示了Makefile的核心逻辑:

  1. 执行 make 命令后,Make工具会找当前目录下的Makefile文件
  2. 它默认执行第一个目标(也就是最上面的那个)
  3. 检查目标的依赖是否存在。如果不存在,就去执行生成那个依赖的规则
  4. 如果依赖已经存在,比较依赖和目标的时间戳。如果依赖比目标新,就重新执行规则
  5. 如果目标比依赖新,就什么都不做——这就是增量编译的原理

💡 个人经验:我习惯把最终目标放在最上面,把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会不会按预期重新编译的时候,先预览再执行,稳得很。