3、第一个CMake项目:创建最小CMakeLists.txt,使用cmake和cmake --build命令,理解构建目录与源码目录分离
当我们第一次接触CMake时,最容易产生的困惑是:它到底是一个“编译器”还是一个“构建工具”?实际上,CMake是一个构建系统生成器。它本身并不直接编译代码,而是读取一个名为 CMakeLists.txt 的文本文件,根据当前平台、编译器环境和用户指定的选项,生成对应的原生构建文件(如Unix/Linux下的Makefile、Windows下的Visual Studio解决方案或Ninja构建文件)。本章将从一个最简单的“Hello World”项目入手,完整演示从创建源码、编写CMakeLists.txt、执行配置到最终编译运行的整个过程,并深入剖析“源码目录”与“构建目录”分离这一CMake核心设计理念。
上图展示了CMake项目构建的核心链路。左侧是源码目录,右侧是构建目录,两者通过 cmake 命令建立联系。下面我们逐步拆解每一个环节。
3.1 核心原理:CMake的两阶段工作模式
CMake的工作流程分为两个截然不同的阶段:配置阶段(Configure)和生成阶段(Generate),这两个阶段通常由 cmake 命令一次性完成。配置阶段会解析 CMakeLists.txt 中的所有指令,检查编译器、库依赖、系统特性,并将结果缓存到 CMakeCache.txt 中。生成阶段则根据配置结果和当前平台,产出具体的构建文件(Makefile、Ninja文件或Visual Studio工程)。
理解这一原理的关键在于:CMake不是构建工具,而是构建工具的“生成器”。它把“描述项目需要什么”和“如何调用编译器”这两件事分离开来。开发者只需要在 CMakeLists.txt 中声明目标(target)、源文件、依赖关系,CMake负责将其翻译成底层构建工具能理解的规则。
关键认知:CMake的输入是 CMakeLists.txt(人类可读的声明式脚本),输出是Makefile或.sln(机器可读的构建规则)。开发者不应该直接修改生成的Makefile,因为下次运行cmake时这些修改会被覆盖。所有修改都应作用于 CMakeLists.txt。
3.2 操作流程:从零创建第一个CMake项目
下面我们通过一个完整的实操流程,演示如何创建并构建一个最小CMake项目。假设我们的项目名为 hello_cmake,使用C++编写。
步骤1:创建项目目录结构
mkdir hello_cmake
cd hello_cmake
mkdir src
touch src/main.cpp
touch CMakeLists.txt
此时目录结构如下:
hello_cmake/
├── CMakeLists.txt
└── src/
└── main.cpp
步骤2:编写源代码
在 src/main.cpp 中输入以下内容:
#include <iostream>
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
步骤3:编写最小CMakeLists.txt
在项目根目录的 CMakeLists.txt 中写入:
cmake_minimum_required(VERSION 3.16)
project(hello_cmake LANGUAGES CXX)
add_executable(hello src/main.cpp)
这三行代码的含义如下:
cmake_minimum_required(VERSION 3.16):声明CMake最低版本要求。如果本机CMake版本低于3.16,会报错并终止。project(hello_cmake LANGUAGES CXX):定义项目名称,并声明使用C++语言。这会触发CMake检查C++编译器是否可用。add_executable(hello src/main.cpp):声明一个可执行文件目标hello,其源文件为src/main.cpp。
步骤4:运行cmake进行配置
在项目根目录执行以下命令(注意-S指定源码目录,-B指定构建目录):
cmake -S . -B build
终端输出类似:
-- The CXX compiler identification is GNU 11.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ - done
-- Configuring done
-- Generating done
-- Build files have been written to: /path/to/hello_cmake/build
此时 build 目录被创建,内部生成了 CMakeCache.txt、CMakeFiles/、Makefile 等文件。
步骤5:编译项目
cmake --build build
输出:
[ 50%] Building CXX object CMakeFiles/hello.dir/src/main.cpp.o
[100%] Linking CXX executable hello
编译完成后,在 build 目录下会生成可执行文件 hello。运行它:
./build/hello
输出:
Hello, CMake!
经验提醒:使用 cmake --build build 而不是直接进入build目录执行 make,是因为前者是跨平台的。在Windows上,它会自动调用MSBuild或Ninja;在Linux/macOS上,它调用make。这样你的构建命令在所有平台上保持一致。
3.3 案例拆解:源码目录与构建目录分离的实战价值
为了深入理解目录分离的意义,我们模拟一个真实开发场景。假设你正在参与一个嵌入式Linux项目,需要为两种不同的硬件平台(ARM Cortex-A9和ARM Cortex-A53)交叉编译同一个应用程序。两种平台使用不同的工具链和编译选项。
传统做法(不分离):直接在源码目录下执行 cmake .,会在源码目录中生成大量 CMakeFiles/、CMakeCache.txt、Makefile 等文件。当你需要切换平台时,必须删除这些文件重新配置,否则会因缓存冲突导致编译错误。而且源码目录被污染,不利于版本控制。
CMake推荐做法(分离):为每个平台创建独立的构建目录:
# 为ARM A9平台构建
cmake -S . -B build-a9 -DCMAKE_TOOLCHAIN_FILE=arm-a9-toolchain.cmake
cmake --build build-a9
# 为ARM A53平台构建
cmake -S . -B build-a53 -DCMAKE_TOOLCHAIN_FILE=arm-a53-toolchain.cmake
cmake --build build-a53
两个构建目录互不干扰,各自保存自己的缓存和生成文件。切换平台时无需删除任何东西,只需使用对应的构建目录即可。此外,你还可以为同一个平台创建 build-debug 和 build-release 目录,分别用于调试和发布版本。
| 对比维度 | 源码目录内构建(不推荐) | 独立构建目录(推荐) |
|---|---|---|
| 源码目录状态 | 被CMakeFiles/、CMakeCache.txt等污染 | 保持纯净,只包含源代码和CMakeLists.txt |
| 多平台支持 | 需要反复删除缓存,容易出错 | 每个平台一个目录,互不干扰 |
| 版本控制 | 需要手动添加大量.gitignore规则 | 只需忽略build*/目录 |
| 清理操作 | 手动删除多个分散的中间文件 | 直接删除整个build目录,干净彻底 |
| 并行构建 | 同一目录下无法同时执行不同配置 | 可同时运行多个构建任务 |
3.4 常见误区与总结复盘
初学者在使用CMake时,最容易犯以下几个错误:
误区一:在源码目录下直接运行 cmake .
这会将所有生成文件写入源码目录。虽然能工作,但会带来一系列问题:无法同时维护多个配置、版本控制混乱、清理困难。正确的做法是始终使用 -B 参数指定构建目录。
误区二:忘记 cmake_minimum_required
有些教程为了简化省略了这行。但在实际项目中,这行是必须的。它确保CMake版本兼容性,避免新版本特性在旧版本上产生不可预知的错误。
误区三:手动修改生成的Makefile
生成的Makefile是CMake的输出产物,任何手动修改都会在下次运行cmake时被覆盖。如果需要调整编译选项,应该修改 CMakeLists.txt 中的 target_compile_options() 或 add_compile_options()。
总结复盘:本章的核心知识点可以归纳为以下四点:
- CMake是构建系统生成器,不是编译器也不是构建工具本身。它读取
CMakeLists.txt,生成Makefile或Ninja文件。 - 最小CMakeLists.txt只需三行:
cmake_minimum_required()、project()、add_executable()。这三行构成了任何CMake项目的基础骨架。 - 构建命令只有两条:
cmake -S . -B build用于配置,cmake --build build用于编译。这两条命令覆盖了90%的日常构建需求。 - 目录分离是CMake的设计精髓。它保证了源码的纯净性,支持多配置并行,简化了清理和版本控制。这是CMake区别于传统Makefile的重要优势之一。