4. 打印调试法

调试 Makefile,说白了就是搞清楚「make 到底在干什么」。

我刚开始用 Makefile 那会儿,经常遇到一种情况:明明改了源码,重新编译却还是旧版本。或者更糟——改了 Makefile,结果整个工程全部重编,一编就是半小时。那时候我只会盯着终端输出看,看得眼睛都快瞎了。

后来我才发现,make 自己带了一套非常强大的调试工具。用好它们,你根本不需要靠猜。

4.1 干运行:make -n

make -n 也叫干运行(dry run)。它不执行任何命令,只把「将要执行的命令」打印出来。

核心价值:在真正动手之前,先看清楚 make 的计划。

用法很简单:

$ make -n
cc -c -o main.o main.c
cc -c -o utils.o utils.c
cc -o app main.o utils.o

看到没?它把三条命令列出来了,但一条都没跑。你可以检查:

  • 目标文件顺序对不对
  • 编译选项有没有漏
  • 链接的库齐不齐

我个人的习惯:每次修改 Makefile 之后,第一件事就是跑 make -n。确认无误了,再真正执行。这习惯帮我省了不知道多少次「编到一半发现错了」的尴尬。

小技巧:make -n 可以和 make -p 配合使用。-p 打印所有变量和规则,-n 打印执行计划。两个一起用,Makefile 在你面前就是透明的。

4.2 调试模式:make -d

make -d 是调试模式。它会输出海量信息——真的,海量。我第一次用的时候,终端刷了几千行,差点把我吓到。

但别怕,我们只需要关注几个关键点。

举个例子,假设我有这样一个 Makefile:

app: main.o utils.o
	cc -o app main.o utils.o

main.o: main.c
	cc -c main.c

utils.o: utils.c
	cc -c utils.c

执行 make -d,输出会包含类似这样的内容:

Considering target file 'app'.
 File 'app' does not exist.
  Considering target file 'main.o'.
   File 'main.o' does not exist.
    Looking for an implicit rule for 'main.o'.
    Trying pattern rule with stem 'main'.
    Found implicit rule '%.o: %.c'.
    Finished prerequisites of target file 'main.o'.
   Must remake target 'main.o'.
   Successfully remade target file 'main.o'.
  Finished prerequisites of target file 'app'.
 Must remake target 'app'.
 Successfully remade target 'app'.

关键信息一目了然:

  • make 先检查 app,发现它不存在
  • 然后检查依赖 main.o,也不存在
  • 找到隐式规则 %.o: %.c
  • 编译 main.o
  • 最后链接 app

我曾经遇到过一个诡异的问题:明明改了头文件,make 就是不重新编译。用 -d 一查,发现 make 认为头文件「不需要考虑」——原来是我忘了把头文件加到依赖里。这种问题,靠肉眼根本看不出来。

注意:make -d 输出量极大。建议用 make -d 2>&1 | less 分页查看,或者重定向到文件 make -d 2>debug.log。

4.3 详细调试:make --debug=v

make --debug=v 是 -d 的「精简版」。它只输出关键决策信息,不会把隐式规则搜索的每一步都列出来。

它的输出格式更友好:

$ make --debug=v
Reading makefiles...
Updating goal targets....
 File 'app' does not exist.
  File 'main.o' does not exist.
  Must remake target 'main.o'.
 Successfully remade target 'main.o'.
  File 'utils.o' does not exist.
  Must remake target 'utils.o'.
 Successfully remade target 'utils.o'.
Must remake target 'app'.
Successfully remade target 'app'.

对比一下 -d 和 --debug=v:

选项 输出量 适用场景
-d 极大(几千行) 排查隐式规则、变量展开等深层问题
--debug=v 中等(几十到几百行) 日常调试,看依赖关系和重编决策
--debug=n 极少 只看哪些文件被重新编译了

我个人更常用 --debug=v。信息量刚刚好,不会像 -d 那样刷屏,又能看到完整的决策链。

4.4 实战:一个完整的调试流程

假设我们遇到一个问题:改了 config.h,但 make 不重新编译依赖它的源文件。

第一步,用 make -n 看看计划:

$ make -n
make: Nothing to be done for 'all'.

嗯?明明改了头文件,make 却说没事干。这说明依赖关系有问题。

第二步,用 make --debug=v 看决策过程:

$ make --debug=v
Reading makefiles...
Updating goal targets....
 File 'all' is up to date.
  File 'main.o' is up to date.
  File 'utils.o' is up to date.

make 认为所有目标都是最新的。为什么?因为它不知道 main.o 依赖 config.h。

第三步,检查 Makefile:

main.o: main.c          # 缺少 config.h!
	cc -c main.c

问题找到了。加上依赖:

main.o: main.c config.h
	cc -c main.c

再跑 make -n:

$ make -n
cc -c main.c

这次对了。make 知道 config.h 变了,所以重新编译 main.o。

调试口诀:
-n 看计划,-d 看细节,--debug=v 看决策。

4.5 知识结构图

下面这张图总结了三种调试工具的关系和使用场景:

Makefile 调试工具全景 Makefile 调试 make -n(干运行) make -d(调试模式) make --debug=v 查看执行计划 不执行任何命令 适合日常检查 完整决策链 输出量极大 适合深层排查 关键决策信息 输出量适中 日常调试首选 调试口诀:-n 看计划,-d 看细节,--debug=v 看决策

4.6 避坑指南

最后分享几个我踩过的坑:

  • 不要在生产构建中用 -n:它虽然不执行命令,但会读取 Makefile 并展开变量。如果 Makefile 里有 $(shell ...),这些 shell 命令还是会跑。
  • -d 输出太大怎么办:用 grep 过滤。比如只看重编决策:make -d 2>&1 | grep -E "Must remake|Successfully remade"
  • 注意 --debug 的级别:v 是 verbose,n 是 basic,a 是 all。我建议从 v 开始,不够再上 a。
我的工作流:
1. 改完 Makefile 先跑 make -n 确认计划
2. 遇到奇怪行为跑 make --debug=v 看决策
3. 还搞不定就上 make -d 配合 grep 过滤
4. 最后用 make -p 检查变量值

说白了,调试 Makefile 就是跟 make 的决策逻辑打交道。用好这三个工具,你就能看穿 make 的每一步想法。下次遇到「改了文件却不重编」的问题,别急着怀疑人生——先跑个 --debug=v 看看。