第十二章:创建你的专业项目
我们已经掌握了构建专业项目的所有必要知识;我们学习了结构化、构建、依赖管理、测试、分析、安装和打包。是时候将这些学到的技能付诸实践,通过创建一个连贯、专业的项目。
需要理解的重要一点是,即使是简单的程序,也会从自动化质量检查和简化的端到端过程中受益,这些过程将原始代码转化为完全成熟的解决方案。确实,这通常是一个相当大的投资,因为需要采取许多步骤才能一切准备就绪——尤其是如果我们试图将这些机制添加到已经存在的代码库中(通常,它们已经很大且复杂)。
那就是从一开始就使用 CMake 并提前设置所有管道的理由;这样做不仅配置起来会更简单,更重要的是,早点做会更有效率,因为所有的质量控制和构建自动化最终都需要添加到长期项目中。
这正是本章我们将要做的——我们将编写一个新的解决方案,尽可能小和简单。它将执行一个单一的(几乎)实用功能——将两个数字相加。限制业务代码的功能将允许我们关注项目中学到的每个其他方面。
为了有一个更复杂的问题来解决,这个项目将同时构建一个库和一个可执行文件。该库将提供内部业务逻辑,并作为 CMake 包供其他项目使用。可执行文件只为最终用户而设计,并实现一个用户界面,显示底层库的功能。
在本章中,我们将涵盖以下主要主题:
- 规划我们的工作
- 项目布局
- 构建与管理依赖
- 测试与程序分析
- 安装与打包
- 提供文档
技术要求
你可以在这个章节中找到的代码文件在 GitHub 上:
github.com/PacktPublishing/Modern-CMake-for-Cpp/tree/main/examples/chapter12
要构建本书中提供的示例,请始终使用推荐命令:
cmake -B <build tree> -S <source tree> cmake --build <build tree>
请确保将占位符和
替换为适当的路径。作为提醒:build tree 是目标/输出目录的路径,source tree 是源代码所在的位置的路径。
规划我们的工作
本章我们将要构建的软件并不打算非常复杂——我们将创建一个简单的计算器,用于相加两个数字(图 12.1)。它将被发布为一个具有文本用户界面的控制台应用程序和一个执行数学运算的库,这可以潜在地用于另一个项目。虽然在现实生活中这种项目用处不大,因为 C++在其标准库中提供了大量的计算支持,但它的平凡性将完美地探索本书中讨论的所有技术如何在实践中共同工作:
图 12.1 – 控制台计算器用户界面的两种状态
通常,项目要么产生面向用户的可执行文件,要么为开发者提供库。同时做两者的项目比较少见,但并非完全罕见——一些应用程序提供独立的 SDK 或支持插件创建的库。另一种情况可能是提供其使用示例的库。本章我们将要构建的项目在一定程度上适合最后一类。
我们将通过回顾章节列表、回顾其内容,并选择其中描述的技巧和工具来开始规划,以构建我们的计算应用程序:
第1章,CMake 的初步步骤:
第一章为我们提供了关于 CMake 的基本信息——如何安装它以及如何使用其命令行构建准备好的项目。这里提供的关于项目文件的信息将是关键:不同文件的责任、通常使用的名称和一些古怪之处。在本章中,我们还讨论了生成器的预设文件,但在本项目中将跳过这些内容。
第2章,CMake 语言:
在这里,我们介绍了编写正确的列表文件和脚本所需的工具。我们分享了关于代码:注释、命令调用和参数的基本信息。我们还详细解释了变量、列表和控制结构,并介绍了一些非常有用的命令。这些知识将在整个项目中得到应用。
第3章,设置你的第一个 CMake 项目:
第三章讨论的主题将对项目产生重大影响:
- 指定最小的 CMake 版本决定了哪些 CMake 政策将适用;命名、版本化和配置项目的语言影响了构建的基本行为。
- 关于项目划分和结构化的洞察力塑造了目录和文件的结构布局。
- 系统发现变量有助于我们决定如何处理不同的环境,特别是针对本项目——例如,我们需要运行
ldconfig
吗? - 工具链配置允许指定特定版本的 C++和编译器支持的标准。
本章还告诉我们,禁用源代码构建通常是个好主意,所以我们将会这样做。
第四章,使用目标工作:
在这里,我们强调了现代 CMake 项目如何广泛使用目标。我们的项目也将如此,以下原因是:
- 定义几个库和可执行文件(用于测试和生产)将使项目保持组织性并保持干燥.
- 目标属性和传递使用要求(传播属性)使配置接近目标定义。
- 生成器表达式将在解决方案中多次出现,但我们将其尽可能保持简单。
在这个项目中,我们将使用自定义命令生成 Valgrind 和覆盖报告文件,并使用目标挂钩(PRE_BUILD
)来清理由覆盖度 instrumentation 产生的.gcda
文件。
第五章,使用 CMake 编译 C++源代码:
没有编译的 C++项目是不存在的。基础知识相当简单,但 CMake 允许我们在许多方面调整此过程:扩展目标源、配置优化器、提供调试信息。对于这个项目,默认编译标志就足够了,但我们将继续尝试预处理器:
- 我们将在编译的可执行文件中存储构建元数据(项目版本、构建时间和 Git 提交 SHA),并展示给用户。
- 我们将启用头文件的预编译。在一个如此小的项目中,这并不是真正的必需品,但它将帮助我们练习这个概念。
统一构建将不再必要——项目不会足够大,以至于添加它们值得。
第六章,使用 CMake 链接:
第六章为我们提供了关于链接(在任何项目中都有用)的一般信息,其中大部分默认情况下就很有用。但由于该项目还提供了一个库,我们将明确参考一些以下构建说明:
- 用于测试和开发的静态库
- 用于发布的共享库
本章概述了如何为测试分离main()
,我们也将这样做。
第七章,使用 CMake 管理依赖关系:
为了使项目更有趣,我们将引入一个外部依赖项:一个文本 UI 库。我们在这一章描述了几种依赖管理方法。选择正确的一个并不太困难:通常推荐使用FetchContent
实用程序模块,最为方便(除非我们正在解决本章中描述的具体边缘情况)。
第八章,测试框架:
适当的自动化测试对于确保我们解决方案的质量随时间不会降低至关重要。我们将添加对 CTest 的支持,并适当结构我们的项目以进行测试(我们将应用前面提到的main()
分离)。
此外,在本章中,我们讨论了两个测试框架:Catch2 和 GTest 带 gMock;对于这个项目,我们将使用后者。为了获得清晰的覆盖信息,我们将使用 LCOV 生成 HTML 报告。
第九章 程序分析工具:
为了进行静态分析,我们可以选择多种工具:Clang-Tidy,Cpplint,Cppcheck,include-what-you-use,以及 link what you use。在本例中,我们将选择 Cppcheck,因为 Clang-Tidy 与使用 GCC 生成的预编译头配合不佳。动态分析将使用 Valgrind 的 Memcheck 工具完成,我们将使用 Memcheck-cover 包装器生成 HTML 报告。在构建过程中,我们的源代码也将自动使用 ClangFormat 格式化。
第十章 生成文档:
由于我们将提供一个库作为这个项目的一部分,提供至少一些文档是关键的。正如我们所知,CMake 允许我们使用 Doxygen 来自动生成文档。我们将通过添加 doxygen-awesome-css 样式来对其进行刷新设计。
第十一章 安装和打包:
最后,我们将配置解决方案的安装和打包。我们将准备形成包的文件,以及目标定义。我们将使用GNUInstallDirs
模块将该解决方案和构建目标的艺术品安装到适当的目录中。此外,我们将配置一些组件以模块化解决方案,并准备与 CPack 一起使用。
专业项目还包括一些文本文件:README
,LICENSE
,INSTALL
等。我们将在最后简要提及这一点。
注意
为了简化问题,我们不会实现检查所有必需的工具和依赖是否可用的逻辑。我们将依靠 CMake 在这里显示其诊断信息,并告诉用户缺少什么。如果你在阅读这本书后发布的项目获得了显著的牵引力,你可能会想要考虑添加这些机制以改善用户体验。
已经形成了明确的计划,接下来让我们讨论一下如何实际地组织项目,包括逻辑目标和目录结构。
项目布局
为了构建任何项目,我们应该首先清楚理解其中将要创建的逻辑目标。在本例中,我们将遵循图 12.2所示的结构:
图 12.2 – 逻辑目标结构
让我们按照构建顺序来探索结构。首先,我们将编译calc_obj
,这是一个对象库。我们在书中提到了几次对象库,但并没有真正介绍这个概念。现在让我们来做这件事。
对象库
对象库用于将多个源文件组合到一个单一的逻辑目标中,并在构建过程中编译成(.o
)目标文件。要创建一个对象库,我们使用与其它库相同的OBJECT
关键字方法:
add_library(<target> OBJECT <sources>)
在构建过程中生成的目标文件可以通过$
生成器表达式作为编译元素添加到其他目标中:
add_library(... $<TARGET_OBJECTS:objlib> ...) add_executable(... $<TARGET_OBJECTS:objlib> ...)
另外,您可以使用target_link_libraries()
命令将它们作为依赖项添加。
在我们的Calc
库中,对象库将有助于避免为库的静态和共享版本重复编译库源。我们只需要记住显式编译目标文件时使用POSITION_INDEPENDENT_CODE
,因为这是共享库的一个要求。
处理完这些之后,让我们回到这个项目的目标:calc_obj
将提供编译后的目标文件,然后将用于calc_static
和calc_shared
库。它们之间有什么实际的区别,为什么要提供两个库?
共享库与静态库
我们在第六章中简要介绍了这两种库,使用 CMake 链接。我们提到,总体内存使用对于使用相同共享库的多个程序可能更有利,并且用户可能已经拥有最流行的库,或者知道如何快速安装它们。更重要的是,共享库以单独的文件形式提供,必须安装在特定的路径上,动态链接器才能找到它们,而静态库作为可执行文件的一部分合并。在这种形式下,它们的使用速度略快,因为不需要进行额外的查找来找到内存中代码的位置。
作为库作者,我们可以决定是否提供库的静态或共享版本,或者我们可以简单地提供这两个版本,并将此决定留给使用我们库的程序员。我们在这里选择后一种方法(只是为了看看它是如何完成的)。
静态库将由calc_test
目标消耗,其中将包含确保库提供的业务功能按预期工作的单元测试。如前所述,我们从相同的一组编译目标文件构建两个版本。在这种场景下,测试任何一个版本都是完全可以的,因为它们的实际行为应该没有实际的区别。
提供的calc_console_static
目标将使用共享库。此目标还将链接到外部依赖项:函数式终端(X)用户界面(FTXUI)库,由 Arthur Sonzogni 编写(在进一步阅读部分有一个到 GitHub 项目的链接)。它为文本用户界面提供了一个无依赖、跨平台的框架。
最后两个目标是calc_console
和calc_console_test
。calc_console
目标只是一个围绕calc_console_static
的main()
引导包装器。它的唯一目的是从业务代码中提取入口点。这允许我们编写单元测试(需要提供自己的入口点)并从calc_console_test
运行它们。
我们现在知道需要构建哪些目标以及它们之间的关系。让我们弄清楚如何用文件和目录结构化项目。
项目文件结构
项目包含两个主要目标,calc
库和calc_console
可执行文件,每个目标都将在src
和test
下的目录树中,以将生产代码与测试分开(如图 12.3所示)。此外,我们将在另外两个目录中拥有我们的文件:
- 项目根目录,包含顶级配置和关键项目文档文件
- 用于所有实用模块和帮助文件的
cmake
目录,CMake 使用这些文件来构建和安装项目:
图 12.3 – 项目目录结构
以下是每个四个主要目录中的完整文件列表:
最初,cmake
目录比业务代码更繁忙,但随着项目的功能增长,这种情况会很快改变。启动一个干净项目的努力是巨大的,但不用担心——很快就会得到回报。
我们将遍历所有文件,并详细查看它们做什么以及它们在项目中的作用。这将在四个步骤中完成:构建、测试、安装和提供文档。
面向 C++ 的现代 CMake 教程(五)(2)https://developer.aliyun.com/article/1526951