调试第二步:使用 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 排查流程图 遇到构建问题 执行 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 就是最好的证据来源。

可可精品资料库