3. 1.3 第一个CMake项目

导语

经过前两节的学习,我们已经了解了CMake的背景理念,也完成了本地的环境安装。从本节开始,我们将真正动手编写代码。就像学习任何一门编程语言时都要从“Hello World”开始一样,我们的CMake之旅也将从一个最简单的可执行程序项目起步。

在这一节中,你不仅会写出人生中第一个CMakeLists.txt,还会完整经历CMake构建的配置(Configure)→生成(Generate)→构建(Build)三大阶段。更重要的是,我会从一开始就带你养成一个黄金习惯:将源码目录与构建目录严格分离。这个习惯将让你在未来管理复杂项目时受益匪浅。

请务必打开你的终端或命令行工具,跟着下面的步骤一步步操作。

最小可运行示例:Hello World

首先,在我们磁盘的任意位置(比如~/projectsD:projects)创建一个新的文件夹,命名为hello_cmake。这个文件夹将承载我们第一个项目的所有内容。

创建源文件

hello_cmake目录下,新建一个名为main.cpp的文件,输入以下C++代码:

#include <iostream>

int main() {
    std::cout << "Hello, CMake World!" << std::endl;
    return 0;
}

创建 CMakeLists.txt

hello_cmake目录下,再新建一个名为CMakeLists.txt的文件(注意大小写,C和M大写,无空格)。这是CMake项目的核心配置文件。输入以下内容:

cmake_minimum_required(VERSION 3.15)
project(HelloCMake VERSION 1.0 LANGUAGES CXX)

add_executable(hello main.cpp)

让我们逐行拆解这个最小配置,理解每一行的职责:

  • cmake_minimum_required(VERSION 3.15):声明本项目所需的最低CMake版本。如果系统上的CMake版本低于3.15,配置会直接报错退出,避免使用旧版本导致不可预期的行为。对于新手,建议至少设置为3.15(Modern CMake的成熟版本)。
  • project(HelloCMake VERSION 1.0 LANGUAGES CXX):定义项目名称。这会在CMake内部创建一系列与项目相关的变量(如PROJECT_NAME)。VERSION 1.0可选地声明了项目版本号,LANGUAGES CXX则显式指明这是一个C++项目。
  • add_executable(hello main.cpp):声明一个可执行文件目标(Target),名为hello,它由main.cpp这一个源文件编译链接而成。这是CMake中最基础、最常用的命令之一。

目录结构规范:源码与构建分离

在继续构建之前,我们先建立正确的目录组织观念。一个健康的CMake项目目录应该长这样:

hello_cmake/
├── CMakeLists.txt    # 项目构建配置的“入口”
├── main.cpp          # 源代码
└── build/            # 构建目录(由CMake生成,不放入版本控制)
    ├── ...           # 编译器生成的中间文件、可执行文件等

这里的关键原则是:源码目录(Source Tree)构建目录(Build Tree)必须分离。

为什么必须分离?

许多初学者会直接在项目根目录下运行cmake .,这会导致编译生成的.o文件、可执行文件、CMake缓存文件等与我们的main.cppCMakeLists.txt混在一起,就像这样:

❌ 错误示例(In-source Build)
hello_cmake/
├── CMakeLists.txt
├── main.cpp
├── CMakeCache.txt      # 缓存文件混入源码
├── CMakeFiles/         # 中间目录混入源码
├── Makefile            # 生成的构建脚本混入源码
└── hello               # 可执行文件混入源码

这种源码内构建(In-source Build)有三大弊端:

  1. 污染版本控制:你需要手动编写很长的.gitignore文件来忽略生成的垃圾文件,稍有不慎就会误提交几MB的构建产物。
  2. 难以管理多配置:如果你想同时生成Debug版本和Release版本,它们会互相覆盖,或者你需要在源码目录里创建更混乱的子目录。
  3. 清理困难:一旦构建出错,你可能分不清哪些文件是源码、哪些是生成的,甚至不敢轻易删除文件。

因此,CMake社区的最佳实践是始终坚持源码外构建(Out-of-source Build)。我们将在下一节演示具体操作。

构建流程详解:三阶段模型

CMake的构建过程不是“一键编译”那么简单,它由三个逻辑上独立的阶段组成。理解这三个阶段,是你排查构建问题的核心基础。

阶段一:配置(Configure)

配置阶段是CMake读取并解析CMakeLists.txt的过程。CMake会:

  • 检查编译器是否存在且可用;
  • 验证cmake_minimum_required的版本要求;
  • 执行CMakeLists.txt中的命令,创建内部数据结构(如目标、变量、属性等);
  • 在构建目录下生成CMakeCache.txt缓存文件,记录本次配置的结果(如编译器路径、用户选项等)。

阶段二:生成(Generate)

生成阶段紧随配置阶段之后(通常在同一个cmake命令中自动完成)。CMake根据你指定的生成器(Generator),产出底层的构建系统文件。例如:

  • 如果你使用默认的Unix Makefiles生成器,CMake会生成Makefile
  • 如果你使用Ninja,会生成build.ninja
  • 如果使用Visual Studio生成器,则会生成.sln.vcxproj项目文件。

这个阶段完成后,构建目录里已经具备了让底层构建工具(make、ninja、MSBuild等)工作的全部“图纸”。

阶段三:构建(Build)

构建阶段是实际的编译和链接发生的地方。底层构建工具(如Make或Ninja)根据生成阶段产出的图纸,调用编译器将main.cpp翻译为机器码,最终链接成可执行文件hello(或Windows下的hello.exe)。

可以用一个比喻来记忆:

  • Configure 像建筑师阅读需求文档(CMakeLists.txt);
  • Generate 像建筑师画出施工图纸(Makefile/build.ninja);
  • Build 像工人根据图纸盖房子(编译链接出二进制文件)。

命令行基础操作

现在,让我们用命令行来完成第一个项目的配置和构建。请确保终端当前的工作目录hello_cmake

Step 1:创建构建目录并进入

mkdir build
cd build

Step 2:配置与生成(Configure & Generate)

运行以下命令。这会在当前目录(build/)下查找上一级目录的CMakeLists.txt,并在当前目录生成构建系统文件:

cmake ..

这里的..表示“源码目录在我的上一级”。如果你看到类似下面的输出,说明配置和生成成功了:

-- 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++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/projects/hello_cmake/build

此时观察build/目录,你会发现CMake生成了CMakeCache.txtCMakeFiles/目录和Makefile(在Linux/macOS或MinGW环境下)。

Step 3:构建(Build)

CMake提供了一个与底层构建工具无关的通用构建命令:

cmake --build .

这里的.表示“在当前目录下执行构建”。这条命令会自动调用合适的底层工具(如makeninja)来完成编译。

构建成功后,你会在build/目录下看到生成的可执行文件:

  • Linux/macOS:hello
  • Windows:Debughello.exe(某些生成器会附加配置子目录)或hello.exe

运行它:

./hello          # Linux/macOS
# 或
Debughello.exe  # Windows (Visual Studio 生成器)

你应该能看到终端输出:Hello, CMake World!

关于 ctest 的基础用法

虽然详细的测试框架会在第七章展开,但ctest作为CMake原生的测试运行工具,值得我们在这里混个脸熟。CMake在生成阶段会准备一个CTestTestfile.cmake。如果项目中定义了测试(通过add_test),你可以在构建完成后运行:

ctest

在我们的Hello World项目中,目前还没有定义任何测试,所以运行ctest会提示没有发现测试。没关系,先记住这条命令,后续我们会频繁使用它。

构建目录管理:In-source vs Out-of-source 实战对比

为了让你直观感受两种构建方式的差异,我们做一个对比实验。

实验 A:Out-of-source(推荐方式)

这就是我们刚才做的。源码目录始终保持干净:

# 在 hello_cmake/build 目录下
cmake ..
cmake --build .

如果你不满意当前的构建结果,或者想换用不同的编译选项重新来过,只需整个删除build目录即可,源码毫发无损:

cd ..
rm -rf build   # Linux/macOS
# 或
rmdir /s /q build   # Windows CMD

这种“随时删、随时建”的特性是Out-of-source最大的优势。

实验 B:In-source(仅作了解,切勿模仿)

为了对比,我们也可以在源码目录直接配置:

# 注意:这次我们直接在 hello_cmake 目录下操作
cd ..   # 如果刚才在build里,先回到hello_cmake
cmake .

执行后,你会发现项目根目录瞬间多出了CMakeCache.txtCMakeFilesMakefile等文件,与main.cppCMakeLists.txt混在一起。构建产生的目标文件也会散落在CMakeFiles子目录中。

强烈建议:立即清理这个混乱现场,回归Out-of-source模式:

# 在 hello_cmake 目录下清理
rm -rf CMakeCache.txt CMakeFiles Makefile cmake_install.cmake hello
mkdir build
cd build
cmake ..

小结

在本节中,我们完成了从零到一的突破:

  • 编写了包含cmake_minimum_requiredprojectadd_executable的最小CMakeLists.txt
  • 建立了源码外构建(Out-of-source Build)的正确观念,养成了使用独立build/目录的习惯;
  • 理解了CMake构建的配置→生成→构建三阶段模型;
  • 掌握了三条最基础的命令:cmake ..(配置生成)、cmake --build .(构建)、ctest(运行测试)。

这只是一个开始。下一节,我们将深入CMakeLists.txt的语法基础,学习变量、列表、条件判断和循环,为你的CMake脚本编写能力打下坚实根基。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……