2. Makefile核心规则

2.1 一条规则,三个要素

Makefile 的规则,说白了就是三样东西:目标(target)、依赖(prerequisites)、命令(recipe)。

我刚开始学 Makefile 时,总觉得这东西很玄乎。后来发现,它就是个「如果依赖变了,就重新生成目标」的简单逻辑。你想想看,这不就是我们日常编译代码的流程吗?

规则的标准格式长这样:

目标: 依赖1 依赖2 依赖3
    命令1
    命令2

注意了,命令前面必须是一个 Tab 键,不是空格。我见过太多新手在这里栽跟头——用空格缩进,然后 make 报错「missing separator」。嗯,这个坑我当年也踩过。

2.2 目标:你想生成什么

目标通常是一个文件名,比如 main.o、app。但也可以是伪目标,比如 clean、all。

Make 会检查目标文件是否存在。如果不存在,就执行命令去生成它。如果存在,就检查依赖有没有更新——依赖比目标新,那就重新生成。

举个例子:

main.o: main.c
    gcc -c main.c -o main.o

这里 main.o 是目标,main.c 是依赖。如果 main.c 被修改了,make 就会重新编译 main.o。

我的习惯:我一般会把第一个目标设为 all,让它依赖所有最终产物。这样执行 make 时,默认就会构建整个项目。

2.3 依赖:触发重建的条件

依赖可以是文件,也可以是其他目标。Make 会递归地检查所有依赖。

比如:

app: main.o utils.o
    gcc main.o utils.o -o app

main.o: main.c
    gcc -c main.c -o main.o

utils.o: utils.c utils.h
    gcc -c utils.c -o utils.o

执行 make app 时,make 会先检查 main.o 和 utils.o 是否需要更新。如果 utils.h 改了,utils.o 就会重新编译,然后 app 也会重新链接。

这就是 Makefile 的「依赖链」——一层层往下推,直到找到需要重新编译的源头。

2.4 命令:如何生成目标

命令就是 shell 命令。每条命令都在独立的 shell 进程中执行。这一点很重要。

比如:

target:
    cd /tmp
    pwd

你以为会输出 /tmp?错了。因为 cd /tmp 在子 shell 里执行,不影响后面的 pwd。如果你想让它们在一个 shell 里执行,可以用 ; 或者 && 连接:

target:
    cd /tmp && pwd

或者用反斜线换行:

target:
    cd /tmp; \
    pwd
我曾经踩过的坑:有一次我在命令里设置了环境变量,结果下一行命令根本拿不到。查了半天才发现,每行命令都是独立 shell。从那以后,我习惯用 && 或者把多行命令写在一行里。

2.5 伪目标 .PHONY

伪目标,就是那些不代表实际文件的目标。最典型的就是 clean。

你想想看,如果当前目录下恰好有一个叫 clean 的文件,那 make clean 会怎样?

Make 会检查 clean 文件是否存在。如果存在,而且没有依赖,它就认为目标已经是最新的了——什么都不做!

这就是为什么我们需要 .PHONY:

.PHONY: clean
clean:
    rm -f *.o app

声明为 .PHONY 后,make 就不会去检查文件是否存在,每次都老老实实执行命令。

常见的伪目标有:

  • all —— 构建所有目标
  • clean —— 清理中间文件
  • install —— 安装程序
  • test —— 运行测试
核心要点:只要目标不代表实际文件,就把它声明为 .PHONY。这是 Makefile 的「最佳实践」,没有例外。

2.6 规则格式的细节

规则还有一些变体,我简单列一下:

格式 说明 示例
单目标多依赖 最常见的形式 app: main.o utils.o
多目标单规则 多个目标共享同一组命令 a.o b.o: common.h
无依赖规则 只有命令,没有依赖 clean: rm -f *.o
无命令规则 只声明依赖关系 app: main.o

多目标规则我用的不多,但偶尔有用。比如当多个 .o 文件都依赖同一个头文件时,可以写成一行:

main.o utils.o: config.h

这样 config.h 改了,两个 .o 都会重新编译。

2.7 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

Makefile 核心规则结构 规则 (Rule) 目标 (Target) 依赖 (Prerequisites) 命令 (Recipe) 文件名(如 main.o) 伪目标(如 clean) 文件依赖(如 .c .h) 目标依赖(递归检查) Shell 命令 Tab 缩进(非空格) 核心逻辑:依赖比目标新 → 执行命令 → 更新目标

2.8 实战中的避坑指南

最后,分享几个我实际项目中遇到的教训:

  • 不要用空格代替 Tab —— 这是 Makefile 最常见的错误。我建议你在编辑器里设置「显示空白字符」,一眼就能看出来。
  • 伪目标一定要声明 —— 我见过有人没加 .PHONY,结果目录里多了个 clean 文件,make clean 再也不工作了。排查了半天才发现。
  • 命令中的变量展开时机 —— 命令里的 $ 会被 make 解释。如果你想传 $ 给 shell,得写成 $$。比如 echo $$HOME 才能输出环境变量。
一个小技巧:如果你不确定规则写对了没有,可以执行 make -n。这个选项只会「模拟」执行,打印出要运行的命令,但不会真的运行。调试时特别好用。

好了,Makefile 的核心规则就这些。说白了就是「目标、依赖、命令」这三个东西,再加上伪目标这个特殊用法。你只要记住这个框架,后面学变量、函数、条件判断都会轻松很多。