调试CMake:从懵圈到心中有数

说实话,CMake 的调试体验,在很长一段时间里都让我挺头疼的。你写了一大堆 target_link_librariesadd_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 里变量传递是个大坑。尤其是 PRIVATEINTERFACEPUBLIC 这些关键字,再加上 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.cmakeXXXConfig.cmake

--debug-try-compile

这个参数专门用来调试 try_compiletry_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.

排查步骤:

  1. 检查 find_package 是否成功。用 message() 打印 fmt_FOUND 变量。
  2. 检查 find_package 的搜索路径。用 --debug-output 查看。
  3. 检查是否忘了 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. 知识体系总览

下面这张图,把本章的核心内容串起来了。你可以把它当成一个调试路线图:

CMake 调试知识体系 CMake 调试 执行追踪 --trace 全局追踪 --trace-source 单文件 变量查看 message() 打印 cmake -LAH 缓存变量 --system-information 调试器 --debug-output --debug-try-compile 常见错误排查 目标未找到 变量作用域 路径/编码问题 生成器表达式误用 核心原则:先定位问题范围,再选择合适的调试工具

6. 写在最后

调试 CMake 其实没那么玄乎。说白了,就是搞清楚「它到底执行了什么」和「变量到底是什么值」。上面这些工具,你多用几次就会形成肌肉记忆。

我个人建议你从 --tracemessage() 开始。这两个最直接,也最容易上手。等你遇到更复杂的问题时,再上 --debug-try-compilecmake -LAH

嗯,今天就聊到这儿。下次你遇到 CMake 报错,别急着改代码,先跑一遍追踪,看看它到底在干什么。