cmake-simple-manual
CMake 不是编译器,也不直接构建代码–它是构建系统的生成器:你用声明式语法描述项目结构(有哪些目标、依赖什么、怎么编译),CMake 据此生成对应构建系统(Makefile、Ninja、VS 工程等)的文件。这层间接性让同一份 CMakeLists.txt 能跨平台、跨编译器工作。
本文从零开始搭建一个 C++ 项目,逐步引入 CMake 的核心概念,最终覆盖目标导向构建、依赖管理、安装导出和预设配置。
最小项目
一个可执行文件只需要三行:
1 | |
cmake_minimum_required:声明最低版本,同时隐式触发策略设置。必须放在第一行,project之前。project:命名项目,声明版本和使用的语言。LANGUAGES CXX限定只启用 C++,避免不必要的 C 编译器探测。add_executable:定义一个构建目标hello,源文件是main.cpp。
对应的 main.cpp:
1 | |
构建流程
CMake 的工作分两个阶段:配置(configure)和生成(generate),之后才是构建(build)。
flowchart LR
A["CMakeLists.txt"] --> B["配置阶段<br/>解析 CMake 语法<br/>检测编译器/依赖"]
B --> C["生成阶段<br/>输出 Makefile/Ninja/VS 工程"]
C --> D["构建阶段<br/>调用底层工具链<br/>编译链接"]
D --> E["可执行文件/库"]
classDef phase fill:#e3f2fd,stroke:#1565c0
classDef io fill:#fff3e0,stroke:#ef6c00
class B,C,D phase
class A,E io始终使用 out-of-source 构建:在源码目录之外创建 build 目录,避免污染源码树:
1 | |
-B build 指定构建目录,CMake 会自动创建。构建产物全部留在 build/ 中,源码目录保持干净。
目标导向构建
现代 CMake 的核心是目标(target)。每个 add_executable 或 add_library 创建一个目标,后续所有配置都通过 target_* 命令挂到目标上,而非使用全局命令污染所有目标。
库目标
1 | |
PUBLIC / PRIVATE / INTERFACE 是传播范围的关键词:
| 关键字 | 自己编译 | 消费者继承 | 适用场景 |
|---|---|---|---|
PRIVATE | 是 | 否 | 内部实现依赖 |
PUBLIC | 是 | 是 | 接口的一部分 |
INTERFACE | 否 | 是 | 仅头文件库 |
链接库
1 | |
编译选项与定义
1 | |
$<...> 是生成器表达式,在生成阶段求值。上面的写法让警告选项只在 GCC/Clang 下生效,/W4 只在 MSVC 下生效,Debug 配置才定义 DEBUG_MODE。
为什么用
target_compile_features而非set(CMAKE_CXX_STANDARD 20)?前者把标准要求绑定到具体目标,消费者链接时自动继承;后者是全局变量,子项目无法覆盖,且不表达"谁需要什么"的依赖关系。
依赖管理
find_package:查找系统已安装的库
1 | |
REQUIRED 表示找不到就立即报错终止–这是正确的做法,不要写 fallback 逻辑掩盖缺失依赖。fmt::fmt 是 CMake 导入目标(imported target),带命名空间 ::,封装了头文件路径、库文件路径和编译选项,链接它即可获得一切。
FetchContent:拉取源码依赖
系统没装或需要特定版本时,用 FetchContent 从 Git 拉取源码直接编译:
1 | |
FetchContent_MakeAvailable 一步完成下载和 add_subdirectory,依赖库的目标直接可用,和 find_package 找到的目标用法完全一致。
多目录项目
项目变大后,每个子目录放自己的 CMakeLists.txt,根目录用 add_subdirectory 串起来:
1 | |
根 CMakeLists.txt:
1 | |
math/CMakeLists.txt:
1 | |
app/CMakeLists.txt:
1 | |
CMAKE_CURRENT_SOURCE_DIR始终指向当前CMakeLists.txt所在目录,而CMAKE_SOURCE_DIR指向根目录。子项目可能被add_subdirectory引入到别的工程中,此时根目录会变,所以始终用CMAKE_CURRENT_SOURCE_DIR引用本地路径。
测试
1 | |
catch_discover_tests 在构建后运行测试可执行文件的 --list-tests,把每个测试用例注册为独立的 CTest 条目。运行测试:
1 | |
安装
1 | |
GNUInstallDirs 提供平台无关的标准安装路径(Linux 下 lib/、include/,Windows 下可能不同)。安装:
1 | |
预设
CMakePresets.json 把常用的配置参数固化下来,省去每次手敲 -D 和 -G:
1 | |
使用预设:
1 | |
预设文件应纳入版本控制,团队成员共享同一套配置。
常见陷阱
| 做法 | 问题 | 正确做法 |
|---|---|---|
include_directories() | 全局污染所有目标 | target_include_directories() |
add_definitions() | 已弃用,全局污染 | target_compile_definitions() |
link_libraries() | 全局污染 | target_link_libraries() |
GLOB 收集源文件 | 新增文件不触发重新配置 | 显式列出源文件 |
CMAKE_SOURCE_DIR | 子项目不友好 | CMAKE_CURRENT_SOURCE_DIR |
set(CMAKE_CXX_STANDARD 20) | 全局变量,不表达依赖 | target_compile_features() |
| 在源码目录内构建 | 污染源码树 | out-of-source 构建 |
速查
flowchart TB
A["CMakeLists.txt"] --> B["目标<br/>add_executable / add_library"]
B --> C["属性<br/>target_include_directories<br/>target_compile_options<br/>target_compile_features"]
B --> D["依赖<br/>target_link_libraries<br/>find_package / FetchContent"]
C --> E["生成<br/>Makefile / Ninja / VS"]
D --> E
E --> F["构建<br/>cmake --build"]
classDef tgt fill:#e3f2fd,stroke:#1565c0
classDef prop fill:#fff3e0,stroke:#ef6c00
classDef gen fill:#e8f5e9,stroke:#2e7d32
class B tgt
class C,D prop
class E,F gen核心心智模型:一切围绕目标。声明目标,用 target_* 给目标挂属性,用 target_link_libraries 表达目标间依赖。依赖关系通过 PUBLIC/PRIVATE 自动传播。这就是现代 CMake 的全部–没有全局变量,没有目录级副作用,每个目标自包含。





