3、第一个CMake项目:创建最小CMakeLists.txt,使用cmake和cmake --build命令,理解构建目录与源码目录分离

当我们第一次接触CMake时,最容易产生的困惑是:它到底是一个“编译器”还是一个“构建工具”?实际上,CMake是一个构建系统生成器。它本身并不直接编译代码,而是读取一个名为 CMakeLists.txt 的文本文件,根据当前平台、编译器环境和用户指定的选项,生成对应的原生构建文件(如Unix/Linux下的Makefile、Windows下的Visual Studio解决方案或Ninja构建文件)。本章将从一个最简单的“Hello World”项目入手,完整演示从创建源码、编写CMakeLists.txt、执行配置到最终编译运行的整个过程,并深入剖析“源码目录”与“构建目录”分离这一CMake核心设计理念。

CMake 最小项目构建流程与目录分离 源码目录 (src/) main.cpp CMakeLists.txt 用户编写 · 只读 · 不产生中间文件 构建目录 (build/) CMakeCache.txt Makefile / .sln CMakeFiles/ 可执行文件 自动生成 · 可删除 · 与源码隔离 cmake -S . -B build 读取CMakeLists.txt cmake --build build ./build/hello 可执行文件(二进制产物) 核心原则 1. 源码目录保持纯净,不写入任何构建中间文件 2. 构建目录可随时删除,重新生成即可 3. 同一源码可对应多个构建目录(Debug/Release) 4. 便于版本控制(.gitignore 忽略 build/)

上图展示了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()。

总结复盘:本章的核心知识点可以归纳为以下四点:

  1. CMake是构建系统生成器,不是编译器也不是构建工具本身。它读取 CMakeLists.txt,生成Makefile或Ninja文件。
  2. 最小CMakeLists.txt只需三行:cmake_minimum_required()、project()、add_executable()。这三行构成了任何CMake项目的基础骨架。
  3. 构建命令只有两条:cmake -S . -B build 用于配置,cmake --build build 用于编译。这两条命令覆盖了90%的日常构建需求。
  4. 目录分离是CMake的设计精髓。它保证了源码的纯净性,支持多配置并行,简化了清理和版本控制。这是CMake区别于传统Makefile的重要优势之一。