CMake 从入门到精通:完整教程 / 第 17 章:问题排查
第 17 章:问题排查
17.1 配置阶段错误
17.1.1 CMake 版本不匹配
错误信息:
CMake Error at CMakeLists.txt:1 (cmake_minimum_required):
CMake 3.22 or higher is required. You are running version 3.16.3.
解决方案:
# 检查版本
cmake --version
# 升级 CMake
pip install cmake --upgrade
# 或
sudo snap install cmake --classic
# 或降低项目要求
cmake_minimum_required(VERSION 3.16)
17.1.2 编译器未找到
错误信息:
CMake Error: Could not find compiler set in environment variable CXX:
g++-15
解决方案:
# 检查编译器
which g++
g++ --version
# 设置正确的编译器
export CXX=g++
export CC=gcc
# 或在 CMake 中指定
cmake -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER=gcc
# 或使用工具链文件
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake
17.1.3 find_package 找不到库
错误信息:
CMake Error: Could not find a package configuration file provided by "OpenSSL"
排查步骤:
# 1. 检查库是否安装
dpkg -l | grep openssl
# 或
apt list --installed 2>/dev/null | grep openssl
# 2. 检查开发包是否安装
apt list --installed 2>/dev/null | grep libssl-dev
# 3. 设置搜索路径
cmake -DCMAKE_PREFIX_PATH=/usr/local/ssl
# 或
cmake -DOPENSSL_ROOT_DIR=/usr/local/ssl
# 4. 使用调试模式查看查找过程
cmake --debug-find-pkg=OpenSSL -S . -B build
# 5. 查看缓存变量
cmake -S . -B build -L | grep -i openssl
17.1.4 源文件未找到
错误信息:
CMake Error: Cannot find source file:
src/main.cpp
解决方案:
# 检查路径是否相对于 CMakeLists.txt 所在目录
# 正确
add_executable(app ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp)
# 或使用相对路径(相对于当前 CMakeLists.txt)
add_executable(app src/main.cpp)
# 检查文件是否存在
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
message(FATAL_ERROR "src/main.cpp 不存在!")
endif()
17.1.5 生成器不支持
错误信息:
CMake Error: Could not create named generator Ninja
解决方案:
# 查看可用生成器
cmake --help | grep generators
# 安装 Ninja
apt install ninja-build
# 或使用 Make
cmake -G "Unix Makefiles"
17.2 构建阶段错误
17.2.1 编译错误
常见错误:
fatal error: some_header.h: No such file or directory
解决方案:
# 添加包含目录
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_BINARY_DIR} # 生成的头文件
)
# 使用 SYSTEM 避免警告
target_include_directories(mylib SYSTEM PUBLIC
/usr/local/include
)
# 检查是否链接了正确的库
target_link_libraries(myapp PRIVATE correct_lib)
17.2.2 链接错误
错误信息:
undefined reference to `some_function()'
排查步骤:
# 1. 检查符号是否存在
nm -C libsome.a | grep some_function
# 或
readelf -s libsome.so | grep some_function
# 2. 检查链接顺序
# 静态库链接顺序很重要:被依赖者在后面
target_link_libraries(app PRIVATE mylib dep_lib)
# 3. 检查是否遗漏链接库
# 常见遗漏:pthread, m, dl, rt
target_link_libraries(app PRIVATE Threads::Threads)
# 4. 检查 C++ 名称修饰
# 确保 C 和 C++ 库正确混合使用
extern "C" {
#include "c_header.h"
}
链接库顺序问题:
# 错误:依赖者在前面
target_link_libraries(app PRIVATE base mylib) # mylib 依赖 base
# 正确:被依赖者在后面
target_link_libraries(app PRIVATE mylib base)
# 或使用链接组解决循环依赖
target_link_libraries(app PRIVATE
-Wl,--start-group
libA libB
-Wl,--end-group
)
17.2.3 未定义符号错误
错误信息:
CMake Error: Target "mylib" has dependency on "OpenSSL::SSL"
but target was not found.
解决方案:
# 确保 find_package 在使用前调用
find_package(OpenSSL REQUIRED)
target_link_libraries(mylib PUBLIC OpenSSL::SSL)
# 或检查目标是否存在
if(TARGET OpenSSL::SSL)
target_link_libraries(mylib PUBLIC OpenSSL::SSL)
else()
message(WARNING "OpenSSL 未找到")
endif()
17.2.4 重复定义错误
错误信息:
multiple definition of `some_function'
解决方案:
# 检查是否重复链接同一库
target_link_libraries(app PRIVATE mylib)
target_link_libraries(app PRIVATE mylib) # 重复!
# 检查头文件是否在多个源文件中定义了全局变量
# 使用 extern 声明
# header.h
extern int global_var; # 声明
# source.cpp
int global_var = 42; # 定义
17.3 运行时错误
17.3.1 共享库找不到
错误信息:
error while loading shared libraries: libmylib.so: cannot open shared object file
解决方案:
# 设置 RPATH
set_target_properties(myapp PROPERTIES
INSTALL_RPATH "$ORIGIN/../lib"
INSTALL_RPATH_USE_LINK_PATH TRUE
)
# 构建时设置 RPATH
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
# 或禁用 RPATH(不推荐)
set(CMAKE_SKIP_RPATH TRUE)
set(CMAKE_SKIP_INSTALL_RPATH TRUE)
# 临时解决方案
export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH
# 永久解决方案
echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/myapp.conf
sudo ldconfig
17.3.2 程序找不到资源文件
解决方案:
# 定义数据目录
target_compile_definitions(myapp PRIVATE
DATA_DIR="${CMAKE_INSTALL_PREFIX}/share/myapp"
)
# 或使用相对于可执行文件的路径
set_target_properties(myapp PROPERTIES
INSTALL_RPATH "$ORIGIN"
)
# 在代码中获取可执行文件路径
# Linux: readlink /proc/self/exe
# macOS: _NSGetExecutablePath
# Windows: GetModuleFileName
17.4 变量调试
17.4.1 打印变量值
# 打印单个变量
message(STATUS "MY_VAR = ${MY_VAR}")
# 打印多个变量
message(STATUS "
项目配置:
PROJECT_NAME: ${PROJECT_NAME}
PROJECT_VERSION: ${PROJECT_VERSION}
CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}
CMAKE_CXX_COMPILER: ${CMAKE_CXX_COMPILER}
CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}
")
# 条件打印
if(VERBOSE_CONFIG)
message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}")
message(STATUS "CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}")
endif()
17.4.2 列出所有变量
# 在 CMakeLists.txt 中
get_cmake_property(_vars VARIABLES)
foreach(_var ${_vars})
message(STATUS "${_var} = ${${_var}}")
endforeach()
# 命令行方式
cmake -S . -B build -L # 用户变量
cmake -S . -B build -LA # 包含高级变量
17.4.3 条件断点
# CMake 没有断点,但可以模拟
function(debug_var var_name)
if(DEBUG_CMAKE)
message(STATUS "[DEBUG] ${var_name} = ${${var_name}}")
if(${ARGC} GREATER 1)
message(STATUS "[DEBUG] ${ARGV1}")
endif()
endif()
endfunction()
debug_var(CMAKE_BUILD_TYPE)
debug_var(PROJECT_VERSION "项目版本")
17.4.4 缓存变量问题
# 查看 CMakeCache.txt
cat build/CMakeCache.txt | grep -i my_var
# 删除缓存重新配置
rm -rf build
cmake -S . -B build
# 只删除特定变量
cmake -S . -B build -UMY_VAR
# 强制覆盖
cmake -S . -B build -DMY_VAR=new_value -DMY_VAR:STRING=new_value
17.5 生成器表达式调试
17.5.1 无法直接打印
# 不能在配置阶段打印生成器表达式
message(STATUS "$<CONFIG>") # 输出:$<CONFIG>(字面量)
# 使用 file(GENERATE) 来调试
file(GENERATE
OUTPUT "${CMAKE_BINARY_DIR}/debug_config.txt"
CONTENT "Config: $<CONFIG>\nCompiler: $<CXX_COMPILER_ID>\n"
)
# 构建后查看
cat build/debug_config.txt
17.5.2 使用自定义目标调试
add_custom_target(debug_genex
COMMAND ${CMAKE_COMMAND} -E echo "Config: $<CONFIG>"
COMMAND ${CMAKE_COMMAND} -E echo "Compiler: $<CXX_COMPILER_ID>"
COMMAND ${CMAKE_COMMAND} -E echo "Platform: $<PLATFORM_ID>"
COMMENT "调试生成器表达式"
)
17.6 目标属性调试
17.6.1 检查目标属性
# 打印目标的所有属性
function(dump_target target)
message(STATUS "=== 目标: ${target} ===")
get_target_property(type ${target} TYPE)
message(STATUS " 类型: ${type}")
get_target_property(srcs ${target} SOURCES)
message(STATUS " 源文件: ${srcs}")
get_target_property(dirs ${target} INCLUDE_DIRECTORIES)
message(STATUS " 包含目录: ${dirs}")
get_target_property(libs ${target} LINK_LIBRARIES)
message(STATUS " 链接库: ${libs}")
get_target_property(defs ${target} COMPILE_DEFINITIONS)
message(STATUS " 编译定义: ${defs}")
get_target_property(opts ${target} COMPILE_OPTIONS)
message(STATUS " 编译选项: ${opts}")
endfunction()
dump_target(myapp)
17.6.2 检查传递依赖
# 递归检查目标依赖
function(dump_target_deps target depth)
math(EXPR next_depth "${depth} + 1")
string(REPEAT " " ${depth} indent)
message(STATUS "${indent}目标: ${target}")
get_target_property(libs ${target} LINK_LIBRARIES)
if(libs)
foreach(lib ${libs})
if(TARGET ${lib})
dump_target_deps(${lib} ${next_depth})
endif()
endforeach()
endif()
endfunction()
dump_target_deps(myapp 0)
17.7 构建日志调试
17.7.1 详细构建输出
# CMake 配置时的详细输出
cmake -S . -B build --log-level=VERBOSE
cmake -S . -B build --log-level=DEBUG
cmake -S . -B build --log-level=TRACE
# 构建时的详细输出
cmake --build build --verbose
# 或
make VERBOSE=1
# 或
ninja -v
# CTest 详细输出
ctest --test-dir build -V
ctest --test-dir build -VV
17.7.2 编译命令数据库
# 生成 compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# 使用 Ninja 生成器时自动产生
cmake -S . -B build -G Ninja
# build/compile_commands.json 包含所有编译命令
# 查看特定文件的编译命令
cat build/compile_commands.json | jq '.[] | select(.file | contains("main.cpp"))'
# 或使用 bear
bear -- make
17.8 常见陷阱和解决方案
17.8.1 变量作用域问题
# 问题:在子目录中设置的变量不影响父目录
# sub/CMakeLists.txt
set(MY_VAR "value") # 仅在 sub/ 有效
# 解决:使用 PARENT_SCOPE
set(MY_VAR "value" PARENT_SCOPE)
# 或使用缓存变量
set(MY_VAR "value" CACHE STRING "描述")
17.8.2 include 与 add_subdirectory 区别
# add_subdirectory: 创建新作用域
add_subdirectory(sub)
# sub 中的变量不会影响当前作用域
# include: 在当前作用域执行
include(sub/CMakeLists.txt)
# sub 中的变量会影响当前作用域(注意!)
17.8.3 file(GLOB) 不自动更新
# 问题:添加新文件后需要重新运行 cmake
file(GLOB SOURCES "src/*.cpp")
# 解决方案一:手动列出文件(推荐)
set(SOURCES
src/main.cpp
src/utils.cpp
src/network.cpp
)
# 解决方案二:使用 CONFIGURE_DEPENDS(CMake 3.12+)
file(GLOB SOURCES CONFIGURE_DEPENDS "src/*.cpp")
17.8.4 目标名称冲突
# 问题:多个目录使用相同的目标名
# dir1/CMakeLists.txt
add_library(utils src/utils.cpp) # 冲突!
# 解决:使用命名空间或前缀
add_library(myproject_dir1_utils src/utils.cpp)
add_library(MyProject::dir1::utils ALIAS myproject_dir1_utils)
17.9 调试工具
17.9.1 CMake 内置调试
# 调试模式
cmake --trace-expand # 展开所有变量
cmake --trace-source=file.cmake # 追踪特定文件
# 性能分析
cmake --profiling-format=google-trace --profiling-output=trace.json
# 使用 chrome://tracing 查看
17.9.2 cmake_print_variables
include(CMakePrintHelpers)
cmake_print_variables(CMAKE_BUILD_TYPE PROJECT_VERSION)
cmake_print_target_properties(myapp)
17.9.3 try_compile
# 测试编译功能
try_compile(HAS_CXX17
${CMAKE_BINARY_DIR}/test
SOURCES ${CMAKE_SOURCE_DIR}/cmake/test_cxx17.cpp
CXX_STANDARD 17
)
if(HAS_CXX17)
message(STATUS "编译器支持 C++17")
else()
message(FATAL_ERROR "需要支持 C++17 的编译器")
endif()
17.10 业务场景
场景:系统化问题排查流程
问题出现
│
├── 1. 配置阶段错误
│ ├── 检查 CMake 版本
│ ├── 检查编译器
│ ├── 检查 find_package 结果
│ └── 查看 CMakeCache.txt
│
├── 2. 构建阶段错误
│ ├── 编译错误 → 检查包含目录、编译选项
│ ├── 链接错误 → 检查链接库、链接顺序
│ └── 使用 --verbose 查看完整命令
│
├── 3. 运行时错误
│ ├── 共享库找不到 → 检查 RPATH
│ ├── 资源文件找不到 → 检查路径
│ └── 使用 ldd 检查依赖
│
└── 4. 调试技巧
├── 清理重新构建:rm -rf build
├── 详细输出:--verbose
├── 变量检查:message()
└── 目标检查:dump_target()
17.11 注意事项
| 问题 | 常见原因 | 解决方案 |
|---|---|---|
| 配置失败 | 版本不匹配/依赖缺失 | 升级 CMake/安装依赖 |
| 编译失败 | 头文件路径/编译选项 | 检查 target_include_directories |
| 链接失败 | 库未链接/顺序错误 | 检查 target_link_libraries |
| 运行时失败 | RPATH/动态库路径 | 设置 INSTALL_RPATH |
| 测试失败 | 工作目录/环境变量 | 设置 WORKING_DIRECTORY |
17.12 扩展阅读
上一章:第 16 章 — Docker 与 CI/CD | 下一章:第 18 章 — 最佳实践 →