CMake 系列教程(五):进阶技巧
从"够用"到"精通"------生成器表达式、Presets、安装打包与自定义命令
一、生成器表达式(Generator Expressions)
1.1 什么是生成器表达式?
生成器表达式是 CMake 中一种延迟求值 的语法,在构建阶段 (而非配置阶段)才展开。语法为 $<...>。
为什么需要延迟求值?因为有些信息在配置阶段无法确定:
- 多配置生成器(Visual Studio)同时存在 Debug 和 Release,配置阶段不知道最终用哪个
- 头文件路径在安装前后不同
- 不同配置需要不同的编译选项
1.2 基本形式
$<条件:真值> # 条件为真时展开为"真值",否则为空
$<条件:真值,假值> # 三元表达式
$<表达式> # 直接求值
1.3 逻辑判断
cmake
# 根据编译器选择选项
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-Wall>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
# 根据构建类型选择选项
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O3>
)
展开示例(GCC + Debug):
$<$<CXX_COMPILER_ID:GNU>:-Wall> → -Wall
$<$<CXX_COMPILER_ID:MSVC>:/W4> → (空)
$<$<CONFIG:Debug>:-g -O0> → -g -O0
$<$<CONFIG:Release>:-O3> → (空)
1.4 常用生成器表达式
编译器相关
| 表达式 | 说明 |
|---|---|
$<CXX_COMPILER_ID> |
编译器 ID(GNU, Clang, MSVC 等) |
$<CXX_COMPILER_VERSION> |
编译器版本 |
$<COMPILE_LANGUAGE> |
当前编译的语言(CXX, C, CUDA 等) |
构建配置
| 表达式 | 说明 |
|---|---|
$<CONFIG> |
当前构建类型(Debug, Release 等) |
$<IF:cond,a,b> |
三元条件(CMake 3.8+) |
目标属性
| 表达式 | 说明 |
|---|---|
$<TARGET_FILE:tgt> |
目标的输出文件路径 |
$<TARGET_FILE_NAME:tgt> |
目标的输出文件名 |
$<TARGET_PROPERTY:tgt,prop> |
目标属性值 |
接口与安装
| 表达式 | 说明 |
|---|---|
$<BUILD_INTERFACE:...> |
构建时使用的值 |
$<INSTALL_INTERFACE:...> |
安装后使用的值 |
1.5 实战:跨平台编译选项
cmake
function(set_modern_compile_options target)
# C++ 标准
target_compile_features(${target} PUBLIC cxx_std_20)
# 跨平台警告选项
target_compile_options(${target} PRIVATE
# GCC / Clang
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:
-Wall -Wextra -Wpedantic -Wshadow
>
# MSVC
$<$<CXX_COMPILER_ID:MSVC>:
/W4 /utf-8 /permissive-
>
)
# Debug 专用选项
target_compile_options(${target} PRIVATE
$<$<AND:$<CXX_COMPILER_ID:GNU,Clang,AppleClang>,$<CONFIG:Debug>>:
-g3 -fsanitize=address -fsanitize=undefined
>
)
# Release 专用选项
target_compile_options(${target} PRIVATE
$<$<AND:$<CXX_COMPILER_ID:GNU,Clang>,$<CONFIG:Release>>:
-O3 -DNDEBUG
>
)
endfunction()
二、CMakePresets:可复现的构建配置
2.1 为什么需要 Presets?
不同开发者、不同环境,构建参数可能不同:
bash
# 开发者 A
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
# 开发者 B
cmake -B build -G "Visual Studio 17 2022" -A x64
# CI
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
Presets 把这些参数固化到 CMakePresets.json,一键配置。
2.2 基本结构
在项目根目录创建 CMakePresets.json:
json
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 25,
"patch": 0
},
"configurePresets": [
{
"name": "base",
"hidden": true,
"binaryDir": "${sourceDir}/build/${presetName}",
"generator": "Ninja"
},
{
"name": "debug",
"displayName": "Debug",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"displayName": "Release",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "release",
"configurePreset": "release"
}
]
}
2.3 使用 Presets
bash
# 列出可用 presets
cmake --list-presets
# 配置
cmake --preset debug
# 构建
cmake --build --preset debug
# 一键构建(配置+构建)
cmake --preset debug && cmake --build --preset debug
2.4 完整示例:多平台 Presets
json
{
"version": 6,
"configurePresets": [
{
"name": "common",
"hidden": true,
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "ninja-debug",
"displayName": "Ninja Debug",
"inherits": "common",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_COMPILER": "clang++"
}
},
{
"name": "ninja-release",
"displayName": "Ninja Release",
"inherits": "common",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "vs2022",
"displayName": "Visual Studio 2022",
"inherits": "common",
"generator": "Visual Studio 17 2022",
"architecture": "x64"
}
],
"buildPresets": [
{
"name": "ninja-debug",
"configurePreset": "ninja-debug"
},
{
"name": "ninja-release",
"configurePreset": "ninja-release"
},
{
"name": "vs2022-debug",
"configurePreset": "vs2022",
"configuration": "Debug"
},
{
"name": "vs2022-release",
"configurePreset": "vs2022",
"configuration": "Release"
}
],
"testPresets": [
{
"name": "ninja-debug",
"configurePreset": "ninja-debug",
"output": {
"outputOnFailure": true
}
}
]
}
💡
CMakePresets.json提交到 Git,团队统一配置。CMakeUserPresets.json是用户自定义扩展,不提交。
三、安装与打包
3.1 install 命令
cmake
include(GNUInstallDirs)
# 安装可执行文件
install(TARGETS myapp
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # Windows 可执行文件
)
# 安装库文件
install(TARGETS mylib
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} # 静态库
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # 动态库(Linux)
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # DLL(Windows)
)
# 安装头文件
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp"
)
# 安装导出目标(供其他项目 find_package 使用)
install(EXPORT mylibTargets
FILE mylibTargets.cmake
NAMESPACE mylib::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mylib
)
3.2 执行安装
bash
# 安装到默认路径(/usr/local)
cmake --install build
# 安装到自定义路径
cmake --install build --prefix /opt/myapp
# Windows 上指定配置
cmake --install build --config Release
3.3 CPack 打包
CPack 是 CMake 内置的打包工具,可生成多种安装包格式:
cmake
# 在 CMakeLists.txt 末尾添加
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_DESCRIPTION "A demo application")
set(CPACK_PACKAGE_VENDOR "MyCompany")
# 不同平台默认格式
# Linux: TGZ (tar.gz), DEB, RPM
# Windows: NSIS (.exe), ZIP, WIX (.msi)
# macOS: DragNDrop (.dmg), TGZ
# 启用多种格式
set(CPACK_GENERATOR "TGZ;ZIP")
# DEB 包额外设置
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libstdc++6")
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
# NSIS 额外设置
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
include(CPack)
构建安装包:
bash
# 方式一
cd build
cpack
# 方式二(推荐)
cpack --build build
四、自定义命令与自定义目标
4.1 add_custom_command
用于在构建过程中执行自定义步骤。
生成代码
cmake
# 用 protoc 生成 C++ 文件
find_package(Protobuf REQUIRED)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/message.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/message.pb.h
COMMAND protobuf::protoc
--cpp_out=${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/proto/message.proto
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/proto/message.proto
COMMENT "Generating protobuf C++ files"
)
# 将生成文件加入目标
add_executable(myapp main.cpp ${CMAKE_CURRENT_BINARY_DIR}/message.pb.cc)
构建后操作
cmake
add_custom_command(
TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:myapp>
${CMAKE_SOURCE_DIR}/bin/
COMMENT "Copying myapp to bin/"
)
4.2 add_custom_target
创建一个始终需要构建的目标(始终过期),通常用于触发 add_custom_command:
cmake
# 定义代码生成步骤
add_custom_command(
OUTPUT generated.cpp
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/codegen.py
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/codegen.py
COMMENT "Running code generator"
)
# 创建自定义目标,让用户可以手动触发
add_custom_target(generate_code
DEPENDS generated.cpp
)
# 让 myapp 依赖代码生成
add_executable(myapp main.cpp generated.cpp)
add_dependencies(myapp generate_code)
bash
# 手动触发代码生成
cmake --build build --target generate_code
4.3 cmake -E:跨平台命令
CMake 自带跨平台命令工具 cmake -E,避免平台差异:
| 命令 | 作用 |
|---|---|
cmake -E copy <src> <dst> |
复制文件 |
cmake -E copy_directory <src> <dst> |
复制目录 |
cmake -E make_directory <dir> |
创建目录 |
cmake -E remove <file> |
删除文件 |
cmake -E echo <text> |
输出文本 |
cmake -E env VAR=value cmd |
设置环境变量执行命令 |
cmake -E tar cfz out.tar.gz dir/ |
压缩目录 |
五、CTest:测试集成
5.1 基本用法
cmake
enable_testing()
add_executable(test_math tests/test_math.cpp)
target_link_libraries(test_math PRIVATE math)
add_test(
NAME test_math
COMMAND test_math
)
5.2 运行测试
bash
# 运行所有测试
ctest --test-dir build
# 详细输出
ctest --test-dir build --output-on-failure
# 并行运行
ctest --test-dir build -j8
# 仅运行匹配名称的测试
ctest --test-dir build -R "math"
# 排除测试
ctest --test-dir build -E "slow"
5.3 集成 Google Test
cmake
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
add_executable(test_math tests/test_math.cpp)
target_link_libraries(test_math PRIVATE math GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(test_math) # 自动注册每个 TEST_F 为独立测试
六、编译命令数据库
6.1 compile_commands.json
现代 C/C++ 工具(clangd、clang-tidy、IDE)依赖 compile_commands.json 获取编译选项。
cmake
# CMakeLists.txt 中添加
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
或在配置时指定:
bash
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
文件生成在 build/compile_commands.json,IDE/编辑器通常需要链接到项目根目录:
bash
ln -s build/compile_commands.json .
七、最佳实践清单
7.1 Do ✅
| 实践 | 原因 |
|---|---|
用 target_* 命令而非全局 CMAKE_CXX_FLAGS |
精确控制,避免全局污染 |
| 显式列出源文件 | file(GLOB) 不会自动检测新文件 |
用 CMAKE_CURRENT_SOURCE_DIR 而非 CMAKE_SOURCE_DIR |
支持项目被 add_subdirectory 引入 |
缓存变量用 CACHE 关键字 |
支持命令行覆盖 -DVAR=VALUE |
使用 FetchContent 时锁定版本号 |
可重现构建 |
提供 CMakePresets.json |
团队统一构建配置 |
| 为库提供 Config 文件和 install 规则 | 方便他人 find_package |
7.2 Don't ❌
| 反模式 | 正确做法 |
|---|---|
include_directories() |
target_include_directories() |
link_directories() |
target_link_libraries() |
add_definitions() |
target_compile_definitions() |
aux_source_directory() |
显式列出源文件 |
全局 CMAKE_CXX_STANDARD |
target_compile_features() |
file(GLOB ...) 收集源文件 |
手动列出 |
在 cmake_minimum_required 中写 VERSION 2.8 |
使用 3.20+ |
八、调试技巧
8.1 查看变量值
cmake
# 输出变量
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
# 查看所有缓存变量
get_cmake_property(cache_vars CACHE_VARIABLES)
foreach(var ${cache_vars})
message(STATUS "${var} = ${${var}}")
endforeach()
8.2 查看目标属性
cmake
# 查看目标的所有接口包含目录
get_property(includes TARGET mylib PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "mylib includes: ${includes}")
# 查看目标的链接库
get_property(libs TARGET myapp PROPERTY LINK_LIBRARIES)
message(STATUS "myapp links: ${libs}")
8.3 命令行调试
bash
# 调试 find_package 搜索过程
cmake -B build --debug-find
# 查看详细配置过程
cmake -B build --trace-source=CMakeLists.txt
# 查看所有 trace
cmake -B build --trace
# 仅展开变量
cmake -B build --trace-expand
九、推荐资源
| 资源 | 链接 | 说明 |
|---|---|---|
| 官方文档 | cmake.org/documentation | 最权威的参考 |
| Modern CMake | cliutils.gitlab.io/modern-cmake | 社区经典指南 |
| CMake Best Practices | Packt 出版 | 系统化进阶 |
| Effective CMake | YouTube (Daniel Pfeifer) | 演讲视频,经典入门 |
| it's time to do CMake right | Pablo Arias 博客 | 现代最佳实践 |
系列总结
五篇文章,从入门到进阶,我们系统学习了 CMake 的核心知识:
| 篇目 | 核心内容 | 关键收获 |
|---|---|---|
| (一)基础知识 | CMake 是什么、安装、核心概念 | 跨平台构建系统生成器,out-of-source build |
| (二)基础命令 | target 体系、属性传播 | 现代 CMake 以目标为中心,PRIVATE/PUBLIC/INTERFACE |
| (三)变量与控制流 | 变量、条件、函数、configure_file | 缓存变量、作用域、生成配置文件 |
| (四)依赖管理 | find_package、FetchContent | 对外 find_package,对内 FetchContent |
| (五)进阶技巧 | 生成器表达式、Presets、安装打包 | 可复现构建、跨平台发布 |
CMake 的学习曲线确实存在,但掌握现代 CMake 的核心思想------以目标为中心、属性显式传播、声明式配置------就能写出清晰、可维护、可移植的构建脚本。
🎉 系列完结。如果对你有帮助,欢迎分享和收藏。如有疑问或建议,欢迎留言交流!