第二章:建立项目

Posted by lili on

没有构建系统,一个项目只是一堆文件的集合。CMake为此带来了一些秩序,从一个名为CMakeLists.txt的人类可读文件开始,该文件定义了应该构建什么以及如何构建,应该运行什么测试以及应该创建哪个(些)软件包。这个文件是整个项目的一个与平台无关的描述,然后CMake将其转换为特定平台的构建工具项目文件。正如其名称所示,它只是一个普通的文本文件,开发者可以在他们喜欢的文本编辑器或开发环境中进行编辑。这个文件的内容将在后续章节中详细讨论,但目前了解它控制了CMake在设置和执行构建时要执行的一切即可。

CMake的一个基本概念是一个项目既有一个源目录又有一个二进制目录。源目录是CMakeLists.txt文件所在的位置,项目的源文件和构建所需的所有其他文件都组织在该位置下。源目录通常在版本控制中,使用像git、subversion或类似的工具。二进制目录是构建产生的所有内容的创建位置。它通常也被称为构建目录。由于在后续章节中将变得清晰的原因,CMake通常使用术语“binary directory”,但在开发者中,“build directory”这个术语更常见。本书倾向于使用后者,因为它通常更直观。CMake、选择的构建工具(例如make、Visual Studio等)、CTest和CPack都将在构建目录及其子目录中创建各种文件。可执行文件、库、测试输出和软件包都在构建目录中创建。CMake还在构建目录中创建一个名为CMakeCache.txt的特殊文件,用于存储在后续运行中重复使用的各种信息。开发者通常不需要关心CMakeCache.txt文件,但后续章节将讨论该文件相关的情况。构建工具的项目文件(例如Xcode或Visual Studio项目文件、Makefiles等)也在构建目录中创建,并且不打算放入版本控制。CMakeLists.txt文件是项目的规范描述,生成的项目文件应被视为构建输出的一部分。

当开发者开始项目工作时,他们必须决定他们希望构建目录与源目录的关系。基本上有两种方法:in-source构建和out-source构建。

In-source构建

虽然可能,但不鼓励源目录和构建目录相同。这种方式被称为in-source构建。在职业生涯初期,开发者通常因为认为它简单而选择使用这种方法。然而,in-source的主要困难在于所有构建输出都与源文件混在一起。这种缺乏分离会导致目录混杂着各种文件和子目录,使得难以管理项目源文件,并存在构建输出覆盖源文件的风险。它还使得与版本控制系统的协作更加困难,因为构建会创建许多文件,源代码控制工具要么必须知道要忽略它们,要么开发者必须在提交时手动排除它们。in-source的另一个缺点是清理所有构建输出并以干净的源代码树重新开始可能不是一件简单的事情。因此,尽量不鼓励开发者使用in-source,即使是对于简单的项目也是如此。

Out-of-source构建

更可取的安排是源目录和构建目录不同,这被称为out-of-source构建。这样可以使源代码和构建输出彼此完全分离,从而避免in-source构建中出现的混杂问题。out-of-source还具有优势,即开发者可以为同一源目录创建多个构建目录,从而可以使用不同的选项集进行构建,例如调试和发布版本等。

本书将始终使用out-of-source,并遵循源目录和构建目录位于共同父目录下的模式。构建目录将被命名为build,或类似的变体。例如:

其中一种变体由一些开发者采用,即将构建目录设置为源目录的子目录。这提供了大部分外源构建的优势,但仍带有in-source方式的一些劣势。除非有很好的理由以这种方式组织事务,推荐将构建目录完全放在源代码树之外。

注:因为我们通常喜欢在顶层项目运行命令,比如查看编辑文件,因此这种变体非常流行。比如如下常见的操作:

git clone http://abc.git
cd abc
mkdir build
cd build
cmake ..

我个人觉得这种方法问题也不大,只需要记得把build目录加入.gitignore就行。

生成的项目文件

一旦选择了目录结构,开发者运行CMake,该工具读取CMakeLists.txt文件并在构建目录中创建项目文件。开发者通过选择特定的项目文件生成器来确定要创建的项目文件类型。支持多种不同的生成器,下表列出了较常用的一些。

一些生成器生成支持多个配置(例如Debug、Release等)的项目。这使得开发者可以在不重新运行CMake的情况下选择不同的构建配置,这对于生成用于Xcode和Visual Studio等IDE环境的项目的生成器更为适用。对于不支持多个配置的生成器,开发者必须重新运行CMake以在Debug、Release等之间切换构建。这些生成器更简单,通常在与特定编译器不太相关的IDE环境中有很好的支持(如CLion、Qt Creator、KDevelop等)。

运行CMake的最基本方式是通过cmake命令行工具。调用它的最简单方式是切换到构建目录,并向cmake传递选项,指定生成器类型和源树的位置。例如:

mkdir build
cd build
cmake -G "Unix Makefiles" ../source

如果省略了 -G 选项,CMake将根据主机平台选择默认的生成器类型。如果使用的是CMake 3.15或更高版本,则可以通过将CMAKE_GENERATOR环境变量设置为所需的默认值来覆盖此默认值。对于所有生成器类型,CMake将执行一系列测试来确定如何设置项目文件。这包括验证编译器是否正常工作、确定支持的编译器特性集以及其他各种任务。在成功完成时,CMake将记录各种信息,最终以以下形式的行来完成:

-- Configuring done
-- Generating done
-- Build files have been written to: /some/path/build

上述说明了项目文件创建实际上涉及两个步骤:配置和生成。在配置阶段,CMake读取CMakeLists.txt文件并建立整个项目的内部表示。完成这一步后,生成阶段创建项目文件。对于基本的CMake使用,配置和生成之间的区别并不太重要,但在后续章节中,配置和生成的分离变得重要。这将在第10章“生成器表达式”中进行更详细的讨论。

当CMake完成运行时,它将在构建目录中保存一个CMakeCache.txt文件。CMake使用这个文件保存详细信息,以便如果再次运行,它可以重用第一次计算的信息,加快项目生成的速度。正如后续章节中所介绍的,它还允许在运行之间保存开发者选项。除了运行cmake命令行工具之外,还提供了一个GUI应用程序cmake-gui,但是GUI应用程序的介绍推迟到第5章“变量”中,那里其实用性更为明显。

运行构建工具

此时,现在有了项目文件,开发者可以按照他们习惯的方式使用所选的构建工具。构建目录将包含必要的项目文件,可以加载到集成开发环境(IDE)中,也可以被命令行工具读取等。或者,cmake可以代表开发者调用构建工具,如下所示:

cmake --build /pathTo/build --config Debug --target MyApp

这适用于开发者可能更习惯通过Xcode或Visual Studio等IDE使用的项目类型。–build选项指向CMake项目生成步骤使用的构建目录。对于多配置生成器,–config选项指定要构建的配置,而对于单配置生成器,将忽略–config选项,而是依赖于在执行CMake项目生成步骤时提供的信息。构建配置的详细指定在第14章“构建类型”中进行。–target选项可用于告诉构建工具要构建什么,如果省略,则将构建默认目标。使用CMake 3.15或更高版本,可以在–target选项后列出多个目标,用空格分隔。虽然开发者通常会在日常开发中直接调用所选的构建工具,但通过上述方式通过cmake命令调用它在驱动自动化构建的脚本中可能更为有用。使用这种方法,一个简单的脚本化构建可能看起来像这样:

mkdir build
cd build
cmake -G "Unix Makefiles" ../source
cmake --build . --config Release --target MyApp

如果开发者希望尝试不同的生成器,所需做的就是更改参数-G,正确的构建工具将自动调用。对于cmake –build来说,构建工具甚至不必在用户的PATH上(尽管在首次调用cmake时可能需要,因为需要进行初始配置步骤)。

推荐做法

即使在初次使用CMake时,建议养成将构建目录与源代码树完全分开的习惯。获得这种习惯的好处的早期体验的一个好方法是为同一源目录设置两个或更多不同的构建。一个构建可以配置为使用Debug设置,另一个用于Release构建。另一个选择是为不同的构建目录使用不同的项目生成器,比如Unix Makefiles和Xcode。这有助于捕捉对特定构建工具的任何意外依赖,或者检查不同生成器类型之间的编译器设置是否有差异。

在项目的早期阶段,特别是如果开发者不习惯编写跨平台软件,很容易专注于使用一种特定类型的项目生成器。然而,项目往往会超出其最初的范围,并且通常需要支持额外的平台和生成器类型。定期使用与开发者通常使用的生成器不同的项目生成器检查构建,可以通过在不需要的地方阻止特定于生成器的代码,从而在将来节省大量的麻烦。这也有助于使项目能够充分利用将来的任何新生成器类型。一个好的策略是确保项目使用每个感兴趣的平台上的默认生成器类型构建,再加上另一种类型。Ninja生成器是后者的一个很好的选择,因为它具有所有生成器中最广泛的平台支持,并且还可以创建非常高效的构建。如果项目正在被脚本化,通过cmake –build调用构建工具,而不是直接调用构建工具。这使得脚本可以轻松地在不修改的情况下在不同的生成器类型之间切换。