调试第二步:使用 make -p(打印数据库)查看变量与规则
说实话,我见过太多人卡在 Makefile 的坑里出不来。
明明觉得规则写对了,变量也定义了,可 make 就是不听使唤。这时候,make -p 就是你的救命稻草。我个人习惯,遇到诡异问题,第一反应就是敲这条命令。
什么是 make -p?
make -p 会打印出 make 内部维护的完整数据库。说白了,就是把 make 脑子里想的东西全部倒出来给你看。
这个数据库包含:
- 所有变量的定义和值
- 所有隐式规则和显式规则
- 所有模式规则(pattern rules)
- 所有后缀规则(suffix rules)
- make 的默认设置和内置变量
你想想看,这相当于让 make 把它的「思维过程」完整呈现出来。哪里出了问题,一目了然。
核心要点:make -p 不会执行任何构建动作,它只是打印数据库然后退出。所以你可以放心使用,不用担心误操作。
基本用法
最简单的用法就是直接敲:
make -p
但这样输出会非常长。我建议配合 grep 来过滤:
make -p | grep -A5 "CC"
这条命令会显示所有包含 CC 的变量定义,以及它们后面的 5 行内容。嗯,这里要注意,-A5 表示显示匹配行之后的 5 行,方便你看到完整的变量值。
如果你想看某个特定变量的值,可以这样:
make -p | grep "^CC "
注意那个 ^CC 的写法——^ 表示行首,后面跟变量名和一个空格。这样能精确匹配变量定义,而不是变量被引用的地方。
我的小技巧:我通常会先把 make -p 的输出重定向到文件里,然后用编辑器慢慢看:
make -p > make_db.txt
vim make_db.txt
这样方便搜索和对比。我在项目中排查过一个很隐蔽的变量覆盖问题,就是靠这个办法找到的。
输出结构解读
make -p 的输出分为几个主要部分。我挑重点说:
1. 变量定义区
这部分列出了所有 GNU make 的变量。格式是这样的:
# Makefile 中的变量
CC = gcc
CFLAGS = -Wall -O2
# 自动变量
$@ = target
$< = first prerequisite
$^ = all prerequisites
注意看,# 开头的行是注释,说明这个变量来自哪里。如果是 # environment,说明是从环境变量继承来的;如果是 # makefile,说明是在 Makefile 里定义的。
我曾经遇到过一个情况:明明在 Makefile 里定义了 CC = clang,但 make 还是用了 gcc。一查 make -p,发现环境变量 CC 覆盖了 Makefile 里的定义。这就是典型的变量优先级问题。
2. 规则定义区
这部分列出了所有规则,包括隐式规则和显式规则。格式如下:
# 隐式规则
%.o: %.c
# commands to execute (built-in):
$(COMPILE.c) $(OUTPUT_OPTION) $<
# 显式规则
main.o: main.c header.h
# commands to execute (from `Makefile', line 10):
gcc -c main.c -o main.o
这里有个关键点:make 会告诉你每条规则是从哪里来的。如果是 built-in,那就是 make 内置的隐式规则;如果是 from 'Makefile', line 10,那就是你写的规则。
注意:如果某个目标没有匹配到任何规则,make -p 里就不会出现它的规则定义。这时候你就要检查是不是规则写错了,或者文件名不匹配。
实战案例:排查变量未定义问题
假设你有这样一个 Makefile:
CC = gcc
CFLAGS = -Wall
main: main.o utils.o
$(CC) $(CFLAGS) -o $@ $^
运行 make 时,报错说 CFLAGS 未定义。你可能会觉得奇怪——明明定义了呀?
这时候用 make -p | grep "CFLAGS" 看看:
# makefile (from 'Makefile', line 2)
CFLAGS = -Wall
嗯,定义是有的。那为什么报错?再仔细看输出:
# 注意这里
CFLAGS = -Wall
# 但下面还有一行
CFLAGS =
哦!原来在 Makefile 的后面某处,又定义了一次 CFLAGS,而且值是空的。这就是变量被覆盖了。我遇到过类似的情况,排查了半天,最后发现是 include 进来的另一个 Makefile 里重新定义了同一个变量。
用 SVG 画一张流程图
下面这张图展示了 make -p 的排查流程:
常见排查场景
| 问题现象 | 用 make -p 查什么 |
典型原因 |
|---|---|---|
| 变量值不对 | 查看变量的最终值,以及它的来源 | 环境变量覆盖、递归赋值问题 |
| 规则没执行 | 查看目标是否有匹配的规则 | 规则写错、文件名不匹配 |
| 隐式规则生效了 | 查看内置的隐式规则列表 | 没有显式覆盖隐式规则 |
| 变量展开异常 | 查看变量的展开结果 | 使用了错误的赋值方式(= vs :=) |
进阶技巧:结合 --just-print
有时候光看数据库还不够。我习惯把 make -p 和 make -n(也就是 --just-print)结合起来用。
make -n 会打印出 make 将要执行的命令,但不实际执行。这样你可以看到:
- 哪些目标会被构建
- 用的是什么命令
- 变量展开后的最终结果是什么
举个例子:
make -n main
输出可能是:
gcc -Wall -O2 -c main.c -o main.o
gcc -Wall -O2 main.o utils.o -o main
如果这里显示的 CFLAGS 值不对,再回头用 make -p 查变量定义。两步配合,基本没有查不出来的问题。
我的经验:我曾经在一个大型项目中,用 make -p 发现了一个隐藏了半年的 bug。那个项目有 30 多个 Makefile 文件互相 include,变量层层覆盖。最后靠 make -p 打印出所有变量的最终值,才定位到问题。从那以后,make -p 就成了我调试 Makefile 的第一选择。
总结一下
make -p 的核心价值就一句话:让 make 告诉你它到底在想什么。
变量有没有定义?值是什么?规则有没有匹配?命令是什么?这些信息,make -p 全都会告诉你。你只需要学会怎么看这些输出,就能快速定位问题。
记住,调试 Makefile 不是靠猜,而是靠证据。make -p 就是最好的证据来源。