商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > CMake常见的调试技巧

CMake常见的调试技巧

  发布于2026-07-04 阅读(0)

扫一扫,手机访问

1.简介

编写CMake脚本时,难免会遇到各种出乎意料的问题——变量值不对、依赖找不到、条件分支走错了路。面对这些让人挠头的情况,掌握正确的调试方法显得尤为重要。下面就来梳理一套从入门到深入的CMake调试思路,从最基础的打印输出,到高级的调试器单步跟踪,希望能帮各位少走弯路。

2.用message()输出关键信息

2.1.message简介

message()这个命令,说它是CMake调试中“最先想到、最常用到”的工具,一点都不为过。它负责将信息打印到控制台,帮助我们看清脚本执行的每一处细节。

基本语法长这样:

message([<模式>] "消息内容")
  • 模式(可选):用来指定消息的级别,控制输出的样式和行为(比如是否中断执行)。
  • 消息内容:可以是文本、变量(记得用 ${变量名} 引用)、表达式,甚至是列表。

2.2.常用模式及作用

CMake通过模式来区分消息的轻重缓急,下面是几个必须掌握的:

模式作用与特点
STATUS最常用,用于输出配置过程中的状态提示(比如变量值、路径信息)。输出时会自动带缩进,与CMake原生的输出风格一致,推荐优先使用。
WARNING警告信息,通常以醒目的方式显示(比如黄色),但不会中断CMake的执行。适合提示过时用法或潜在问题。
SEND_ERROR错误信息,会继续执行后续脚本,但最终会标记构建失败。适用于非致命错误,比如可选依赖缺失。
FATAL_ERROR致命错误,一旦出现,CMake立即停止执行。适用于关键依赖缺失、无效配置等必须解决的问题。
无模式默认模式,输出普通文本。不推荐使用,因为风格与CMake原生输出不一致,容易造成混淆。
DEPRECATION过时提示,仅在开发者模式(-Wdev)下显示,用于标记即将废弃的功能。

2.3.核心用法示例

1.输出状态信息(STATUS)

用来反馈配置过程中的关键信息,比如变量值、路径、条件分支的情况:

# 输出普通变量
set(MY_VAR "test")
message(STATUS "MY_VAR = ${MY_VAR}")  # 输出:-- MY_VAR = test

# 输出缓存变量(如 find_package 结果)
find_package(OpenSSL)
message(STATUS "OpenSSL_FOUND = ${OpenSSL_FOUND}")  # 检查依赖是否找到
message(STATUS "OpenSSL_INCLUDE_DIRS = ${OpenSSL_INCLUDE_DIRS}")  # 路径是否正确

# 输出内置变量(如路径、编译器信息)
message(STATUS "CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}")  # 源码根目录
message(STATUS "CMAKE_CXX_COMPILER = ${CMAKE_CXX_COMPILER}")  # 编译器路径

2.调试变量与列表

变量引用一定要用 ${},否则会被当成普通字符串:

set(MY_VAR "hello")
message(STATUS "变量值: ${MY_VAR}")  # 正确:输出 "变量值: hello"
# message(STATUS "变量值: MY_VAR")  # 错误:输出 "变量值: MY_VAR"

列表默认用分号分隔,可以用 string(JOIN) 来格式化输出:

set(MYLIST "a" "b" "c")  # CMake 列表(内部存储为 "a;b;c")
string(JOIN ", " LIST_STR "${MYLIST}")  # 转换为 "a, b, c"
message(STATUS "列表内容: ${LIST_STR}")  # 输出:列表内容: a, b, c

3.跟踪条件分支执行

if() 分支逻辑不符合预期时,在分支内输出标记,就能确认是否进入了目标分支:

option(ENABLE_FEATURE "启用功能" OFF)

if(ENABLE_FEATURE)
    message(STATUS "进入 ENABLE_FEATURE 分支")  # 若未输出,说明条件不成立
    # ... 功能代码 ...
else()
    message(STATUS "进入 ELSE 分支(功能未启用)")  # 确认是否走了默认分支
endif()

# 复杂条件判断(如版本比较)
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER "11.0")
    message(STATUS "编译器版本 > 11.0")
else()
    message(STATUS "编译器版本 <= 11.0(当前: ${CMAKE_CXX_COMPILER_VERSION})")
endif()

4.错误与警告提示

警告(WARNING):提示潜在问题但不中断执行:

if(CMAKE_VERSION VERSION_LESS "3.10")
    message(WARNING "CMake 版本过低,部分功能可能受限(推荐 >=3.10)")
endif()

致命错误(FATAL_ERROR):关键问题必须解决时中断执行:

if(NOT EXISTS "${CMAKE_SOURCE_DIR}/src/main.cpp")
    message(FATAL_ERROR "未找到核心源文件 src/main.cpp,请检查源码完整性!")
endif()

2.4.常见问题及解决

  • 优先使用 STATUS 模式:保持输出风格与 CMake 一致,避免混乱。
  • 变量未展开:忘记用 ${} 引用变量,导致输出的是变量名而非值。比如 message(STATUS "VAR: MY_VAR") 应该改成 message(STATUS "VAR: ${MY_VAR}")
  • 列表格式混乱:CMake 列表默认用分号分隔,可以通过 string(JOIN ", " 新变量 原列表) 转换成更易读的格式。
  • 消息不显示:可能是模式级别过高(比如 DEPRECATION 默认不显示),或者 CMake 以静默模式运行(比如加了 -Wno-dev)。改用 STATUSWARNING 模式即可。

3.查看缓存变量:cmake -L与缓存文件

CMake 会把关键变量(比如 optionfind_package 结果、路径配置)存储在 CMakeCache.txt 中,这个文件在构建目录下。这些变量可能被缓存而不更新,导致配置出现异常。

3.1.列出所有缓存变量(cmake -L)

在构建目录执行以下命令,可以列出所有缓存变量及其值,快速确认变量是否被正确设置:

# 列出所有非高级缓存变量(常用)
cmake -L .

# 列出所有缓存变量(包括高级变量,如编译器细节)
cmake -LA .

# 搜索特定变量(结合 grep)
cmake -LA . | grep "OpenSSL"  # 查找与 OpenSSL 相关的缓存变量

示例输出(部分):

ENABLE_FEATURE:BOOL=OFF
OpenSSL_FOUND:BOOL=ON
OpenSSL_INCLUDE_DIRS:PATH=/usr/include/openssl
...

3.2.直接查看 / 删除CMakeCache.txt

如果怀疑旧的缓存影响了配置(比如修改了 option 后值没有更新),可以:

  • 直接打开构建目录下的 CMakeCache.txt,搜索目标变量(比如 ENABLE_FEATURE),查看其实际值;
  • 删除 CMakeCache.txt 或整个构建目录(rm -rf build && mkdir build && cd build),然后重新配置,避免缓存干扰。

4.变量追踪与作用域

4.1.CMAKE_MESSAGE_CONTEXT(CMake 3.25+)

  • 设置一个上下文字符串,这个字符串会自动添加到当前目录及所有子目录中所有后续 message() 调用的输出中。
  • 在大型项目或递归结构中追踪消息来源,简直好用到飞起。
list(APPEND CMAKE_MESSAGE_CONTEXT "MyModule")
message(STATUS "Configuring MyModule...") # 输出: [MyModule] -- Configuring MyModule...
list(POP_BACK CMAKE_MESSAGE_CONTEXT) # 退出当前上下文

4.2.作用域问题

  • set(... PARENT_SCOPE): 修改父作用域变量。
  • set(... CACHE ... FORCE): 强制修改缓存变量。
  • 使用 message() 在不同位置(函数内/外、不同 CMakeLists.txt 中)打印变量值,观察其变化,是诊断作用域问题的关键手段。

4.3.监控变量变化:variable_watch()

这个命令用来跟踪指定变量的读取、修改、删除操作。当变量发生这些行为时,CMake 会自动输出调试信息,包括操作类型、位置、新旧值等。

用法

# 监控单个变量
variable_watch(MY_VAR)

# 监控多个变量
variable_watch(CMAKE_CXX_STANDARD)
variable_watch(FOO_BAR)

效果:当 MY_VAR 被读取(如 if(MY_VAR))、修改(如 set(MY_VAR 1))或删除(如 unset(MY_VAR))时,会输出类似:

Variable MY_VAR was modified at [...]/CMakeLists.txt:10 (set). Old value: "0", new value: "1"

4.4.属性获取

4.4.1.获取 CMake 全局属性(get_cmake_property())

用来查询 CMake 的全局属性(比如已定义的变量列表、目标列表、目录列表等),结合 message() 可以打印出全局状态。

常用场景

  • 打印所有已定义的变量
  • 打印所有已创建的目标(可执行文件、库)
  • 打印所有包含的目录

示例

# 打印所有已定义的变量
get_cmake_property(all_vars VARIABLES)
list(SORT all_vars)  # 排序便于查看
message("All variables:n${all_vars}")

# 打印所有目标(可执行文件、库等)
get_cmake_property(all_targets BUILDSYSTEM_TARGETS)
message("All targets:n${all_targets}")

# 打印所有包含的目录
get_cmake_property(all_dirs SUBDIRECTORIES)
message("All subdirectories:n${all_dirs}")

4.4.2.查询目标属性(get_target_property())

用来获取特定目标(比如可执行文件、库)的属性(包含目录、链接库、编译选项等),排查目标配置问题。

常用属性INCLUDE_DIRECTORIES(包含目录)、LINK_LIBRARIES(链接库)、COMPILE_DEFINITIONS(编译宏)、SOURCES(源文件列表)等。

示例

# 假设已创建目标 "my_app"
add_executable(my_app main.cpp)

# 查看目标的包含目录
get_target_property(inc_dirs my_app INCLUDE_DIRECTORIES)
message("my_app include dirs: ${inc_dirs}")

# 查看目标的链接库
get_target_property(link_libs my_app LINK_LIBRARIES)
message("my_app linked libs: ${link_libs}")

# 查看目标的编译选项
get_target_property(compile_opts my_app COMPILE_OPTIONS)
message("my_app compile options: ${compile_opts}")

4.4.3.批量打印属性(cmake_print_properties())

用来批量打印指定类型(目标、源文件、目录等)的属性,比 get_target_property() 高效得多。

支持的类型TARGETSSOURCESDIRECTORIESTESTS 等。

示例

# 打印目标 "my_app" 的所有属性
cmake_print_properties(
  TARGETS my_app
  PROPERTIES INCLUDE_DIRECTORIES LINK_LIBRARIES COMPILE_DEFINITIONS
)

# 打印当前目录的属性(如 CMAKE_CURRENT_SOURCE_DIR)
cmake_print_properties(
  DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}
  PROPERTIES CMAKE_CURRENT_SOURCE_DIR CMAKE_CURRENT_BINARY_DIR
)

4.4.4.输出到文件(file(WRITE))

当调试信息太多,控制台输出乱成一团时,可以把信息写入文件,再慢慢查看。

示例

# 将所有变量写入文件
get_cmake_property(all_vars VARIABLES)
list(SORT all_vars)
file(WRITE "${CMAKE_BINARY_DIR}/cmake_vars.txt" "All variables:n${all_vars}")

# 将目标属性写入文件
get_target_property(inc_dirs my_app INCLUDE_DIRECTORIES)
file(APPEND "${CMAKE_BINARY_DIR}/my_app_info.txt" "Include dirs: ${inc_dirs}n")

5.详细跟踪 CMake 执行流程:--debug-output与--trace

5.1.--debug-output:输出调试级信息

显示 CMake 内部的调试信息(比如变量查找、缓存读取),但不会显示所有命令:

cmake --debug-output ..  # 在构建目录执行,.. 是源码目录

CMake常见的调试技巧

5.2.--trace:跟踪所有执行的命令(最详细)

输出每一行执行的 CMake 命令(包括 ifsetinclude 等),适合追踪脚本执行的完整路径:

cmake --trace ..  # 输出所有命令(可能非常多,建议重定向到文件)
cmake --trace .. > cmake_trace.log  # 保存到文件,方便搜索
cmake --trace-expand .. > cmake_trace_expanded.log
  • 启用后会打印 CMake 执行的每一行脚本,是终极调试手段,当然输出量也巨大。
  • cmake --trace .: 基本跟踪。
  • cmake --trace-expand .: 跟踪并展开所有变量。这是查看变量实际值如何被代入命令的最强方式。
  • 可以指定跟踪范围 --trace-source=, --trace-redirect=

5.3.--warn-uninitialized

  • 警告使用了未显式初始化(未设置)的变量,这个选项非常有用!
  • cmake --warn-uninitialized .

5.4.--graphviz=(CMake 2.8.10+)

  • 生成一个 .dot 文件,可视化显示目标之间的依赖关系图
  • cmake --graphviz=graph.dot .,然后用 Graphviz 工具(比如 dot -Tpng graph.dot -o graph.png)生成图片查看。

6.调试find_package依赖查找失败

find_package 找不到依赖是很多人的噩梦(路径错误、版本不匹配,原因很多),可以通过以下方法定位:

1.启用查找调试模式(CMAKE_FIND_DEBUG_MODE)

CMAKE_FIND_DEBUG_MODE 设为 ON,CMake 会输出 find_package 查找依赖的详细过程(搜索路径、检查的文件、匹配的版本等):

# 在 find_package 前设置(临时生效)
set(CMAKE_FIND_DEBUG_MODE ON)
find_package(SomeLib REQUIRED)
set(CMAKE_FIND_DEBUG_MODE OFF)  # 用完关闭,避免输出过多

示例输出(关键部分):

CMAKE_FIND_DEBUG_MODE: FIND_PACKAGE(SomeLib)
CMAKE_FIND_DEBUG_MODE:   Checking prefixes: /usr/local, /usr, ...
CMAKE_FIND_DEBUG_MODE:   Looking for SomeLibConfig.cmake in .../lib/cmake/SomeLib
CMAKE_FIND_DEBUG_MODE:   Found SomeLibConfig.cmake at /usr/lib/cmake/SomeLib
...

2.检查 SomeLib_DIR 缓存变量

  • 通过 CMake GUI 或 cmake -L 查看缓存变量,确保 SomeLib_DIR 指向了包含 SomeLibConfig.cmake 的正确目录。

3.手动检查模块路径

  • 打印 CMAKE_MODULE_PATHCMAKE_PREFIX_PATH,查看自定义查找路径。
  • 检查标准路径(/usr/lib/cmake/SomeLib, /usr/local/lib/cmake/SomeLib 等)。

7.使用 CMake GUI /ccmake

1.cmake-gui (图形界面)

  • 可视化缓存变量: 一目了然地看到所有缓存变量的当前值(包括类型和描述)。
  • 修改和重新配置: 方便地修改变量值(比如 CMAKE_BUILD_TYPE, BUILD_SHARED_LIBS, 库路径等),点一下 “Configure” 就能看到效果。
  • 查看生成输出: 界面下方有输出日志窗口。
  • 分组和搜索: 管理大量变量非常方便。

2.ccmake (终端 curses 界面)

在终端中提供类似 cmake-gui 的交互式缓存变量编辑功能,对于远程开发或无 GUI 环境特别有用。

基本操作:

  • cg 进行配置/生成。
  • t 切换高级变量显示。
  • ? 查看帮助。
  • 方向键移动,Enter 编辑变量。

8.调试工具链文件 (CMAKE_TOOLCHAIN_FILE)

  • 大量使用 message() 在工具链文件的关键位置(设置编译器标志、路径、平台变量前后)打印信息。
  • 检查环境变量: 工具链文件经常依赖环境变量(PATH, CC, CXX, SDKROOT 等),确保它们设置正确,并在工具链文件中打印出来。
  • 验证编译器: 在工具链文件末尾或之后添加:
message(STATUS "CMAKE_C_COMPILER = ${CMAKE_C_COMPILER}")
message(STATUS "CMAKE_CXX_COMPILER = ${CMAKE_CXX_COMPILER}")
enable_language(C CXX) # 强制尝试检测编译器

9.用VS2022单步调试CMakeList.txt工程

9.1.CMake版本说明

用 VS2022 单步调试 CMake 脚本,CMake 最低版本要求是 3.27.0

CMake 3.27 是官方首次正式推出 CMake 调试器(Debug Adapter Protocol)的版本。

  • 只有这个版本及以上,才支持 VS2022 的断点、单步、变量查看功能。
  • 这是 VS 调试 CMake 脚本的底层依赖,绕不过去。

VS2022 自带的 CMake 版本

  • VS2022 17.4 及以上版本自带 CMake 3.27+(开箱即用,无需手动安装)。
  • 如果你用的是新版 VS2022,直接用内置的 CMake 就能调试。
  • 老版 VS2022 自带的是低版本 CMake,必须升级

9.2.调试步骤

1.用 VS2022 打开 CMake 工程,以 fineftp-server 为例,下载源码后直接打开它。

2.在 CMakeList.txt 中设置断点。

CMake常见的调试技巧

选中 CMakeList.txt 文件,右键弹出菜单,选择 "使用CMake调试程序配置缓存",程序会停在断点位置。在局部变量窗口,可以看到 CMake 缓存变量的值,如下图所示:

CMake常见的调试技巧

3.调试查找第三方库 find_package。

当程序执行到 find_package(asio REQUIRED) 时,单步往下走,就能看到 asio 相关的 CMake 变量,从而判断查找是否成功。成功查找的效果如下:

CMake常见的调试技巧

可以说,这种可视化的调试方式,是目前检查 CMakeLists.txt 正确性最直观、最有效的方法。

10.用VSCode单步调试CMakeList.txt工程

使用 VSCode 调试 CMake,必须安装 CMake Tools 插件,如下图:

CMake常见的调试技巧

10.1.CMake版本说明

同 9.1。

10.2.调试步骤

1.以 fineftp-server 为例,同样用 VSCode 打开 fineftp-server 源码文件夹,如下图所示:

CMake常见的调试技巧

2.在 CMakeList.txt 文件中设置断点。

选中 CMakeList.txt 文件,右键弹出菜单,选择 "使用CMake调试器清理重新配置所有项目",程序会停在断点位置。在局部变量窗口,可以看到 CMake 缓存变量的值,如下图所示:

CMake常见的调试技巧

3.调试查找第三方库 find_package。

当程序执行到 find_package(asio REQUIRED) 时,单步往下走,就能看到 asio 相关的 CMake 变量,从而判断查找是否成功。成功查找的效果如下:

CMake常见的调试技巧

其实方法都和 VS2022 差不多,熟悉一个,另一个也就上手了。

11.检查编译器 / 平台兼容性:日志文件

CMake 在配置时会执行各种编译测试(比如检查编译器特性、库能否链接),结果会记录在以下日志文件中,对于调试“编译失败”“特性检测错误”非常有帮助:

  • CMakeFiles/CMakeOutput.log:记录成功的编译测试(比如“编译器支持 C++17”)。
  • CMakeFiles/CMakeError.log:记录失败的编译测试(比如“链接库时未找到符号”“编译器不支持某特性”)。

用法:当遇到“某特性被误判不支持”或“库链接失败”时,打开这两个文件,搜索具体的测试代码和错误信息(比如编译器报错),通常就能定位问题(比如缺少头文件、库路径错误)。

12.验证条件判断逻辑

CMake 的 if() 条件判断支持多种语法(版本比较、变量存在性、路径检查等)。如果分支逻辑异常,可以直接输出条件表达式的结果:

# 检查变量是否存在(NOT DEFINED)
message(STATUS "MY_VAR 是否未定义: $>")  # 生成器表达式(CMake 3.15+)

# 检查版本比较结果
set(CMAKE_VERSION_STR "3.20.0")
message(STATUS "版本是否 >=3.10: ${CMAKE_VERSION_STR VERSION_GREATER_EQUAL 3.10}")  # 输出 TRUE/FALSE

# 检查路径是否存在
message(STATUS "src 目录是否存在: ${EXISTS ${CMAKE_SOURCE_DIR}/src}")

13.其他实用技巧

1.使用 VERBOSE 构建输出编译命令

配置时设置 CMAKE_VERBOSE_MAKEFILEON,构建时就会输出详细的编译/链接命令。这能帮你检查是否使用了正确的头文件路径、库路径、宏定义:

set(CMAKE_VERBOSE_MAKEFILE ON)  # 在 CMakeLists.txt 中设置

或者构建时临时启用:

make VERBOSE=1  # 或 ninja -v

2.用 try_compile/try_run 调试编译特性

当需要验证“某段代码是否能编译/运行”时,用 try_compile(编译测试)或 try_run(运行测试),并输出结果:

# 测试代码是否能编译(如检查是否支持 std::optional)
try_compile(
    SUPPORT_OPTIONAL
    ${CMAKE_BINARY_DIR}/test  # 测试目录
    SOURCES ${CMAKE_SOURCE_DIR}/test/optional_test.cpp  # 测试代码
)
message(STATUS "是否支持 std::optional: ${SUPPORT_OPTIONAL}")

3.缩小范围:逐步注释代码

如果脚本复杂,可以逐步注释掉部分代码(比如 includefind_package、条件分支),定位到具体是哪段代码导致了异常。类似“二分法”调试的思路,很实用。

14.总结

CMake 调试的核心,说到底就是三件事:验证变量值跟踪执行流程检查依赖查找过程。常用的工具链包括:

  • message() 输出变量和分支状态;
  • cmake -L 查看缓存变量;
  • --trace/--debug-output 跟踪命令执行;
  • CMAKE_FIND_DEBUG_MODE 调试依赖查找;
  • 日志文件(CMakeOutput.log/CMakeError.log)分析编译测试。

根据问题的复杂程度,由浅入深地运用这些技巧,大部分 CMake 配置问题都能有效定位和解决。最后提醒一句:调试完成后,记得清理或注释掉调试用的 message() 语句,保持 CMakeLists.txt 的整洁。

本文转载于:https://www.jb51.net/program/3667080dn.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注