一、CMake 的设计哲学:为什么会有它?
说实话,我刚接触 CMake 的时候,心里是有点抵触的。那时候我已经用 Makefile 写了好几年,觉得「make 就挺好,干嘛要学新东西?」直到我在一个跨平台项目上栽了跟头——同样的代码,Linux 上编译好好的,Windows 上折腾了两天还没跑起来。嗯,从那以后,我老老实实开始学 CMake。
CMake 的设计哲学,说白了就一句话:一次编写,到处编译。它不直接编译代码,而是生成对应平台的构建文件。在 Linux 上生成 Makefile,在 Windows 上生成 Visual Studio 工程,在 macOS 上生成 Xcode 项目。你只需要写一份 CMakeLists.txt,剩下的交给 CMake 去处理。
核心思想:CMake 是一个构建系统生成器(Build System Generator),不是构建系统本身。它把「描述项目结构」和「调用编译器」这两件事分开了。
我个人习惯把 CMake 比作一个翻译官。你跟它说「我要编译一个叫 hello 的程序,源文件是 main.c」,它转头就去跟 make 说「嘿,帮我把 main.c 编译成 hello」。你不需要关心 make 的语法,CMake 帮你搞定。
二、CMakeLists.txt 的基本结构
每个 CMake 项目,根目录下必须有一个 CMakeLists.txt 文件。这是 CMake 的入口,也是你唯一需要手写的构建描述文件。
一个最简单的 CMakeLists.txt 长这样:
cmake_minimum_required(VERSION 3.10)
project(HelloWorld)
add_executable(hello main.c)
三行代码,搞定一个可执行文件的构建。我们来拆开看看每一行在干什么。
2.1 cmake_minimum_required()
这行指定了 CMake 的最低版本。为什么要写这个?我在项目中遇到过,有人用 CMake 2.8 的语法写项目,结果同事装了 CMake 3.20,有些旧语法已经不兼容了。加上这行,CMake 会检查当前版本,不够就报错,避免莫名其妙的编译失败。
我的建议:不要写太老的版本。现在主流项目一般写 3.10 或 3.16。太老的版本会限制你使用新特性。
2.2 project()
project() 是每个 CMake 项目的灵魂。它定义项目名称,还会隐式设置一些变量:
PROJECT_NAME— 项目名,比如上面的 "HelloWorld"PROJECT_SOURCE_DIR— 源码根目录PROJECT_BINARY_DIR— 构建目录
你还可以在 project() 里指定语言:
project(MyProject C CXX) # 明确使用 C 和 C++
如果不写,CMake 默认支持所有语言。我个人习惯显式指定,这样 CMake 不会去检测 Fortran、ASM 这些我用不到的编译器,能省一点配置时间。
2.3 add_executable()
这行告诉 CMake:我要生成一个可执行文件。第一个参数是目标名,后面的参数是源文件列表。
add_executable(hello main.c utils.c logger.c)
目标名 hello 会变成最终的可执行文件名(Windows 下是 hello.exe)。源文件可以写多个,用空格隔开。
注意:源文件路径是相对于当前 CMakeLists.txt 的。如果源文件在子目录里,要写相对路径,比如 src/main.c。我曾经见过有人写绝对路径,结果换台机器就编译不了。
三、cmake 命令与构建流程
写好了 CMakeLists.txt,接下来就是运行 cmake 命令。整个流程分两步:
- 配置阶段(Configure): cmake 读取 CMakeLists.txt,检查编译器、依赖库,生成构建文件。
- 构建阶段(Build): 用生成的构建文件去编译代码。
实际操作是这样的:
# 1. 创建构建目录(我习惯叫 build)
mkdir build && cd build
# 2. 配置
cmake ..
# 3. 构建
cmake --build .
为什么要单独建一个 build 目录?这叫「外部构建」(Out-of-source build)。生成的中间文件、目标文件全在 build 里,源码目录干干净净。如果你哪天想清理,直接 rm -rf build 就行,不会误删源码。
重要:永远不要在源码目录里直接运行 cmake。这是很多新手会犯的错误。一旦在源码目录里生成了构建文件,清理起来很麻烦。
cmake --build . 是 CMake 提供的通用构建命令。它会自动调用 make(或者 ninja、msbuild 等),不需要你手动敲 make。我个人觉得这个命令很实用,尤其是在 Windows 上,你不需要记住 Visual Studio 的那套命令行参数。
四、一个完整的入门示例
我们来写一个稍微完整点的例子。假设项目结构如下:
myapp/
├── CMakeLists.txt
├── src/
│ ├── main.c
│ └── utils.c
└── include/
└── utils.h
CMakeLists.txt 的内容:
cmake_minimum_required(VERSION 3.10)
project(MyApp VERSION 1.0.0 LANGUAGES C)
# 指定头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 收集源文件
set(SOURCES
src/main.c
src/utils.c
)
# 生成可执行文件
add_executable(myapp ${SOURCES})
这里用到了两个新命令:
include_directories()— 告诉编译器去哪里找头文件set()— 定义变量,把源文件列表存起来,方便管理
构建命令和之前一样:
mkdir build && cd build
cmake ..
cmake --build .
./myapp
五、CMake 的构建流程全景图
下面这张图展示了 CMake 从 CMakeLists.txt 到最终可执行文件的完整流程:
从这张图可以看得很清楚:CMake 的工作分两个阶段。配置阶段生成构建文件,构建阶段才真正编译代码。这两个阶段是分开的,你修改了 CMakeLists.txt 只需要重新配置,不需要重新构建所有代码。
六、几个实用小技巧
技巧1:用 cmake -B build 替代 mkdir build && cd build && cmake ..。CMake 3.13 之后支持 -B 参数,会自动创建构建目录并配置。
技巧2:想看看 CMake 到底检测到了什么?用 cmake -LA .. 可以列出所有缓存变量,包括编译器路径、安装路径等。
避坑指南:我曾经在 Windows 上遇到一个问题——CMake 找不到 Visual Studio。后来发现是因为没有在「Visual Studio 开发人员命令提示符」里运行 cmake。如果你用 Windows,记得从开始菜单打开「Developer Command Prompt for VS」,再执行 cmake 命令。
好了,这一章的内容就到这里。CMake 入门其实不难,记住三个核心命令:cmake_minimum_required()、project()、add_executable(),再加上外部构建的习惯,你就能应付大部分简单项目了。