基础知识
编译 C++ 源代码似乎是一个相当简单的过程。让我们创建一个简单的小程序,例如经典的 hello.cpp,如下:
chapter-01/01-hello/hello.cpp
#include <iostream>
int main() {
std::cout << "Hello World!" << std::endl;
return 0;
}
现在,要获得可执行文件,我们需要运行一行命令。用文件名作为参数调用编译器:
$ g++ hello.cpp -o a.out
代码正确,编译器会默默地帮我们生成机器能够读懂的二进制可执行文件。我们可以通过文件名来调用它:
$ ./a.out
Hello World!
$
然而,随着我们项目的发展,你很快就会明白,将所有代码保存在一个文件中是不可能的。良好的代码实践建议文件应该保持短小并且具有良好的组织结构。手工编译每个源代码文件可能是一个令人厌倦并且极易出错的过程。一定有更好的办法。
CMake 是什么?
假设我们通过编写一个脚本来自动化构建,该脚本遍历我们的项目树并编译所有内容。为了避免任何不必要的编译,我们的脚本将检测源代码在上次运行之后是否被修改过。现在,我们想要一种方便的方式来管理每个文件传递给编译器的参数——最好是我们希望根据可配置的标准来做。此外,我们的脚本应该知道如何以二进制形式链接所有已编译的文件,或者更好的是,如何构建可以在更大的项目中重用并作为模块合并的整体解决方案。
我们添加的特性越多,就越有可能得到一个成熟的解决方案。构建软件是一个通用的过程,可以包括许多不同的方面:
- 编译成可执行文件和库
- 依赖管理
- 测试
- 安装
- 打包
- 生成文档
- 更多的测试
要想出一个真正模块化的、功能强大的、适合各种用途的 C++ 构建应用程序,需要很长时间。但确实有人确实做到了。Kitware 的 Bill Hoffman 在 20 多年前实现了 CMake 的第一个版本。如你所料,它非常地成功。它现在已经有很多功能并且得到了来自社区的支持。今天,CMake 正在被积极地开发,并且已经成为 C 和 C++ 程序员的行业标准。
采用自动化方式构建代码的想法比 CMake 出现早得多,所以很自然地,有很多解决方案:Make、Autotools、SCons、Ninja、Premake 等等。但是为什么 CMake 如今独占鳌头呢?
就个人主观而言,我认为 CMake 做到了以下几点:
- 它专注于支持现代编译器和工具链
- CMake 真正做到了跨平台——它支持在 Windows、Linux、macOS 和 Cygwin 上构建
- 它可以为主流的 IDE 软件生成项目文件,如:Microsoft Visual Studio、Xcode 和
Eclipse CDT。此外,它也是其他软件(如 CLion)的项目模型。 - CMake 的操作建立在正确的抽象级别上——它允许你在项目中对文件进行分组,并支持目标的可复用性。
- 有大量的项目是用 CMake 构建的,并提供了一个简单的方法来将它们包含到你的项目中。
- CMake 将测试、打包和安装作为构建过程的固有部分。
- 过时的、没用的特性被弃用,以保持 CMake 的精简。
CMake 为构建提供了一个统一而流畅的体验。不管你是在 IDE 中构建软件,还是直接从命令行构建软件;更重要的是,它也照顾到后期的构建阶段。即使前面的所有环境都不同,你的持续集成/连续部署(CI/CD) 也可以轻松地使用相同的CMake配置,并使用单一标准构建项目。
CMake 是如何起作用的?
你可能会有这样的印象,CMake 是一个一端读取源代码,另一端生成二进制文件的工具——虽然这在原则上是正确的,但并不是全部。
事实上,CMake 并不能自己构建任何东西——它依赖于系统中的其他工具来执行实际的编译、链接和其他任务。你可以将其视为构建过程的协调者:它知道需要完成哪些步骤,最终目标是什么,以及如何为工作找到合适的工人和材料。
这个过程分为三个阶段:
- 配置
- 生成
- 构建
配置阶段
这个阶段 CMake 会从源代码目录(源码树)下读取一些项目相关的信息,并为生成阶段准备一个输出目录(构建树)。
CMake 首先创建一个空的构建树,并收集有关它工作环境的所有信息,例如:计算机体系结构、可用的编译器、链接器和归档器。此外,它检查一个简单的测试程序是否可以被正确编译。
接下来,CMakeLists.txt 项目配置文件被解析并执行(是的,CMake 项目是用 CMake 的编码语言配置的)。这个文件是一个 CMake 项目的最低限度(源代码文件可以稍后添加)。它告诉 CMake 关于项目结构、目标和依赖项(库和其他 CMake 包)的信息。在此过程中,CMake 将收集到的信息存储在构建树中,例如:系统详细信息、项目配置、日志和临时文件,这些将用于下一步。具体来说,它将创建一个 CMakeCache.txt 文件来存储已经确定的变量(例如编译器和其他工具的路径),并在下次配置时节省时间。
生成阶段
读完项目配置之后,CMake 将为当前具体的工作环境生成一个构建系统。构建系统是一个为其他构建工具提供的简要工程模板(例如,GNU 的 Makefile 、Ninja 或者 Visual Studio IDE 的工程文件)。在这个阶段,CMake 仍然可以通过计算生成器表达式对构建配置进行一些最后的修改。
注意:
生成阶段会在配置阶段之后自动执行。由于这个原因,当提到构建系统的“配置”或“生成”时,本书和一些其他资料经常指的是这两个阶段。要想显式地只运行配置阶段,可以使用 cmake-gui 程序。
构建阶段
为了生成项目中指定的最终工件,我们必须运行适当的构建工具。可以通过 IDE 直接调用,或者使用 CMake 命令。接下来,这些构建工具将执行构建步骤,使用编译器、链接器、静态和动态分析工具、测试框架、报告工具和其他任何您能想到的工具来生成目标。
还记得在理解基础部分中我们的 hello.cpp 程序吗?使用 CMake 也很容易构建它。只需要加入以下的 CMakeLists.txt 并敲两条简单的命令:cmake -B buildtree 和 cmake --build buildtree,如下所示:
chapter01/01-hello/CMakeLists.txt:CMake 语言中的 Hello world
cmake_minimum_required(VERSION 3.20)
project(Hello)
add_executable(Hello hello.cpp)
下面是 Dockerized Linux 系统的输出(注意,我们将在为不同平台上安装 CMake 部分讨论 Docker):
root@5f81fe44c9bd:/root/examples/chapter01/01-hello# cmake
-B buildtree.
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /root/examples/
chapter01/01-hello/buildtree
root@5f81fe44c9bd:/root/examples/chapter01/01-hello# cmake
--build buildtree/
Scanning dependencies of target Hello
[ 50%] Building CXX object CMakeFiles/Hello.dir/hello.cpp.o
[100%] Linking CXX executable Hello
[100%] Built target Hello
接下来就是运行:
root@68c249f65ce2:~# ./buildtree/Hello
Hello World!
在这里,我们生成了一个 buildtree 目录下的构建系统。在此之后,我们执行构建阶段,并生成我们最终能够运行的二进制文件。
现在你看到了最终的结果,我敢肯定你一定会有很多问题:这个过程的先决条件是什么?这些命令是什么意思?为什么我们需要两个文件?我如何编写我自己的项目文件?不要担心——这些问题将在下面的部分中得到解答。
获取帮助
这本书将为您提供与当前版本的 CMake 相关的最重要的信息(在撰写本书时是3.20)。为了向你提供最好的建议,我故意避免了任何已被弃用和不再被推荐的特性。我强烈建议至少使用 3.15 版本,它被认为是“现代 CMake”。如需要更多信息,你可以在网上找到最新的、完整的官方文档 https://cmake.org/cmake/help/。
为不同平台安装 CMake
CMake 是一个用 C++ 编写的跨平台开源软件。
这意味着你完全可以自己编译它,然而,通常没必要。因为你完全可以从官网(https://cmake.org/download/)上下载提前编译好的二进制文件。
类 Unix 系统可以直接通过命令行来获取安装包。
注意
牢记 CMake 不会同编译器一起被提供。如果你的系统还未安装编译器,你需要在使用 CMake 之前提供它,并确保将它的可执行文件路径添加到 PATH 环境变量了以使 CMake 能找到它。
学习本书的过程中,为了避免解决工具和依赖问题,我建议选择第一种安装方法:Docker。
让我们来看看可以使用 CMake 的不同环境。
Docker
Docker(https://www.docker.com/)是一个提供操作系统级虚拟化的跨平台工具,允许应用程序以完整的包(称为容器)的形式发布。