CMake 入门
不论你是写 C/C++、CUDA,还是要同时照顾 Linux/Windows/macOS,当项目走出“玩具”阶段,构建就会成为最大的摩擦源:不同编译器、不同 IDE、不同参数、不同依赖的发现与链接。如果还靠手写 Makefile
或分别维护 VS/Xcode 工程,成本会直线上升。
这正是 CMake 出现的原因:用一份跨平台的构建脚本,生成各平台各 IDE 需要的工程与命令。
CMake 解决了哪些痛点(精简版)
跨平台:一份
CMakeLists.txt
,到处生成工程(Ninja/Makefiles、Visual Studio、Xcode)。少折腾:自动管理头文件目录、编译选项、链接依赖,不用手拼长命令。
更清爽:源代码与构建产物分离,目录不再被临时文件污染。
一图速懂:它到底做了什么
graph TD;
A["写 CMakeLists.txt"] --> B["cmake -S 源码 -B 构建"];
B --> C["构建:cmake --build 构建目录"];
C --> D["运行:./build/hello(或 IDE 里运行)"];
B --> E["按平台生成合适的工程/构建文件"];
为什么在 B 分叉
- B 是“配置/生成”阶段,产生两种后续选择:
- 走 C:继续用
cmake --build
统一构建(CMake 调用本机构建器编译/链接)。 - 走 E:直接使用已生成的本机构建文件(Makefile、Ninja、Visual Studio、Xcode 工程)在命令行或 IDE 中构建。
- 二者本质等价,通常二选一即可;团队混用也没问题。
配置阶段(B)的产物
- 本机构建文件:
Makefile
、build.ninja
、VS.sln/.vcxproj
、Xcode.xcodeproj
(由生成器决定)。 - CMake 缓存与元数据:
CMakeCache.txt
、CMakeFiles/
、平台/编译器/特性探测结果。 - 可选辅助:
compile_commands.json
(-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
)、通过configure_file()
生成的配置头/源文件。 - 位置:全部写入
-B
指定的构建目录。
构建阶段(C)的产物
- 可执行文件:如
build/hello
(Windows 为build/hello.exe
)。 - 库文件:静态库(
.a
/.lib
)、共享库(.so
/.dll
/.dylib
)。 - 中间文件:目标文件(
*.o
/*.obj
)、依赖/缓存等,位于构建目录。 - 自定义输出:可用
CMAKE_RUNTIME_OUTPUT_DIRECTORY
、CMAKE_LIBRARY_OUTPUT_DIRECTORY
、CMAKE_ARCHIVE_OUTPUT_DIRECTORY
调整位置。
5 分钟上手
目录结构:
1 | your_project/ |
CMakeLists.txt
:
1 | cmake_minimum_required(VERSION 3.20) |
src/main.cpp
:
1 |
|
构建与运行:
1 | cmake -S . -B build |
CMake 改变了什么:一个对比例子
场景:两个源文件 src/main.cpp
、src/util.cpp
,需要 C++17,并在 Linux 与 Windows 上都能构建。
没有 CMake(每个平台都要记不同命令)
Linux(GCC/Clang):
1
g++ -std=c++17 -O2 src/main.cpp src/util.cpp -Iinclude -o app
- Windows(MSVC):
1
cl /std:c++17 /O2 src\main.cpp src\util.cpp /I include /Fe:app.exe
维护代价:命令不通用,IDE 工程(VS/Xcode)还要各自维护一份。
有 CMake(一份脚本到处用)
CMakeLists.txt
:
1
2
3
4
5cmake_minimum_required(VERSION 3.20)
project(app LANGUAGES CXX)
add_executable(app src/main.cpp src/util.cpp)
target_compile_features(app PRIVATE cxx_std_17)
target_include_directories(app PRIVATE include)
构建命令(所有平台一致):
1
2cmake -S . -B build
cmake --build build
效果:不用关心底层是 Make 还是 Ninja、GCC 还是 MSVC,也不用为 VS/Xcode 手工维护工程;需要 IDE 时让 CMake 直接生成即可。诸如 -lm
、异常/运行时等平台差异,交给生成器与工具链处理。
g++ 命令解析(在没有 CMake 时)
以 Linux 的命令为例:
1 | g++ -std=c++17 -O2 src/main.cpp src/util.cpp -Iinclude -o app |
g++:调用 GNU C++ 编译器。
-std=c++17:启用 C++17 标准(在 MSVC 下等价为
/std:c++17
)。-O2:开启二级优化(体积与速度的折中)。
src/main.cpp src/util.cpp:要编译并参与链接的源文件。
-Iinclude:增加头文件搜索目录
include
。-o app:输出可执行文件名为
app
。
在实际项目中,这条命令会迅速膨胀:再加多目录、宏定义、库搜索路径(-L
)、链接库(-lxxx
)、平台差异参数,就会变得又长又难复用。这正是 CMake 希望你“声明语义、自动映射细节”的原因。
工作原理
graph TD;
A["CMakeLists.txt:声明要什么"] --> B["配置阶段:检测平台/编译器/特性"];
B --> C["选择生成器:Ninja/Makefiles/VS/Xcode"];
C --> D["生成本机构建文件(规则/工程)"];
D --> E["cmake --build:调用本机构建器编译/链接"];
一句话:你只写“意图”,CMake 按“环境”翻译成正确做法。
这几行背后发生了什么
1 | cmake_minimum_required(VERSION 3.20) |
cmake_minimum_required(VERSION 3.20):锁定最低版本,启用对应语法与行为,避免旧版差异。
project(app LANGUAGES CXX):确定工程名与语言;CMake 会探测可用的 C++ 编译器与平台特性。
add_executable(app …):声明目标
app
;CMake 为它建立依赖图,并在生成阶段把它翻译成本机的规则/工程项。target_compile_features(app PRIVATE cxx_std_17):声明“需要 C++17”,CMake 按编译器映射成正确开关:
GCC/Clang:
-std=c++17
MSVC:
/std:c++17
target_include_directories(app PRIVATE include):声明头文件目录,按平台生成参数:
GCC/Clang:
-I include
MSVC:
/I include
这就是“一份脚本到处用”的核心:写语义,不写平台细节;由生成器与工具链适配差异。
CMake 支持哪些语言?
首要用户群是 C/C++,但并不止于此。常见支持包括:
C、C++:最成熟、生态最广。
CUDA、HIP:GPU 相关项目常用。
Fortran:在科学计算/数值领域仍然重要。
Objective-C/Swift(通过外部集成):在 Apple 平台工程生成方面配合 Xcode。
ASM、ISPC 等:低层或专用场景。
此外,CMake 也常被当作“元构建工具”,去驱动其他语言/工具链(如生成外部工具命令、打包脚本)。原则是:若目标/命令能被描述成“配置+构建”的过程,CMake 往往能充当胶水把它们组织起来,但其强项仍然是 C/C++ 及其周边。
就这么记住
写清“要什么”:目标、源文件、需要的标准或库。
总在源码外构建:
-S
指源码,-B
指构建目录。统一的构建命令:
cmake --build
,不用关心底层是 Make 还是 Ninja。
结语
先用这套最小流程把程序跑起来,再根据需要逐步增加依赖与选项;当你不想再重复为不同平台维护多套工程时,CMake 就是省心的那一个。