调试CMake:使用--debug-output、--trace等选项排查问题
写CMakeLists.txt的时候,最头疼的是什么?我个人觉得,不是语法记不住,而是脚本跑起来之后,你完全不知道它到底干了什么。尤其是项目大了,变量满天飞,条件判断层层嵌套,突然某个库没找到,或者某个宏没定义……这时候你盯着屏幕干瞪眼,没用。
其实CMake自带了一套调试工具,只是很多人不知道。今天我就带你把这几个选项玩明白。说白了,就是给CMake装上“监控摄像头”,看它每一步是怎么执行的。
--debug-output:看CMake在“想什么”
这个选项最简单,也最常用。加上它之后,CMake会把内部的一些关键决策过程打印出来。比如它什么时候加载了某个模块,什么时候检测到了编译器,什么时候缓存了变量。
我在项目中遇到过一个问题:明明在CMakeLists.txt里设置了CMAKE_CXX_STANDARD,但编译出来的代码就是用的老标准。加上--debug-output一跑,发现后面有个find_package偷偷把它覆盖了。嗯,这种问题你不看内部日志,根本猜不到。
# 基本用法
cmake --debug-output -B build
# 输出示例(简化)
...
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- debug: Adding target 'myapp'
-- debug: Evaluating generator expression: $<TARGET_PROPERTY:myapp,OUTPUT_NAME>
...
你想想看,这些信息平时是不会显示的。只有加上--debug-output,CMake才会把“内心独白”告诉你。我个人习惯在排查变量覆盖、模块加载顺序问题时,第一个就用它。
--debug-output可以和-D选项一起用。比如 cmake --debug-output -DCMAKE_BUILD_TYPE=Debug -B build,这样既能调试,又能指定构建类型。
--trace:看CMake执行了“哪一行”
--trace比--debug-output更底层。它会打印出CMake解析和执行每一条命令的过程。说白了,就是把你CMakeLists.txt里的每一行代码,以及它调用的所有内部命令,全部列出来。
为什么会需要这个?举个例子:你写了一个foreach循环,里面调用了set和add_library,结果发现某个库没生成。你怀疑是循环条件写错了,或者变量没传进去。这时候--trace就能告诉你:第几行执行了,变量值是什么,执行结果是什么。
# 基本用法
cmake --trace -B build
# 输出示例(简化)
...
/path/to/CMakeLists.txt:42: set(MY_VAR "hello")
/path/to/CMakeLists.txt:43: message(STATUS "MY_VAR is ${MY_VAR}")
/path/to/CMakeLists.txt:45: add_library(myapp main.cpp)
...
每一行前面都带着文件名和行号。你想想看,这比你在代码里到处加message()要方便多少?我曾经调试一个嵌套了五六层的include的项目,全靠--trace才理清楚文件加载顺序。
--trace的输出量非常大。如果你的项目有几百行CMakeLists.txt,加上几十个第三方库的配置文件,输出可能上万行。建议配合grep使用,比如 cmake --trace -B build 2>&1 | grep "my_target"。
--trace-expand:展开变量,看真实值
--trace虽然好,但它有个小缺点:它打印的是你写的原始代码,变量名还是${MY_VAR}这种形式。如果你想知道变量展开之后的值是什么,就得用--trace-expand。
这个选项会把所有变量引用都替换成实际值。比如你写了set(OUTPUT_DIR "${PROJECT_SOURCE_DIR}/output"),--trace会原样输出,而--trace-expand会输出set(OUTPUT_DIR "/home/user/myproject/output")。
# 基本用法
cmake --trace-expand -B build
# 输出对比
# --trace 输出:
/path/CMakeLists.txt:10: set(OUTPUT_DIR "${PROJECT_SOURCE_DIR}/output")
# --trace-expand 输出:
/path/CMakeLists.txt:10: set(OUTPUT_DIR "/home/user/myproject/output")
我个人习惯在排查路径问题时,优先用--trace-expand。比如某个头文件找不到,你怀疑是include_directories里的路径没拼对,用这个一看就明白了。
--trace-source:只跟踪特定文件
如果项目太大,--trace输出太多,你可以用--trace-source指定只跟踪某个文件。比如你只关心cmake/helpers.cmake这个文件里的执行情况。
# 只跟踪 cmake/helpers.cmake
cmake --trace-source=cmake/helpers.cmake -B build
这个选项可以多次使用,跟踪多个文件。我在调试第三方库的FindXXX.cmake模块时,经常这么干。因为那些模块动辄几百行,全量跟踪根本看不过来。
--warn-uninitialized:揪出未初始化的变量
这个选项不是调试选项,但它能帮你发现很多潜在问题。加上它之后,如果CMake发现你使用了未初始化的变量,就会给出警告。
cmake --warn-uninitialized -B build
# 输出示例
CMake Warning: Variable 'MY_UNDEFINED_VAR' is uninitialized.
Called from: /path/CMakeLists.txt:55 (message)
你想想看,有时候你写了个if(MY_FLAG),结果MY_FLAG根本没定义,这个条件永远为假。这种bug很难发现,但--warn-uninitialized能直接帮你揪出来。我曾经在一个大型项目中,用这个选项一次性发现了7个未初始化的变量,全是拼写错误导致的。
综合使用:调试实战流程
说了这么多,到底怎么用?我一般按这个顺序来:
- 先跑一遍
cmake -B build,看看有没有明显的语法错误。 - 如果配置成功但行为不对,加上
--debug-output,看CMake内部决策过程。 - 如果怀疑变量值不对,加上
--trace-expand,看变量展开后的真实值。 - 如果怀疑执行顺序不对,加上
--trace,看每一行代码的执行顺序。 - 如果怀疑某个模块有问题,用
--trace-source只跟踪那个模块。 - 最后,加上
--warn-uninitialized,检查有没有未初始化的变量。
这个流程我用了好几年,基本能解决90%的CMake调试问题。剩下的10%,那就得靠message()大法和耐心了。
--debug-output:看CMake内部决策过程,适合排查变量覆盖、模块加载问题。--trace:看每一行代码的执行,适合排查执行顺序、条件判断问题。--trace-expand:看变量展开后的真实值,适合排查路径、字符串拼接问题。--trace-source:只跟踪特定文件,适合调试第三方模块。--warn-uninitialized:检查未初始化的变量,适合排查拼写错误、遗漏定义。
调试选项对比表
| 选项 | 输出内容 | 适用场景 | 输出量 |
|---|---|---|---|
--debug-output |
CMake内部决策日志 | 变量覆盖、模块加载 | 中等 |
--trace |
每行代码执行记录 | 执行顺序、条件判断 | 很大 |
--trace-expand |
变量展开后的代码 | 路径、字符串拼接 | 很大 |
--trace-source |
指定文件的执行记录 | 调试第三方模块 | 可控 |
--warn-uninitialized |
未初始化变量警告 | 拼写错误、遗漏定义 | 小 |
SVG:CMake调试选项知识图谱
这张图把五个调试选项的关系和用途都画出来了。你从中心出发,根据你的问题类型,选择对应的分支。我个人建议你把这张图存下来,下次遇到CMake问题,先看一眼,就知道该用哪个选项了。
--trace跑一遍,看看每一行是怎么执行的。跑个三五次,你就熟悉了。我当年就是这么练出来的。