调试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循环,里面调用了setadd_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个未初始化的变量,全是拼写错误导致的。

综合使用:调试实战流程

说了这么多,到底怎么用?我一般按这个顺序来:

  1. 先跑一遍cmake -B build,看看有没有明显的语法错误。
  2. 如果配置成功但行为不对,加上--debug-output,看CMake内部决策过程。
  3. 如果怀疑变量值不对,加上--trace-expand,看变量展开后的真实值。
  4. 如果怀疑执行顺序不对,加上--trace,看每一行代码的执行顺序。
  5. 如果怀疑某个模块有问题,用--trace-source只跟踪那个模块。
  6. 最后,加上--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调试选项知识图谱 CMake调试选项 --debug-output 看CMake内部决策过程 --trace 看每一行代码执行 --trace-expand 看变量展开后的真实值 --trace-source 只跟踪特定文件 --warn-uninitialized 检查未初始化变量 组合使用,覆盖90%的CMake调试场景

这张图把五个调试选项的关系和用途都画出来了。你从中心出发,根据你的问题类型,选择对应的分支。我个人建议你把这张图存下来,下次遇到CMake问题,先看一眼,就知道该用哪个选项了。

我的经验: 刚开始用这些选项时,可能会觉得输出太多、眼花缭乱。别急,先从小项目练手。随便写个简单的CMakeLists.txt,加上--trace跑一遍,看看每一行是怎么执行的。跑个三五次,你就熟悉了。我当年就是这么练出来的。

公众号:蓝海资料掘金营,微信deep3321