调试CMake:从懵圈到心中有数
说实话,CMake 的调试体验,在很长一段时间里都让我挺头疼的。你写了一大堆 target_link_libraries、add_subdirectory,结果一运行,报个莫名其妙的错。变量没传过去?路径写错了?还是某个 if 分支根本没走?
我刚开始用 CMake 那会儿,遇到问题全靠加 message() 打印。后来发现,CMake 其实给了我们一套挺完整的调试工具。今天我就把这些家伙什儿都翻出来,跟你好好聊聊。
1. --trace 与 --trace-source:让 CMake 把每一步都告诉你
先说说最基础的。你想想看,CMake 本质上就是个脚本解释器。它一行一行地读你的 CMakeLists.txt,然后执行。如果某个地方出了问题,最直接的办法就是——让它把执行过程打印出来。
--trace:全局追踪
在运行 cmake 时加上 --trace 参数,CMake 就会把每一条被执行的命令都打印到终端。包括它在哪个文件的哪一行。
cmake --trace -B build
输出大概长这样:
/path/to/CMakeLists.txt:10: add_executable(myapp main.cpp)
/path/to/CMakeLists.txt:12: target_link_libraries(myapp PRIVATE fmt)
/path/to/cmake/helpers.cmake:5: function(check_dependency)
/path/to/cmake/helpers.cmake:6: if(NOT TARGET fmt::fmt)
...
我个人习惯在排查「某个函数到底有没有被调用」或者「某个条件分支到底走了哪一边」时,直接开 --trace。它不会漏掉任何东西。
--trace 的输出会非常非常多。建议配合 --trace-source 使用,或者把输出重定向到文件里慢慢看。
--trace-source:只盯一个文件
有时候你只想知道某个特定的 .cmake 文件或 CMakeLists.txt 里发生了什么。这时候用 --trace-source 更合适。
cmake --trace-source=CMakeLists.txt -B build
它只会打印指定文件中的执行记录。我在项目中遇到过好几次,某个 .cmake 模块里的变量死活没生效。用 --trace-source 一追,发现是函数返回前忘了 set(... PARENT_SCOPE)。嗯,这种问题光靠看代码真的很难发现。
2. 变量查看技巧:别瞎猜,直接看
CMake 里变量传递是个大坑。尤其是 PRIVATE、INTERFACE、PUBLIC 这些关键字,再加上 PARENT_SCOPE,一不小心变量就丢了。
message() 的妙用
最朴素也最有效的方法,就是打印。但很多人只会写 message("${VAR}")。我建议你加上变量名一起打印,方便对照:
message(STATUS "MY_VAR = ${MY_VAR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
如果你想知道一个变量到底有没有被定义,可以用:
if(DEFINED MY_VAR)
message(STATUS "MY_VAR is defined: ${MY_VAR}")
else()
message(STATUS "MY_VAR is NOT defined")
endif()
cmake --system-information
想一口气看所有 CMake 内置变量?用这个命令:
cmake --system-information
它会输出一份完整的变量报告,包括编译器路径、系统架构、安装路径等等。我一般在新环境上配项目时,先跑一遍这个,心里有个底。
cmake -LAH
这个命令我特别喜欢。它能列出所有缓存变量,包括你定义的 option() 和 set(... CACHE ...):
cmake -LAH -B build
输出里会显示每个变量的类型、值和帮助信息。比如:
// Enable verbose output
VERBOSE_OUTPUT:BOOL=OFF
// Path to the external library
EXTERNAL_LIB_PATH:STRING=/usr/local/lib
有一次我同事说他的 BUILD_SHARED_LIBS 明明设成了 ON,但编译出来还是静态库。我让他跑了一下 cmake -LAH,发现他设的变量名拼错了……这种问题,你盯着代码看一天也看不出来。
3. CMake 调试器:cmake --debug-output 与 cmake --debug-trycompile
除了追踪执行过程,CMake 还提供了几个调试级别的输出选项。
--debug-output
这个参数会打印更详细的内部信息,比如 find_package 的搜索路径、try_compile 的详细过程等。
cmake --debug-output -B build
我一般在排查「为什么找不到某个包」时,会开这个。它会告诉你 CMake 去哪些目录找了,每个目录下有没有找到对应的 FindXXX.cmake 或 XXXConfig.cmake。
--debug-try-compile
这个参数专门用来调试 try_compile 和 try_run。它会保留编译过程中生成的临时文件,方便你手动检查。
cmake --debug-try-compile -B build
默认情况下,CMake 会在 ${CMAKE_BINARY_DIR}/CMakeFiles/CMakeTmp/ 下生成这些临时文件。你可以进去看源码、看编译命令,甚至手动编译试试。
CheckCXXSourceCompiles 之类的检测,发现结果总是不对,就用 --debug-try-compile 看看它到底编译了什么东西。我曾经遇到过,它生成的测试代码里少了一个头文件包含,导致检测失败。
4. 常见错误排查:那些年我踩过的坑
下面这几个错误,我敢说你迟早会遇到。提前知道怎么排查,能省不少时间。
错误1:target_link_libraries 找不到目标
报错信息类似:
Target "myapp" links to target "fmt::fmt" but the target was not found.
排查步骤:
- 检查
find_package是否成功。用message()打印fmt_FOUND变量。 - 检查
find_package的搜索路径。用--debug-output查看。 - 检查是否忘了
add_subdirectory或者FetchContent没有正确下载。
错误2:变量作用域问题
你在子目录的 CMakeLists.txt 里设了一个变量,回到父目录发现它没了。
排查方法:
- 在子目录里用
set(MY_VAR "value" PARENT_SCOPE)显式传递。 - 或者用
set(MY_VAR "value" CACHE INTERNAL "")设为全局缓存变量。 - 用
--trace追踪变量的设置和读取位置。
错误3:路径中包含空格或中文
CMake 对路径中的空格和特殊字符处理得不太好。我建议你从一开始就养成习惯:
- 项目路径不要有空格。
- 所有路径用
file(TO_CMAKE_PATH)或file(TO_NATIVE_PATH)做转换。 - 用
message()打印路径变量,确认它不是你想象的那样。
错误4:生成器表达式用错了地方
生成器表达式(比如 $<CONFIG:Debug>)只能在某些上下文中使用。如果你在 if() 条件里用,它不会报错,但也不会生效。
排查方法:
- 记住:生成器表达式只在
target_*命令、install()、file(GENERATE)等地方有效。 - 如果你不确定,先用
message()打印一个普通变量,确认逻辑正确,再换成生成器表达式。
5. 知识体系总览
下面这张图,把本章的核心内容串起来了。你可以把它当成一个调试路线图:
6. 写在最后
调试 CMake 其实没那么玄乎。说白了,就是搞清楚「它到底执行了什么」和「变量到底是什么值」。上面这些工具,你多用几次就会形成肌肉记忆。
我个人建议你从 --trace 和 message() 开始。这两个最直接,也最容易上手。等你遇到更复杂的问题时,再上 --debug-try-compile 和 cmake -LAH。
嗯,今天就聊到这儿。下次你遇到 CMake 报错,别急着改代码,先跑一遍追踪,看看它到底在干什么。