CMake 系列教程(五):进阶技巧

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 的核心思想------以目标为中心、属性显式传播、声明式配置------就能写出清晰、可维护、可移植的构建脚本。


🎉 系列完结。如果对你有帮助,欢迎分享和收藏。如有疑问或建议,欢迎留言交流!

相关推荐
踏着七彩祥云的小丑1 小时前
Go学习第5天:变量作用域 + 数组 + 指针
开发语言·学习·golang·go
Sam_Deep_Thinking1 小时前
java中的class到底是个什么东西?
java·开发语言·面试
影寂ldy1 小时前
C# 三大内置委托(Action / Func / Predicate)+ Lambda
c++·算法·c#
资深流水灯工程师1 小时前
PySide6 QMainWindow与QWidget秒解
开发语言·python
字节高级特工2 小时前
智能指针原理与使用场景全解析
开发语言·c++·算法
码界索隆2 小时前
Python转Java系列:面向对象基础
java·开发语言·python
逻辑星辰2 小时前
x-ds-pow-response逆向分析
开发语言·人工智能·python·深度学习·算法
AI科技星2 小时前
《全域数学/数术工坊》体系总览
c语言·开发语言·汇编·electron·概率论
范什么特西2 小时前
Maven中dependencies和dependencyManagement区别
java·开发语言·maven