CMake 工作流程 (CMake Workflow)

运行 CMake 的方式

  1. cmake-gui:Qt 图形界面
  2. ccmake:终端交互界面
  3. cmake:非交互式命令行

CMake 执行流程

  1. 读取缓存文件
    • 如果存在 CMakeCache.txt,CMake 会读取用户上一次的配置
  2. 用户编辑缓存(可选)
    • 用户可以修改一些变量
  3. CMake 配置 (configure)
    • 根据 CMakeLists.txt 文件和用户设置,检查依赖、生成配置
    • 如果配置不完整或需要修改,用户可以继续编辑
  4. CMake 生成 (generate)
    • 用户确认后,CMake 会生成 本机平台的构建系统文件(Makefile、Ninja 文件、Visual Studio 工程等)
      流程示意
CMake reads CMakeCache.txt  ---> User edits? ---> configure ---> generate
             |
             └--> CMake reads CMakeLists.txt

“Usage Requirements” / 现代 CMake (Modern CMake)

  • 现代风格:以 target 为中心
target_include_directories(example PUBLIC "inc")
  • 含义:
    • example 目标自身以及链接到它的目标都会使用 -Iinc
    • 每个 target 应该 完整描述如何使用它
    • 内部和外部 target 的使用方式一致

目标依赖与使用范围

现代 CMake 用三种关键关键词控制依赖和可见性:

关键词 含义 使用场景
PRIVATE 只有本 target 使用 内部实现依赖
INTERFACE 只有依赖本 target 的目标使用 提供给外部使用的接口
PUBLIC 本 target 和依赖它的目标都使用 兼具 PRIVATE + INTERFACE 功能

特殊生成器表达式

  • $<BUILD_INTERFACE>:仅在构建目录使用
  • $<INSTALL_INTERFACE>:仅在安装后的目标使用

示意结构

Root Directory
 ├── Executable
 │    ├── Library A
 │    └── Library B
 ├── Library A
 └── Library B
  • 每个库或可执行文件应 明确声明使用的 include、link 和依赖关系
  • 通过 PUBLIC / PRIVATE / INTERFACE 来控制哪些 target 可见
    总结理解
  • 现代 CMake 核心思想:每个 target 自描述自己的依赖和使用方式
  • target_include_directories 等命令替代了全局 include_directories
  • PRIVATE / INTERFACE / PUBLIC 控制 编译可见性和依赖传播
  • $<BUILD_INTERFACE> / $<INSTALL_INTERFACE> 控制 构建时 vs 安装时 的路径

Usage Requirements / target_link_libraries 示例 + Unity/Jumbo Builds 内容用整理理解:

Usage Requirements / target_link_libraries 示例

示例 1:PUBLIC 链接

target_link_libraries(trunk PUBLIC root)
target_link_libraries(leaf PUBLIC trunk)
  • 含义:
    • trunk 依赖 root,并且 trunk 的依赖也会传递给依赖 trunk 的目标
    • leaf 依赖 trunk,因为 trunk 是 PUBLIC,所以 leaf 自动也能访问 root
  • 生成的命令:
/usr/bin/c++ -fPIC -shared -Wl,-soname,libleaf.so \
            -o libleaf.so leaf.cxx.o libtrunk.so libroot.so
  • 注意:编译 leaf 时,会同时链接 trunk 和 root

示例 2:PRIVATE 链接

target_link_libraries(trunk PRIVATE root)
target_link_libraries(leaf PUBLIC trunk)
  • 含义:
    • trunk 依赖 root,但 这个依赖不会传递给依赖 trunk 的目标
    • leaf 依赖 trunk,但不会看到 root
  • 生成的命令:
/usr/bin/c++ -fPIC -shared -Wl,-soname,libleaf.so \
            -o libleaf.so leaf.cxx.o libtrunk.so
  • 注意:编译 leaf 时,只链接 trunk,不链接 root
    总结
  • PUBLIC:依赖会传递
  • PRIVATE:依赖只在自身 target 可见,不传递

Unity / Jumbo Builds

  • 概念
    • 将多个源文件合并成一个编译单元,以 减少编译开销
    • 控制合并大小可以用:
CMAKE_UNITY_BUILD_BATCH_SIZE
  • 注意:
    • 对于 C++ 模块 (C++20 Modules),这种做法可能效果有限
    • 优点:减少编译时间
    • 缺点:可能增加单个编译单元的内存占用或调试难度
      理解要点
  1. target_link_libraries 的 PUBLIC/PRIVATE 决定 依赖传播
  2. Unity / Jumbo Builds 是 编译优化手段,通过合并源文件减少总编译次数
  3. 对现代 CMake 项目,合理使用 PUBLIC/PRIVATE/INTERFACE + Unity Build 可以 优化依赖管理和编译速度

一个完整的 CMake + C++ 代码例子,演示 PUBLIC vs PRIVATE target_link_libraries 的区别。

目录结构

CMakeExample/
├── CMakeLists.txt
├── root/
│   ├── CMakeLists.txt
│   └── root.cpp
├── trunk/
│   ├── CMakeLists.txt
│   └── trunk.cpp
└── leaf/
    ├── CMakeLists.txt
    └── leaf.cpp

root/root.cpp

#include <iostream>
void root_function() {
    std::cout << "Hello from root!" << std::endl;
}

trunk/trunk.cpp

#include <iostream>
#ifdef USE_ROOT
extern void root_function();
#endif
void trunk_function() {
    std::cout << "Hello from trunk!" << std::endl;
#ifdef USE_ROOT
    root_function();
#endif
}

leaf/leaf.cpp

#include <iostream>
extern void trunk_function();
int main() {
    trunk_function();
    std::cout << "Hello from leaf!" << std::endl;
    return 0;
}

root/CMakeLists.txt

add_library(root STATIC root.cpp)

trunk/CMakeLists.txt

PUBLIC 依赖 root
add_library(trunk STATIC trunk.cpp)
target_link_libraries(trunk PUBLIC root)
target_compile_definitions(trunk PUBLIC USE_ROOT)
PRIVATE 依赖 root
# add_library(trunk STATIC trunk.cpp)
# target_link_libraries(trunk PRIVATE root)
# target_compile_definitions(trunk PRIVATE USE_ROOT)

注:切换 PUBLIC / PRIVATE 看效果差异

leaf/CMakeLists.txt

add_executable(leaf leaf.cpp)
target_link_libraries(leaf PUBLIC trunk)

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(CMakePublicPrivateExample)
add_subdirectory(root)
add_subdirectory(trunk)
add_subdirectory(leaf)

效果对比

1⃣ 使用 PUBLIC
cmake -B build
cmake --build build
./build/leaf/leaf

输出:

Hello from trunk!
Hello from root!
Hello from leaf!
  • leaf 能访问 root,因为 trunk 对 root 是 PUBLIC
2⃣ 使用 PRIVATE
# 修改 trunk/CMakeLists.txt 为 PRIVATE
cmake -B build -U *
cmake --build build
./build/leaf/leaf
  • 编译 leaf 时 会报错,因为 leaf 看不到 root
  • leaf 只能访问 trunk 自己的内容
    总结
  • PUBLIC:依赖传递给使用它的目标
  • PRIVATE:依赖只对自身可见,不传递
    target_link_libraries(leaf PUBLIC trunk)
[proc] Executing command: /usr/bin/cmake --build /home/xiaqiu/test/build --config Debug --target all --
[build] [3/6  16% :: 0.381] Building CXX object CppCon/day416/code/root/CMakeFiles/root.dir/root.cpp.o
[build] [4/6  33% :: 0.381] Building CXX object CppCon/day416/code/leaf/CMakeFiles/leaf.dir/leaf.cpp.o
[build] [4/6  50% :: 0.383] Building CXX object CppCon/day416/code/trunk/CMakeFiles/trunk.dir/trunk.cpp.o
[build] [4/6  66% :: 0.712] Linking CXX static library CppCon/day416/code/root/libroot.a
[build] [5/6  83% :: 0.759] Linking CXX static library CppCon/day416/code/trunk/libtrunk.a
[build] [6/6 100% :: 0.989] Linking CXX executable CppCon/day416/code/leaf/leaf
[driver] Build completed: 00:00:01.007

没发现什么区别

CMake Presets 理解

概念

  • Presets(预设) 是一种 保存 CMake 常用配置的机制
  • 可以把项目中经常使用的配置(变量、构建目录、生成器等)保存到 JSON 文件中,方便重复使用和共享

文件类型

  1. CMakePresets.json
    • 版本控制(可以放到 Git 等仓库)
    • 用于 团队共享
    • 保存项目通用的配置,例如:
      • 常用构建类型(Debug / Release)
      • 编译器选择
      • 构建目录
  2. CMakeUserPresets.json
    • 不版本控制(仅在本地使用)
    • 用于 机器或用户特定的设置
    • 示例用途:
      • 本地特殊路径
      • 私有编译器配置
      • 用户偏好选项

优势

  • 统一配置:团队成员使用相同 preset,避免每人手动配置
  • 易于切换:快速在不同构建配置间切换(Debug / Release / Custom)
  • 可维护性:减少手动重复输入命令或修改缓存
    理解口诀
  • CMakePresets.json = 共享 / 版本控制
  • CMakeUserPresets.json = 本地 / 用户专属

CMake Presets JSON 示例

{
  "version": 3,
  "configurePresets": [
    {
      "name": "ninja-debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    }
  ]
}

字段解释

字段 含义
version Preset 文件的版本号,这里是 3
configurePresets 一个数组,存放所有配置预设(configure presets)
name 预设名称,用于引用,比如 "ninja-debug"
generator 指定构建工具,这里用 "Ninja"
binaryDir 构建目录,这里 ${sourceDir}/build/debug 表示在项目源目录下创建 build/debug
cacheVariables 配置 CMake 缓存变量,比如:CMAKE_BUILD_TYPE 设置为 "Debug"

理解

  • 这个 preset 定义了一个 Debug 构建,使用 Ninja 构建工具
  • 构建输出会放在 ${sourceDir}/build/debug 目录
  • CMake 执行配置时,会自动把 CMAKE_BUILD_TYPE 设置为 Debug
  • 优点:
    • 无需每次在命令行手动输入 -G Ninja -B build/debug -DCMAKE_BUILD_TYPE=Debug
    • 团队共享这个 preset,每个人都能统一构建环境
      总结口诀
  • name → 预设名字
  • generator → 使用哪个构建工具
  • binaryDir → 构建输出目录
  • cacheVariables → 配置缓存变量(编译类型、路径等)

一个完整的 CMake Presets 示例,包含 Debug 和 Release 两个 preset,并展示如何快速使用它们构建项目。

CMakePresets.json 示例

{
  "version": 3,
  "configurePresets": [
    {
      "name": "ninja-debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "MY_CUSTOM_OPTION": "ON"
      }
    },
    {
      "name": "ninja-release",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "MY_CUSTOM_OPTION": "OFF"
      }
    }
  ]
}

说明

字段 解释
version Preset 文件版本
configurePresets 配置预设数组
name 预设名称,例如 "ninja-debug""ninja-release"
generator 指定构建工具,这里都使用 "Ninja"
binaryDir 构建输出目录,Debug 和 Release 分开
cacheVariables 配置缓存变量,示例中除了 CMAKE_BUILD_TYPE 还有自定义选项 MY_CUSTOM_OPTION

使用示例

  1. 选择 Debug 构建
cmake --preset=ninja-debug
cmake --build build/debug
  1. 选择 Release 构建
cmake --preset=ninja-release
cmake --build build/release

通过 preset,无需每次手动输入生成器、构建目录和缓存变量,直接快速切换配置。
总结

  • Debug / Release 可以用不同的 preset 分开管理
  • preset 可以保存生成器、构建目录、缓存变量等
  • 方便团队共享或者个人快速切换构建环境

预编译头文件 (Precompiled Headers, PCH)

概念

  • 预编译头文件:将一些常用的头文件提前编译成部分处理过的文件
  • 目的
    • 避免每次编译都重复解析大量公共头文件
    • 加快编译速度

使用方式 (CMake 示例)

add_library(leaf SHARED leaf.cxx)
target_precompile_headers(leaf
  PRIVATE 
    <iostream>
    <vector>
    <unordered_map>
  INTERFACE
    "leaf.h"
)

字段解释

关键词 含义
target_precompile_headers 给指定 target 设置预编译头文件
PRIVATE 只有本 target 使用这些头文件进行预编译
INTERFACE 依赖该 target 的其他目标也会使用这些头文件进行预编译
<iostream>, <vector>, <unordered_map> C++ 标准库头文件,PRIVATE,只在 leaf 自己编译时使用
"leaf.h" 自定义头文件,INTERFACE,依赖 leaf 的其他 target 也会使用

理解要点

  1. 加速编译
    • 常用头文件只编译一次
    • 后续编译同样 target 或依赖它的 target 时直接使用预编译结果
  2. PRIVATE / INTERFACE 区别
    • PRIVATE → 仅 leaf 使用
    • INTERFACE → 依赖 leaf 的 target 也能使用
  3. 应用场景
    • 大型项目中公共头文件(如 STL、第三方库)
    • 头文件解析复杂、耗时的项目
      总结口诀
  • PCH = 头文件提前编译 → 减少重复解析 → 提升速度
  • PRIVATE = 自己用,INTERFACE = 传递给依赖者

写一个 CMake + C++ 小例子,演示 预编译头文件 (PCH) 的使用。

目录结构

PCHExample/
├── CMakeLists.txt
├── leaf/
│   ├── CMakeLists.txt
│   ├── leaf.h
│   └── leaf.cpp
└── main.cpp

leaf/leaf.h

#pragma once
#include <iostream>
#include <vector>
#include <unordered_map>
inline void leaf_function() {
    std::cout << "Hello from leaf!" << std::endl;
}

leaf/leaf.cpp

#include "leaf.h"
// 可以添加更多实现

main.cpp

#include "leaf/leaf.h"
int main() {
    leaf_function();
    std::cout << "Hello from main!" << std::endl;
    return 0;
}

leaf/CMakeLists.txt

add_library(leaf STATIC leaf.cpp)
# 设置预编译头文件
target_precompile_headers(leaf
  PRIVATE
    <iostream>
    <vector>
    <unordered_map>
  INTERFACE
    "leaf.h"
)

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.24)
project(PCHExample)
add_subdirectory(leaf)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE leaf)

效果说明

  1. leaf.cpp 编译时,<iostream>, <vector>, <unordered_map> 会被 预编译
  2. main.cpp 编译时,由于 INTERFACE 设置,leaf.h 也会使用预编译头文件,加快编译速度
  3. 输出:
Hello from leaf!
Hello from main!

总结

  • PRIVATE 的头文件只用于 leaf 自己
  • INTERFACE 的头文件会被依赖 leaf 的 target(main)使用
  • 对大型项目,PCH 能显著减少头文件重复解析时间

目录内容

xiaqiu@xz:~/test/build/CppCon/day416/code/leaf/CMakeFiles/leaf.dir$ tree
.
├── cmake_pch.hxx
├── cmake_pch.hxx.cxx
├── cmake_pch.hxx.gch
└── leaf.cpp.o
1 directory, 4 files
xiaqiu@xz:~/test/build/CppCon/day416/code/leaf/CMakeFiles/leaf.dir$ 

文件解释

  1. cmake_pch.hxx
    • 这是 CMake 生成的 统一预编译头文件(PCH)源文件)
    • 它会包含你在 target_precompile_headers 中指定的头文件
    • 类似把 <iostream>, <vector>, <unordered_map>"leaf.h" 放进一个单独文件,供编译器预编译
  2. cmake_pch.hxx.cxx
    • 实际的 编译单元源文件
    • 编译器会把这个文件编译成 预编译头文件
    • 相当于“中间文件”,将公共头文件解析一次,生成可重用的编译结果
  3. cmake_pch.hxx.gch
    • .gch 文件是 GCC/Clang 生成的预编译头文件(GCC Precompiled Header)
    • 编译器在后续编译 leaf.cpp 时,会直接使用这个 .gch 文件
    • 避免每次都重新解析头文件,提高编译速度
  4. leaf.cpp.o
    • leaf.cpp 编译后的目标文件
    • 编译器在编译时使用了上面生成的 PCH 文件(cmake_pch.hxx.gch)

总结理解

  • 作用链
target_precompile_headers 指定的头文件
        ↓
cmake_pch.hxx.cxx → 编译生成 cmake_pch.hxx.gch (预编译头文件)
        ↓
leaf.cpp 编译 → 使用 cmake_pch.hxx.gch 加速编译 → 生成 leaf.cpp.o
  • 好处
    • 避免重复解析 <iostream>, <vector> 等头文件
    • 对大型项目或依赖复杂头文件的 target,能显著提升编译速度

Multi-Config Ninja(多配置 Ninja 构建)

概念

  • Ninja 是一种快速的命令行构建工具
  • Multi-Config 版本支持同时在一个构建目录里生成多种配置(Debug / Release 等),类似 Visual Studio 的多配置项目

使用示例

# 使用 ccache 加速编译
export CMAKE_CXX_COMPILER_LAUNCHER=ccache
# 配置构建目录,指定 Multi-Config Ninja
cmake -G "Ninja Multi-Config" -S ./src -B ./build
# 构建 Debug 配置
cmake --build ./build --config Debug -j10 -v
# 编译器实际调用
/usr/bin/ccache gcc ...
# 测试
ctest --build-config Debug -j10

要点

  • CMAKE__COMPILER_LAUNCHER 可以指定编译器启动器,如 ccache,用来加速编译
  • Multi-Config 构建目录可以同时支持 Debug 和 Release,无需重复生成

全平台安装系统(Install System)

概念

  • CMake 提供统一的安装机制,可以跨平台安装目标文件、普通文件或整个目录
  • 默认提供一些安装路径,但可以自定义规则

示例

add_library(leaf SHARED leaf.cxx)
install(TARGETS root trunk leaf parasite)
  • 说明:
    • 安装 target(库或可执行文件)
    • 可以指定安装目录(CMAKE_INSTALL_PREFIX)
    • 支持文件、目录安装

打包(Packaging)

概念

  • CPack 是 CMake 自带的打包工具
  • 可以创建 平台特定的安装程序(Installer),如:
    • Windows .msi.exe
    • Linux .deb / .rpm
    • macOS .dmg

特点

  • 配置简单,命令式或脚本式
  • 可以生成专业的安装包,方便分发

总结

  1. Multi-Config Ninja
    • 支持 Debug / Release 共存
    • 配合 ccache 可加速编译
    • 通过 --config 指定构建类型
  2. Install System
    • 安装库、可执行文件、文件夹
    • 支持跨平台
    • 默认路径 + 可自定义规则
  3. CPack
    • CMake 自带打包工具
    • 创建平台相关安装程序
    • 简化发布流程
      口诀理解
  • Ninja Multi-Config = 同一个目录多配置 + 快速编译
  • install() = 安装目标 / 文件 / 目录
  • CPack = 打包成平台安装器

CPack 功能特性(CPack Features)

  • 支持项目类型
    • CMake 项目非 CMake 项目 都可以使用
  • 跨平台支持的安装包类型
    | 平台 | 支持的包类型 |
    | ------------ | --------------------------------- |
    | Unix / Linux | .tar.gz (TGZ),自解压 TGZ (STGZ) |
    | Windows | WiX → MSI 安装包、NSIS / NSIS64 安装程序 |
    | MacOS | Drag & Drop 安装、PackageMaker |
    | Linux | .deb(Debian 包)、.rpm(RPM 包管理器) |
  • 总结:CPack 能根据不同平台生成对应的标准安装包,无需手动打包脚本

使用 CPack(Using CPack)

  1. 安装必要工具(Windows 示例):
    • ZIP 命令行工具
    • NSIS / NSIS64
    • WiX 工具
  2. 设置项目支持 CPack
    • 先确保 make install 工作正常
    • 在 CMakeLists.txt 中使用 install(...) 安装 target / 文件 / 目录
  3. 注意事项
    • 确保可执行文件能使用 相对路径,并可从任意目录运行
    • 根据需要设置 CPack 选项变量,例如包名、版本号、安装目录等
  4. 在 CMakeLists.txt 中包含 CPack
include(CPack)
  • 这一步会启用 CPack 打包功能,并生成平台特定安装包命令

理解总结

  1. 功能强大:跨平台、支持多种安装包类型
  2. 使用方便:依赖 CMake 的 install() 系统即可
  3. 配置灵活:可设置安装路径、包名、版本号等
  4. 流程
CMake 配置 -> make install -> include(CPack) -> 生成安装包

口诀理解

  • Unix = TGZ / STGZ
  • Windows = MSI / NSIS
  • Mac = Drag&Drop / PackageMaker
  • Linux = DEB / RPM

CMake + CPack 小项目示例,展示如何配置 install 并生成跨平台安装包。

目录结构

CPackExample/
├── CMakeLists.txt
├── main.cpp
└── include/
    └── hello.h

include/hello.h

#pragma once
#include <iostream>
inline void say_hello() {
    std::cout << "Hello from CPackExample!" << std::endl;
}

main.cpp

#include "hello.h"
int main() {
    say_hello();
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.24)
project(CPackExample VERSION 1.0 LANGUAGES CXX)
# 添加可执行文件
add_executable(CPackExample main.cpp)
target_include_directories(CPackExample PUBLIC include)
# 安装目标
install(TARGETS CPackExample
        RUNTIME DESTINATION bin)   # Windows / Linux 可执行文件安装路径
install(DIRECTORY include/ DESTINATION include)  # 安装头文件
# 配置 CPack
set(CPACK_PACKAGE_NAME "CPackExample")
set(CPACK_PACKAGE_VENDOR "xiaqiu")
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH 0)
set(CPACK_GENERATOR "TGZ;ZIP")  # 生成 .tar.gz 和 .zip 安装包
include(CPack)

使用流程

  1. 生成构建目录并编译
cmake -B build -S .
cmake --build build -j4
  1. 安装到临时目录(模拟安装)
cmake --install build --prefix ./install
  • 可执行文件在 install/bin/CPackExample
  • 头文件在 install/include/hello.h
  1. 生成安装包
cmake --build build --target package
  • 生成 CPackExample-1.0-Linux.tar.gzCPackExample-1.0-Linux.zip(Linux 示例)

效果说明

  • 你可以直接把生成的 .tar.gz.zip 分发给别人
  • 包含可执行文件和头文件
  • Windows 下可用 NSIS / MSI,Mac 下可用 Drag&Drop,只需修改 CPACK_GENERATOR
    总结
  • install() → 安装文件/目标
  • CPack → 打包安装目录成标准安装包
  • 配置 CPACK_PACKAGE_* → 设置包名、版本、作者等信息
  • 生成器 (CPACK_GENERATOR) 决定输出格式
xiaqiu@xz:~/test/CppCon/day416/code$ cmake -B build -S .
-- The CXX compiler identification is GNU 13.3.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.8s)
-- Generating done (0.0s)
-- Build files have been written to: /home/xiaqiu/test/CppCon/day416/code/build
xiaqiu@xz:~/test/CppCon/day416/code$ cmake --build build -j4
[ 50%] Building CXX object CMakeFiles/CPackExample.dir/main.cpp.o
[100%] Linking CXX executable CPackExample
[100%] Built target CPackExample
xiaqiu@xz:~/test/CppCon/day416/code$ ls
CMakeLists.txt  build  include  main.cpp
xiaqiu@xz:~/test/CppCon/day416/code$ cmake --install build --prefix ./install
-- Install configuration: ""
-- Installing: /home/xiaqiu/test/CppCon/day416/code/./install/bin/CPackExample
-- Installing: /home/xiaqiu/test/CppCon/day416/code/./install/include
-- Installing: /home/xiaqiu/test/CppCon/day416/code/./install/include/hello.h
xiaqiu@xz:~/test/CppCon/day416/code$ ls
CMakeLists.txt  build  include  install  main.cpp
xiaqiu@xz:~/test/CppCon/day416/code$ cmake --build build --target package
[100%] Built target CPackExample
Run CPack packaging tool...
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: CPackExample
CPack: - Install project: CPackExample []
CPack: Create package
CPack: - package: /home/xiaqiu/test/CppCon/day416/code/build/CPackExample-1.0.0-Linux.tar.gz generated.
CPack: Create package using ZIP
CPack: Install projects
CPack: - Run preinstall target for: CPackExample
CPack: - Install project: CPackExample []
CPack: Create package
CPack: - package: /home/xiaqiu/test/CppCon/day416/code/build/CPackExample-1.0.0-Linux.zip generated.
xiaqiu@xz:~/test/CppCon/day416/code$ ls
CMakeLists.txt  build  include  install  main.cpp
xiaqiu@xz:~/test/CppCon/day416/code$ cd build/
xiaqiu@xz:~/test/CppCon/day416/code/build$ ls
CMakeCache.txt  CPackConfig.cmake  CPackExample-1.0.0-Linux.tar.gz  CPackSourceConfig.cmake  _CPack_Packages      install_manifest.txt
CMakeFiles      CPackExample       CPackExample-1.0.0-Linux.zip     Makefile                 cmake_install.cmake
xiaqiu@xz:~/test/CppCon/day416/code/build$ 

CMake 测试 (Testing with CMake)

启用测试

  • 必须先启用测试功能,通过以下任意一种方式:
include(CTest)
# 或者
enable_testing()
  • 作用:告诉 CMake 项目里会有测试用例,生成测试相关的构建规则

添加测试

add_test(NAME testname
         COMMAND exename arg1 arg2 ...)
  • NAME:测试名称(testname)
  • COMMAND:执行的可执行文件及参数
  • 返回值
    • 可执行文件返回 0 → 测试通过
    • 返回非 0 → 测试失败

CTest 工具

  • ctest 是随 CMake 分发的命令行工具
  • 用于运行项目中的所有测试
  • 用法
# 在构建目录顶层运行所有测试
ctest

示例输出

$ ctest
Test project /tmp/example/bin
  Start 1: case1
1/1 Test #1: case1 .............................   Passed    0.00 sec
  Start 2: case2
2/2 Test #2: case2 .............................   Passed    0.00 sec
100% tests passed, 0 tests failed out of 2
Total Test time (real) =   0.01 sec
  • 每个测试都会显示:
    • 测试编号
    • 测试名称
    • 测试状态(Passed / Failed)
    • 执行时间
  • 总结信息:
    • 测试总数
    • 通过数量
    • 失败数量
    • 总耗时

理解总结

  1. 启用测试include(CTest)enable_testing()
  2. 添加测试add_test(NAME … COMMAND …)
  3. 运行测试ctest
  4. 返回值判断
    • 0 → 通过
    • 0 → 失败
      口诀理解
  • include/enable → add_test → ctest → 返回 0 通过

一个 完整的 CMake + CTest 小项目示例,演示如何配置测试、编译并运行测试。

目录结构

CTestExample/
├── CMakeLists.txt
├── main.cpp
└── tests/
    ├── CMakeLists.txt
    ├── test_add.cpp
    └── test_subtract.cpp

main.cpp

#pragma once
int add(int a, int b) {
    return a + b;
}
int subtract(int a, int b) {
    return a - b;
}

tests/test_add.cpp

#include <cassert>
#include "../main.cpp"
int main() {
    assert(add(2, 3) == 5);      // 通过
    assert(add(-1, 1) == 0);     // 通过
    return 0;                     // 返回 0 表示测试通过
}

tests/test_subtract.cpp

#include <cassert>
#include "../main.cpp"
int main() {
    assert(subtract(5, 3) == 2);   // 通过
    assert(subtract(2, 2) == 0);   // 通过
    return 0;                       // 返回 0 表示测试通过
}

tests/CMakeLists.txt

# 添加测试可执行文件
add_executable(test_add test_add.cpp)
add_executable(test_subtract test_subtract.cpp)
# 添加 CTest 测试
add_test(NAME add_test COMMAND test_add)
add_test(NAME subtract_test COMMAND test_subtract)

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.24)
project(CTestExample)
# 启用 CTest 测试功能
enable_testing()
# 或者 include(CTest)
# 添加 tests 子目录
add_subdirectory(tests)

使用流程

  1. 生成构建目录并编译
cmake -B build -S .
cmake --build build -j4
  1. 运行测试
ctest -C Debug -V

示例输出

Test project /path/to/CTestExample/build
  Start 1: add_test
1/2 Test #1: add_test .......................   Passed    0.00 sec
  Start 2: subtract_test
2/2 Test #2: subtract_test ..................   Passed    0.00 sec
100% tests passed, 0 tests failed out of 2
Total Test time (real) = 0.01 sec

要点总结

  1. 启用测试enable_testing()include(CTest)
  2. 添加测试可执行文件add_executable(test_name …)
  3. 注册测试add_test(NAME … COMMAND …)
  4. 运行测试ctest
  5. 通过/失败判断 → 可执行文件返回 0 表示通过
    扩展
  • 可以用 ctest -V 查看详细输出
  • 可以用 ctest -R <regex> 只运行匹配的测试
  • 未来可以加入 CTest 配合 CPack,打包时自动运行测试

GoogleTest 集成(GoogleTest integration)

步骤概览

  1. 引入 GoogleTest 模块
include(GoogleTest)
  • 这个模块提供了 CMake 与 GoogleTest 的集成功能
  1. 添加测试可执行文件
add_executable(tests tests.cpp)
target_link_libraries(tests GTest::GTest)
  • add_executable:生成测试可执行文件
  • target_link_libraries:链接 GoogleTest 库
  1. 使用 gtest_discover_tests 自动发现测试
gtest_discover_tests(tests)
  • 作用
    • CMake 会调用测试可执行文件,让它列出所有 GoogleTest 的 test case
    • 自动将这些测试注册到 CTest
    • 无需手动写 add_test()
    • 可以发现新测试而 不需要重新运行 CMake

工作原理

tests (GoogleTest 可执行文件)
       |
       v
gtest_discover_tests
       |
       v
自动注册所有测试到 CTest
  • 后续使用 ctest 就可以直接运行这些测试
  • 支持详细输出、单独运行某个测试、并行运行等 CTest 功能

理解总结

  1. include(GoogleTest) → 引入 GoogleTest 集成模块
  2. add_executable + target_link_libraries → 编译 GoogleTest 测试程序
  3. gtest_discover_tests → 自动发现 GoogleTest 测试,注册到 CTest,无需手动 add_test
  4. 优势
    • 自动管理测试列表
    • 新增测试无需重新配置 CMake
    • 与 CTest 无缝集成
      口诀理解
  • include → add_executable → link GTest → gtest_discover_tests → ctest 运行所有测试

一个 完整的 CMake + GoogleTest 小项目示例,演示如何自动发现测试并用 ctest 运行。

目录结构

GTestExample/
├── CMakeLists.txt
├── main.cpp
└── tests/
    ├── CMakeLists.txt
    └── test_math.cpp

main.cpp

#pragma once
int add(int a, int b) {
    return a + b;
}
int subtract(int a, int b) {
    return a - b;
}

tests/test_math.cpp

#include <gtest/gtest.h>
#include "../main.cpp"
TEST(AddTest, PositiveNumbers) { EXPECT_EQ(add(2, 3), 5); }
TEST(AddTest, NegativeNumbers) { EXPECT_EQ(add(-1, -1), -2); }
TEST(SubtractTest, PositiveNumbers) { EXPECT_EQ(subtract(5, 3), 2); }
TEST(SubtractTest, ZeroResult) { EXPECT_EQ(subtract(2, 2), 0); }
int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

tests/CMakeLists.txt

# 添加测试可执行文件
add_executable(tests test_math.cpp)
# 链接 GoogleTest 库
target_link_libraries(tests GTest::GTest)
# 启用 GoogleTest 自动发现
include(GoogleTest)
gtest_discover_tests(tests)

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.24)
project(GTestExample LANGUAGES CXX)
# 启用测试功能
enable_testing()
# 查找 GoogleTest
find_package(GTest REQUIRED)
# 添加 tests 子目录
add_subdirectory(tests)

使用流程

  1. 生成构建目录并编译
cmake -B build -S .
cmake --build build -j4
  1. 运行测试
cd build
ctest -V

示例输出

Test project /path/to/GTestExample/build
UpdateCTestConfiguration  from :/path/to/GTestExample/build/DartConfiguration.tcl
Constructing a list of tests
Checking test dependency graph...
Checking test dependency graph end
1/4 Testing: tests/AddTest.PositiveNumbers
1/4 Test: tests/AddTest.PositiveNumbers ...   Passed    0.00 sec
2/4 Testing: tests/AddTest.NegativeNumbers
2/4 Test: tests/AddTest.NegativeNumbers ...   Passed    0.00 sec
3/4 Testing: tests/SubtractTest.PositiveNumbers
3/4 Test: tests/SubtractTest.PositiveNumbers ...   Passed    0.00 sec
4/4 Testing: tests/SubtractTest.ZeroResult
4/4 Test: tests/SubtractTest.ZeroResult ...   Passed    0.00 sec
100% tests passed, 0 tests failed out of 4
Total Test time (real) = 0.01 sec

要点总结

  1. 查找 GTestfind_package(GTest REQUIRED)
  2. 添加测试可执行文件add_executable
  3. 链接 GTesttarget_link_libraries(tests GTest::GTest)
  4. 自动发现测试gtest_discover_tests(tests)
  5. 运行测试ctest -V
    好处
  • 新增 TEST 宏定义的测试无需修改 CMake
  • 支持 CTest 命令行参数,例如只运行部分测试、并行运行等
    好的,我来帮你把 CTest 多核测试(multi-core tests) 的内容用整理理解:

CTest 多核测试

概念

  • PROCESSOR_AFFINITY
    • 在支持的平台上,可以将测试进程绑定到指定的 CPU 核心
    • 这样可以:
      • 减少 CPU 上其他进程干扰
      • 控制测试进程在固定核心运行
      • 更稳定地测量性能
  • 相关属性
    • PROCESSOR_AFFINITY ON:启用 CPU 核心绑定
    • PROCESSORS <核心列表或数量>:指定使用的核心

CMake 示例

# 添加测试
add_test(NAME myTest COMMAND myTestExecutable)
# 设置多核属性
set_tests_properties(myTest PROPERTIES
                     PROCESSOR_AFFINITY ON
                     PROCESSORS 4)
  • 上述配置意思:
    • 启用多核绑定
    • myTest 测试绑定到 第 4 个核心(或者指定核心 0~3 共 4 核,具体平台实现可能不同)

使用场景

  1. 性能测试
    • 测试程序对 CPU 核心敏感时,固定核心可以减少波动
  2. 并行测试
    • 多个测试同时运行时,可以指定不同核心,提高并行度

注意事项

  • 并非所有平台都支持 CPU 核心绑定
  • Linux 上一般需要 sched_setaffinity 支持
  • Windows 上依赖进程亲和性(Processor Affinity)
    理解总结
PROCESSOR_AFFINITY ON → 启用多核绑定
PROCESSORS N         → 指定使用第 N 核或 N 个核
作用:稳定测试性能、控制 CPU 分配

CDash https://my.cdash.org/
CDash 是一个开源的基于 Web 的软件测试服务器,用于聚合、分析和展示全球客户端提交的测试结果。它是 CMake、CTest 和 CPack 工具链的一部分,广泛应用于持续集成(CI)和质量监控流程中 ([cdash.org][1])。

CDash 的作用

  • 集中展示测试结果:CDash 汇总来自多个平台和构建配置的测试结果,生成易于浏览的 Web 仪表盘。
  • 支持多种构建系统:虽然与 CMake 紧密集成,但也支持非 CMake 项目的测试结果提交。
  • 自动化质量监控:与 CTest 配合使用,可实现夜间构建、覆盖率分析、内存检查等功能。
  • 跨平台支持:适用于 Linux、Windows、macOS 等多种操作系统。

如何使用 CDash

1. 创建 CDash 项目

访问 https://my.cdash.org,注册账户并创建一个新项目。在项目设置页面中,您将获得一个 CTestConfig.cmake 文件,其中包含项目名称、提交 URL 和夜间构建开始时间等信息。

2. 配置 CMake 项目

在项目的顶层 CMakeLists.txt 中,添加以下内容:

include(CTest)

这将启用 CTest 支持,并允许将测试结果提交到 CDash。

3. 配置 CTestConfig.cmake

将从 CDash 项目页面下载的 CTestConfig.cmake 文件放置在项目的顶层目录。该文件包含以下内容:

set(CTEST_PROJECT_NAME "YourProjectName")
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")
set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=YourProjectName")
set(CTEST_DROP_SITE_CDASH TRUE)

请勿修改该文件,以确保 CTest 正确提交结果。

4. 提交测试结果

在构建目录中,运行以下命令以提交测试结果:

ctest -D Experimental

对于多配置生成器(如 Visual Studio),请指定配置类型:

ctest -C Debug -D Experimental

这将构建项目、运行测试,并将结果提交到 CDash。

CDash 仪表盘内容

CDash 仪表盘通常包括以下部分:

  • Nightly:每日构建和测试的结果。
  • Experimental:开发人员提交的实验性构建结果。
  • Continuous:持续集成的构建和测试结果。
  • Coverage:代码覆盖率分析结果。
  • Dynamic Analysis:动态分析(如内存泄漏检查)结果。
    每个部分都提供详细的构建和测试信息,帮助开发人员快速定位问题。

CDash 的优势

  • 集中管理:所有测试结果集中在一个地方,便于查看和分析。
  • 跨平台支持:支持多种操作系统和构建配置。
  • 自动化:与 CTest 配合使用,实现自动化构建、测试和结果提交。
  • 可扩展性:支持自定义构建和测试流程,满足不同项目的需求。

相关资源

  • CDash 官方网站
  • CMake 官方文档:CDash
  • CDash GitHub 仓库
    如果您希望我为您提供一个完整的示例,展示如何配置 CMake 项目以使用 CDash,请告诉我,我将很乐意为您提供帮助。
    [1]: https://www.cdash.org/?utm_source=chatgpt.com “Accelerate your Development Process with CDash”

C++20 模块示例及问题

模块示例

// B.cpp
export module B;
export void b() { }
// A.cpp
export module A;
import B;
export void a() { b(); }
  • 模块 B 导出函数 b
  • 模块 A 导入模块 B 并调用 b()

常见错误

cl -std:c++20 -interface -c A.cpp
A.cpp(2): error C2230: could not find module 'B'
A.cpp(3): error C3861: 'b': identifier not found

原因:

  1. 编译顺序很重要:模块 B 必须先生成模块接口文件(BMI)
  2. 缺少模块接口文件:A.cpp 编译时找不到 B 的 .ifc 文件

解决方法

  1. 先编译 B.cpp 接口
cl -std:c++20 -interface -c B.cpp
  1. 再编译 A.cpp
cl -std:c++20 -interface -c A.cpp
  • 编译后生成的文件:
A.cpp  A.ifc  A.obj  B.cpp  B.ifc  B.obj
  • .ifc → MSVC 模块接口文件
  • GCC 对应 .gcm 文件

模块的构建挑战

  • C++ 模块引入新的编译模型
    • 传统头文件依赖只需解析一次,模块依赖可能需要在首次构建时动态发现
  • 需要工具链和标准支持
    • 模块依赖不能完全靠构建系统推断
    • 编译器需提供模块接口和依赖信息

CMake 对模块的经验

  • Fortran 模块的经验
    • 2005 年:CMake 添加 Fortran 模块依赖扫描
    • 2015 年:Ninja 支持 Fortran 模块依赖
    • 2019 年:Ninja 合并对模块的支持,为 C++ 模块打基础
  • 动态依赖解析
    • 模块依赖信息可能需要在首次构建时从源文件内容解析
    • 传统头文件只在第二次及以后才需要
  • CMake 的做法
    • Fortran 模块:基于 makedepf90 的解析器
    • Ninja 构建工具上游补丁支持动态依赖
    • C++ 模块也需要类似解析和动态依赖信息

文档与标准

  • P1838R0:模块用户词汇和文件扩展标准
    • MSVC → .ifc
    • GCC → .gcm
    • 规范了模块接口文件、导入导出语法等
  • 参考资料

理解总结

  1. 模块编译顺序很重要
    • 先编译依赖模块接口
    • 再编译使用该模块的文件
  2. BMI(Built Module Interface)
    • MSVC → .ifc
    • GCC → .gcm
    • 模块文件必须生成并可被依赖模块找到
  3. CMake 经验
    • 先从 Fortran 模块开始支持
    • Ninja 构建工具支持动态依赖
    • C++ 模块需要类似机制,解析源文件确定依赖
  4. 工具链与标准协作
    • 编译器提供模块接口
    • CMake/Ninja 处理依赖和构建顺序
      核心理解
  • 模块不是普通头文件,必须按依赖顺序编译
  • 首次构建需要解析源文件动态发现依赖
  • CMake 已有 Fortran 模块经验,可迁移到 C++20 模块
    好的,我来帮你把 Fortran 模块的构建工作原理 用整理理解。

Fortran 模块示例

module math
contains
    function add(a,b)
        real :: add
        real, intent(in) :: a, b
        add = a + b
    end function
end module
program main
    use math
    print *, 'sum is', add(1.,2.)
end program
  • 模块 math 导出函数 add
  • 编译 main 时,需要 math.mod 文件
  • 编译输出:
    • math.o → 模块的目标文件
    • math.mod → 模块接口文件(编译器生成)

单目标构建图(单库/单模块)

假设有三个源文件 a1.F90, a2.F90, a3.F90

  1. a1.F90 编译生成模块接口:
    a1.F90 → preprocess → a1.pp.f90 → compile → math.mod + a1.o
    
  2. a2.F90 依赖 math.mod:
    a2.F90 → preprocess → a2.pp.f90 → compile → a2.o
    
    • 必须等待 a1.F90 生成 math.mod
  3. a3.F90 独立于 math.mod,可并行编译:
    a3.F90 → preprocess → a3.pp.f90 → compile → a3.o
    

特点

  • Ninja 构建系统会根据依赖生成 构建图
  • 并行编译:
    • a1.F90 和 a3.F90 可同时进行
    • a2.F90 必须等待 math.mod
  • 增量编译优化
    • 如果 a1.F90 修改后 math.mod 未变化,a2.F90 不会被重复编译
    • 新增依赖会被动态发现,通过 A.dd 文件更新构建图

多目标构建(多个库/模块)

假设有库 A、B、C:

  1. 每个库生成 .ddi 文件(direct dependency info,直接依赖信息)
  2. 构建步骤:
    • 预处理编译收集依赖链接
  3. 模块间依赖:
    • 库 B 中的模块可以被库 C 中的源文件使用
    • Ninja 根据 .dd 文件正确处理依赖
    • 不会破坏增量编译效率

示意

Library B:
b1.ddi → preprocess → compile → collate → B.dd
b2.ddi → ...
b3.ddi → ...
Library A:
a1.ddi → preprocess → compile → collate → A.dd
a2.ddi → ...
a3.ddi → ...
Library C:
c1.ddi → preprocess → compile → collate → C.dd
c2.ddi → ...
c3.ddi → ...
  • .ddi.dd 文件记录依赖信息
  • 支持 跨库模块依赖,确保模块先生成再被依赖文件使用
  • Ninja 根据依赖图处理增量编译和并行编译

理解总结

  1. 模块依赖关系
    • 模块必须先生成 .mod(或 .ifc)文件
    • 依赖该模块的源文件必须等待
  2. 增量编译优化
    • 如果模块未变化,依赖它的文件不会被重复编译
    • 新增依赖会动态更新构建图
  3. 多库模块支持
    • 使用 .ddi / .dd 文件保存模块依赖
    • 支持跨库模块使用
    • 构建系统(如 Ninja)能正确处理并行和增量编译
  4. 核心思想
    • 动态发现模块依赖 → 构建图更新 → 并行 + 增量编译

这段关于 C++20 模块当前支持情况(MSVC / GCC / CMake) 用整理理解:

当前模块支持现状

1. Visual Studio 2022 Preview

  • 支持模块扫描(module scanning)
    • 使用 /scanDependencies 参数生成 JSON 依赖文件
    • 示例:
      cl -std:c++20 -scanDependencies A.json -c A.cpp
      
    • 输出 JSON 文件:
      {
        "version": 1,
        "rules": [
          {
            "primary-output": "A.obj",
            "outputs": ["A.json", "A.ifc"],
            "provides": [{"logical-name": "A", "source-path": "A.cpp"}],
            "requires": [{"logical-name": "B"}]
          }
        ]
      }
      
    • 解释
      • provides → 当前编译单元提供的模块(A)
      • requires → 当前编译单元依赖的模块(B)
    • 同样 B.cpp JSON 输出没有 requires,表示它没有依赖其他模块

2. GCC(带补丁)

  • 目前 支持命名模块(named modules),但需要补丁:
  • 使用 -fmodules-ts 及依赖扫描生成 .ddi 文件
  • 示例:
    c++ -std=gnu++20 -MD -MF A.cpp.o.ddi.d -E -x c++ A.cpp \
        -MT A.cpp.o.ddi -fmodules-ts -fdep-file=A.cpp.o.ddi \
        -fdep-output=A.cpp.o -fdep-format=trtbd -o A.cpp.o.ddi.i
    
  • 输出 JSON 类似:
    {
      "rules": [
        {
          "primary-output": "A.cpp.o",
          "provides": [{"logical-name": "A"}],
          "requires": [{"logical-name": "B"}]
        }
      ]
    }
    
  • 解释
    • 与 MSVC 类似,GCC JSON 文件记录了模块提供和依赖关系
    • B.cpp 的 JSON 文件 requires 为空,表示无依赖

3. CMake 对模块的支持

  • CMake 主分支(master)已经 开始支持 C++20 模块
  • 借鉴了 Fortran 模块的动态依赖机制
  • 构建系统可利用:
    • JSON / DDI 文件进行依赖解析
    • 保证模块接口生成顺序正确
    • 支持增量构建和并行编译

理解总结

  1. 模块扫描
    • MSVC → /scanDependencies 生成 JSON
    • GCC → -fmodules-ts + -fdep-file 生成 DDI/JSON
    • JSON 文件记录 provides(提供模块)和 requires(依赖模块)
  2. 命名模块支持
    • VS 2022 Preview 支持
    • GCC 需补丁支持
  3. 构建顺序和依赖管理
    • 构建系统(如 CMake + Ninja)可以利用扫描信息
    • 确保模块先生成接口文件,再编译依赖模块
    • 支持增量构建和并行编译
  4. 结论
    • 当前基本支持命名模块
    • 构建工具链正在逐步完善对 C++20 模块的完整支持
      理解口诀
A depends on B
先生成 B.ifc
再编译 A.cpp
MSVC / GCC 都有扫描 JSON
CMake 可自动解析依赖

CMake 3.23+ 的 FILE_SET 功能 用整理理解:

FILE_SET 概念

  • FILE_SET 是 CMake 3.23 引入的新特性
  • 作用:把一组文件(通常是头文件或 C++ 模块)组织到目标(target)中,便于管理和安装

语法

target_sources(<target>
    [<INTERFACE|PUBLIC|PRIVATE>]
    [FILE_SET <set> [TYPE <type>] [BASE_DIRS <dirs>...] [FILES <files>...]]...)
  • <target> → 目标名(库或可执行文件)
  • <INTERFACE|PUBLIC|PRIVATE> → 文件集的作用域
  • FILE_SET <set> → 文件集名称
  • TYPE → 文件类型,目前只支持:
    • HEADERS → 头文件
    • CXX_MODULES → C++ 模块
  • BASE_DIRS → 文件所在目录
  • FILES → 文件列表

注意:目标不能是 custom targetframework target

使用 FILE_SET 管理头文件

旧方式(没有 FILE_SET)

set(Eigen_headers
  src/eigen.h
  src/vector.h
  src/matrix.h
)
add_library(Eigen INTERFACE ${Eigen_headers})
target_include_directories(Eigen INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
  $<INSTALL_INTERFACE:include/Eigen>
)
install(TARGETS Eigen EXPORT eigenExport)
install(EXPORT eigenExport NAMESPACE Upstream:: DESTINATION lib/cmake/Eigen)
install(FILES ${Eigen_headers} DESTINATION include/Eigen)
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Eigen)
  • 需要单独调用 install(FILES ...) 来安装头文件
  • 代码稍繁琐

新方式(使用 FILE_SET)

add_library(Eigen INTERFACE)
target_sources(Eigen INTERFACE
    FILE_SET HEADERS
    BASE_DIRS src
    FILES src/eigen.h src/vector.h src/matrix.h
)
install(TARGETS Eigen EXPORT eigenExport
    FILE_SET HEADERS DESTINATION include/Eigen
)
install(EXPORT eigenExport NAMESPACE Upstream:: DESTINATION lib/cmake/Eigen)
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Eigen)
  • 优点:
    • 文件集(FILE_SET)直接关联到库目标
    • 安装时可直接指定 FILE_SET HEADERS DESTINATION
    • 更加整洁和可维护

安装执行

cmake --install . --prefix $HOME/Work/cxxmodules/file_set/b/inst
ninja install
  • 输出示例:
Installing: .../inst/include/Eigen/eigen.h
Installing: .../inst/include/Eigen/vector.h
Installing: .../inst/include/Eigen/matrix.h
Installing: .../inst/lib/cmake/Eigen/eigenExport.cmake
  • 头文件和 CMake 配置文件自动安装

FILE_SET 的扩展

  • 文件类型 TYPE 目前可选:
    • HEADERS → 普通头文件
    • CXX_MODULES → C++20 模块文件
  • 使用 FILE_SET 可以方便管理 头文件 + 模块文件,尤其适合 header-only 库模块库

总结

  1. FILE_SET = 目标的文件集合(头文件或 C++ 模块)
  2. 作用
    • 管理目标源文件
    • 简化安装(install)步骤
    • 方便导出到 CMake 包
  3. 使用方法
    • target_sources() 指定 FILE_SET
    • install(TARGETS ... FILE_SET ...) 安装文件集
  4. 类型
    • HEADERS → 头文件
    • CXX_MODULES → C++ 模块
      理解口诀
库有头文件?FILE_SET HEADERS
库有模块文件?FILE_SET CXX_MODULES
target_sources + install 一起用
安装更方便,代码更整洁

C++20 模块 + CMake FILE_SET + 构建流程 内容用整理和理解:

1. FILE_SET 与 C++20 模块

  • CMake 3.23 引入 FILE_SET 支持模块文件(CXX_MODULES
  • 示例:
cmake_minimum_required(VERSION 3.23)
project(simple CXX)
set(CMAKE_CXX_STANDARD 20)
add_library(simple)
target_sources(simple
  PRIVATE
  FILE_SET cxx_modules TYPE CXX_MODULES FILES
  A.cpp B.cpp
)
  • A.cppB.cpp 被作为 C++模块文件加入目标 simple
  • 注意:C++20 模块支持在 CMake 中仍是 实验性,通过 CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 激活

2. MSVC 编译模块流程

  • 使用 ninja + CMake 构建
  • 核心步骤:
    1. 编译每个单元(TU)生成 .obj 并扫描依赖
      cl.exe /std:c++20 -interface -scanDependencies A.cpp
      cl.exe /std:c++20 -interface -scanDependencies B.cpp
      
    2. cmake_ninja_dyndep 生成 dyndep 文件 (CXX.dd)
    3. 再根据依赖顺序编译生成 .obj,最后生成 .lib.exe
  • 文件说明:
    • .ddi → 单个 TU 的 JSON 依赖文件(MSVC: .obj.ddi
    • CXX.dd → 合并所有 TU 的动态依赖文件
    • TU.modmap → 编译器使用的 BMI(Built Module Interface)映射
    • CXXModules.json → CMake 可读版本

3. g++ 编译模块流程(带 patch)

  • 类似 MSVC,但使用 GCC 的模块扫描与 BMI 生成
  • 核心命令:
    c++ -std=gnu++20 -fmodules-ts -fdep-file=... -fdep-output=... -fmodule-mapper=...
    
  • Ninja + CMake 处理依赖顺序
  • 最终生成 .o 和静态库 .a

4. 模块类型概念

  1. Module Unit
    • 含有 module 声明的单元(TU)
  2. Named Module
    • 同一 module-name所有模块单元集合
  3. Module Interface Unit
    • export module ... 开头的模块单元
    • 对外提供模块接口
  4. Module Implementation Unit
    • 没有 export 的模块单元
    • 提供实现,不对外导出
  5. Module Partition
    • 模块内部划分单元
    • module <name>:<partition>
    • 接口单元或实现单元都可以是 partition
    • 规则:
      • Interface Unit partition 必须被主模块接口导出
      • Partition 仅能被同模块的其他 TU 导入

5. 模块示例

假设模块 A 有 4 个单元:

// Translation unit #1: primary interface
export module A;
export import :Foo;
export int baz();
// Translation unit #2: interface partition
export module A:Foo;
import :Internals;
export int foo() { return 2*(bar()+1); }
// Translation unit #3: implementation partition
module A:Internals;
int bar();
// Translation unit #4: implementation unit
module A;
import :Internals;
int bar() { return baz()-10; }
int baz() { return 30; }
  • 结构:
    1. 主模块接口单元 (A)
    2. 分区接口单元 (A:Foo)
    3. 分区实现单元 (A:Internals)
    4. 实现单元(提供 barbaz
  • 外部只看得到主接口和接口分区,内部实现不可被外部直接导入

6. CMake + FILE_SET + 模块的优势

  • 将模块文件作为 FILE_SET 类型 CXX_MODULES 添加到库目标
  • Ninja + CMake 能自动:
    • 扫描依赖
    • 排序编译顺序
    • 并行编译
  • 支持 MSVC 和 GCC(带补丁)
  • 可管理大型模块库,保证正确依赖关系
    总结口诀
module unit = TU里有module
named module = 同名模块集合
interface unit = export module
implementation unit = 非export模块
partition = module内部划分单元
CMake FILE_SET + ninja dyndep自动处理依赖

CMake + C++20 模块 (STD example & Named Modules) 内容整理成理解,重点讲清楚核心概念、问题和解决方案。

1. 标准示例:CMake FILE_SET + 模块

初始写法(有问题)

add_library(std_module_example)
target_link_libraries(std_module_example t3lib)
target_sources(std_module_example
    PUBLIC
     FILE_SET cxx_modules TYPE CXX_MODULES FILES
     t1.cxx t2.cxx t3.cxx t4.cxx
)
add_executable(main main.cxx)
target_link_libraries(main std_module_example)
  • 问题t4.cxx 并没有导出任何模块,但被放在 CXX_MODULES 文件集中
  • GCC 构建失败:
CMake Error: Output t4.cxx.o is of type `CXX_MODULES` but does not provide a module

修正方法

把非模块文件单独处理:

add_library(std_module_example)
target_link_libraries(std_module_example t3lib)
target_sources(std_module_example
  PRIVATE
     t4.cxx  # 普通源文件
  PUBLIC
     FILE_SET cxx_modules TYPE CXX_MODULES FILES
     t1.cxx t2.cxx t3.cxx
)
  • 规则:FILE_SET cxx_modules 只能包含模块文件
  • 普通 .cpp/.cxx 文件不能放进去

2. MSVC 内部模块修复(VS Dev17.3 Preview)

  • CL.exe 现在正确输出依赖信息
  • /scanDependencies 有 ISO 标准模式和 MS 扩展模式
  • 输出 JSON 中增加 is-interface 字段
  • 可以在 CMake 中集成,只在 MSVC 扩展模式下需要标记源文件

3. 模块拆分跨库

示例:将 t3.cxx 拆到单独库 t3lib

add_library(t3lib)
target_sources(t3lib
  PUBLIC
      FILE_SET cxx_modules_internals TYPE CXX_MODULE FILES
        t3.cxx
)
add_library(std_module_example)
target_link_libraries(std_module_example t3lib)
target_sources(std_module_example
  PRIVATE t4.cxx
  PUBLIC FILE_SET cxx_modules TYPE CXX_MODULES FILES t1.cxx t2.cxx
)
add_executable(main main.cxx)
target_link_libraries(main std_module_example)
  • 好处:
    • 可以把模块内部实现单独放库
    • 主库只暴露模块接口和其他模块文件

4. Named Modules 示例

模块结构:

  • mymodule.cpp:模块接口单元,导出 mod_f,导入内部 partition
  • mymodule_impl.cpp:模块实现单元,实现 mod_gpart_g
  • mymodule_part.cpp:模块分区,导出 part_fpart_gpart_g_impl
  • mymodule_part_impl.cpp:实现分区函数 part_g_impl
  • mymodule_part_internal.cpp:内部 partition,不导出,只模块内部使用
    main.cpp 导入模块使用:
import MyModule;
int main() {
    mod_f();
    mod_g();
    part_f();
    part_g();
    part_g_impl();
    return 0;
}

CMake 配置:

add_library(with_named_modules)
target_sources(with_named_modules
    PRIVATE mymodule_impl.cpp mymodule_part_impl.cpp main.cpp
    PUBLIC FILE_SET cxx_modules TYPE CXX_MODULES FILES
        mymodule_part.cpp
        mymodule.cpp
        mymodule_part_internal.cpp
)

5. 安装和导出

install(TARGETS with_named_modules
    EXPORT with_named_modules
    RUNTIME DESTINATION "bin" COMPONENT "runtime"
    ARCHIVE DESTINATION "lib" COMPONENT "development"
    LIBRARY DESTINATION "lib" COMPONENT "runtime"
    FILE_SET cxx_modules DESTINATION "include/cxx/cxx-modules-examples" COMPONENT "development"
    FILE_SET cxx_modules_internals DESTINATION "include/cxx/cxx-modules-examples" COMPONENT "development"
    CXX_MODULES_BMI DESTINATION "lib/${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}/$<CONFIG>" COMPONENT "development"
)
export(EXPORT with_named_modules
    NAMESPACE NS::
    FILE "${CMAKE_BINARY_DIR}/lib/cmake/cxx-modules-examples/with_named_modules-targets.cmake"
    CXX_MODULES_DIRECTORY "with_named_modules-cxx-modules"
)
  • 会安装模块源文件、BMI 文件、CMake Targets
  • Windows 和 Linux/GCC 都可执行

6. CMake 开启实验模块支持

GCC

set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "uuid")
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
set(CMAKE_CXX_STANDARD 20)
include(gcc_modules.cmake)

MSVC

set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "uuid")
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
set(CMAKE_CXX_STANDARD 20)
include(msvc_modules.cmake)
  • 通过 cmake_ninja_dyndep 或 MSVC /scanDependencies 扫描模块依赖
  • 支持 FILE_SET 类型模块文件

7. 当前状态

  • CMake 对模块 仍是实验性支持
  • MSVC 2022 Preview + patched GCC 可以使用 p1689r5 依赖扫描
  • Clang 暂未支持完整依赖扫描
  • Header Units(头文件单元)支持有限
  • 可以跨库拆分模块,并自动处理依赖顺序和 BMI 文件
    核心原则总结
  1. FILE_SET cxx_modules 只能放 模块文件
  2. 非模块源文件必须单独 PRIVATE/PUBLIC
  3. 模块拆分可分库管理(接口库 + 内部库)
  4. CMake experimental API + dyndep 扫描依赖
  5. 安装模块时同时安装源文件和 BMI 文件
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐