面向 C++ 的现代 CMake 教程(二)(1)https://developer.aliyun.com/article/1525488
宿主系统信息
CMake 可以提供更多的变量,但为了节省时间,它不查询环境中的罕见信息,例如处理器是否支持 MMX或总物理内存是多少。这并不意味着这些信息不可用——你只需要通过以下命令显式地请求它:
cmake_host_system_information(RESULT QUERY …)
我们需要提供一个目标变量和我们要关注的键列表。如果我们只提供一个键,变量将包含一个值;否则,它将是一组值。我们可以询问许多关于环境和操作系统的详细信息:
如果需要,我们甚至可以查询处理器特定信息:
平台是否有 32 位或 64 位架构?
在 64 位架构中,内存地址、处理器寄存器、处理器指令、地址总线和数据总线都是 64 位的。虽然这是一个简化的定义,但它给出了 64 位平台与 32 位平台有何不同的粗略概念。
在 C++中,不同的架构意味着一些基本数据类型(int
和long
)和指针有不同的位宽。CMake 利用指针大小来收集目标机器的信息。通过CMAKE_SIZEOF_VOID_P
变量可获得此信息,对于 64 位该值为8
(因为指针是 8 字节宽)和对于 32 位该值为4
(4 字节):
if(CMAKE_SIZEOF_VOID_P EQUAL 8) message(STATUS "Target is 64 bits") endif()
系统的字节序是什么?
架构可以是大端或小端。字节序是数据字中的字节顺序或处理器的自然数据单位。一个大端系统将最高有效字节存储在最低的内存地址,最低有效字节存储在最高的内存地址。一个小端系统与此相反。
在大多数情况下,字节顺序不影响,但当你编写需要可移植的位操作代码时,CMake 将提供 BIG_ENDIAN
或 LITTLE_ENDIAN
值,存储在 CMAKE__BYTE_ORDER
变量中,其中 是
C
、CXX
、OBJC
或 CUDA
。
现在我们已经知道如何查询环境,让我们将重点转移到项目的主要设置上。
配置工具链
对于 CMake 项目,工具链包括构建和运行应用程序的所有工具——例如,工作环境、生成器、CMake 执行文件本身以及编译器。
想象一下一个经验较少的使用者在构建过程中遇到一些神秘的编译和语法错误时会感到怎样。他们不得不深入源代码试图了解发生了什么。经过一个小时的调试后,他们发现正确的解决方案是更新他们的编译器。我们能否为用户提供更好的体验,并在开始构建前检查编译器中是否包含了所有必需的功能?
当然!有方法可以指定这些要求。如果工具链不支持所有必需的功能,CMake 将提前停止并显示发生了什么清晰的消息,要求用户介入。
设置 C++ 标准
我们可能首先想要做的是设置编译器需要支持的 C++ 标准,如果用户想要构建我们的项目的话。对于新项目,这至少应该是 C++14,但最好是 C++17 或 C++20。CMake 还支持将标准设置为实验性的 C++23,但那只是一个草案版本。
注意
自 C++11 正式发布以来已经过去了 10 年,它不再被认为是现代 C++ 标准。除非你的目标环境非常老旧,否则不建议用这个版本开始项目。
坚持旧标准的原因之一是因为你在构建太难升级的遗留目标。然而,C++ 委员会非常努力地保持 C++ 的向后兼容性,在大多数情况下,你将不会有任何问题将标准提升到更高版本。
CMake 支持基于每个目标单独设置标准,这意味着你可以拥有任何粒度。我相信最好让整个项目统一到一个标准上。这可以通过将 CMAKE_CXX_STANDARD
变量设置为以下值之一来实现:98
、11
、14
、17
、20
或 23
(自 CMake 3.20 起)。这将作为所有随后定义的目标的默认值(所以最好在根列表文件的顶部附近设置它)。如果需要,你可以按每个目标单独覆盖它,像这样:
set_property(TARGET <target> PROPERTY CXX_STANDARD <standard>)
坚持标准支持
上文提到的CXX_STANDARD
属性即使编译器不支持期望的版本——它也被视为一个偏好。CMake 不知道我们的代码实际上是否使用了在之前的编译器中不可用的全新特性,并且它会尝试利用可用的所有内容。
如果我们确信这将不会成功,我们可以设置另一个默认标志(它可以通过与前一个相同的方式覆盖)并明确要求我们目标的标准:
set(CMAKE_CXX_STANDARD_REQUIRED ON)
在这种情况下,如果最新的编译器不在系统当中(在这个例子中,GNU GCC 11
),用户将只看到以下消息,并且构建将停止:
Target "Standard" requires the language dialect "CXX23" (with compiler extensions), but CMake does not know the compile flags to use to enable it.
要求 C++23 可能有点过分,即使在一个现代环境中。但 C++14 应该完全没问题,因为它自 2015 年以来已经在GCC/Clang中得到全面支持。
供应商特定的扩展
根据你在组织中实施的政策,你可能对允许或禁用供应商特定的扩展感兴趣。这些是什么?嗯,我们可以说 C++标准对于一些编译器生产商来说进展得太慢,所以他们决定向语言添加他们自己的增强——如果你喜欢的话,就是插件。为了实现这一点,CMake 将把-std=gnu++14
添加到编译命令行中,而不是-std=c++14
。
一方面,这可能是想要的,因为它允许一些方便的功能。但另一方面,如果你的代码切换到不同的编译器(或者你的用户这样做!)构建将失败!
这也是一个针对每个目标的属性,其有一个默认变量,CMAKE_CXX_EXTENSIONS
。CMake 在这里更加宽松,除非我们明确告诉它不要这样做,否则允许扩展:
set(CMAKE_CXX_EXTENSIONS OFF)
如果可能的话,我建议这样做,因为此选项将坚持拥有与供应商无关的代码。此类代码不会对用户施加任何不必要的要求。类似地,你可以使用set_property()
按每个目标的基础更改此值。
跨过程优化
通常,编译器在单个翻译单元的层面上优化代码,这意味着你的.cpp
文件将被预处理、编译,然后优化。后来,这些文件将被链接器用来构建单一的二进制文件。现代编译器可以在链接后进行优化(这称为链接时优化),以便所有编译单元可以作为一个单一模块进行优化。
如果你的编译器支持跨过程优化,使用它可能是个好主意。我们将采用与之前相同的方法。此设置的默认变量称为CMAKE_INTERPROCEDURAL_OPTIMIZATION
。但在设置之前,我们需要确保它被支持以避免错误:
include(CheckIPOSupported) check_ipo_supported(RESULT ipo_supported) if(ipo_supported) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION True) endif()
正如你所见,我们不得不包含一个内置模块来获取check_ipo_supported()
命令的访问权限。
检查支持的编译器功能
如我们之前讨论的,如果我们的构建失败,最好是早点失败,这样我们就可以向用户提供一个清晰的反馈信息。我们特别感兴趣的是衡量哪些 C++特性被支持(以及哪些不被支持)。CMake 将在配置阶段询问编译器,并将可用特性的列表存储在CMAKE_CXX_COMPILE_FEATURES
变量中。我们可以编写一个非常具体的检查,询问某个特性是否可用:
chapter03/07-features/CMakeLists.txt
list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_variable_templates result) if(result EQUAL -1) message(FATAL_ERROR "I really need variable templates.") endif()
正如您可能猜到的,为每个使用特性编写一个测试文件是一项艰巨的任务。即使是 CMake 的作者也建议只检查某些高级元特性是否存在:cxx_std_98
、cxx_std_11
、cxx_std_14
、cxx_std_17
、cxx_std_20
和cxx_std_23
。每个元特性都表明编译器支持特定的 C++标准。如果您愿意,您可以像前一个示例中那样使用它们。
已知于 CMake 的所有特性的完整列表可以在文档中找到:
cmake.org/cmake/help/latest/prop_gbl/CMAKE_CXX_KNOWN_FEATURES.html
编译测试文件
当我用 GCC 4.7.x 编译一个应用程序时,有一个特别有趣的场景出现在我面前。我已手动在编译器的参考资料中确认了我们使用的所有 C++11 特性都被支持。然而,解决方案仍然无法正确工作。代码默默地忽略了标准头文件的调用。结果证明,GCC 4.7.x 有一个 bug,正则表达式库没有被实现。
没有一个单一的检查能保护你免受此类 bug 的影响,但通过创建一个测试文件,你可以填入所有你想检查的特性,从而有机会减少这种行为。CMake 提供了两个配置时间命令,try_compile()
和try_run()
,以验证您需要的所有内容在目标平台上是否支持。
第二个命令给您更多的自由,因为您可以确保代码不仅编译成功,而且执行也正确(您可以潜在地测试regex
是否工作)。当然,这对于交叉编译场景不起作用(因为主机无法运行为不同目标构建的可执行文件)。只需记住,这个检查的目的是在编译成功时向用户提供一个快速的反馈,所以它并不是用来运行任何单元测试或其他复杂内容的——尽量保持文件尽可能简单。例如,像这样:
chapter03/08-test_run/main.cpp
#include <iostream> int main() { std::cout << "Quick check if things work." << std::endl; }
调用test_run()
其实并不复杂。我们首先设置所需的标准,然后调用test_run()
,并将收集的信息打印给用户:
chapter03/08-test_run/CMakeLists.txt
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) try_run(run_result compile_result ${CMAKE_BINARY_DIR}/test_output ${CMAKE_SOURCE_DIR}/main.cpp RUN_OUTPUT_VARIABLE output) message("run_result: ${run_result}") message("compile_result: ${compile_result}") message("output:\n" ${output})
这个命令有很多可选字段需要设置,一开始可能会觉得有点复杂,但当我们阅读并与示例中的调用进行比较时,一切都会变得明朗起来:
try_run(<runResultVar> <compileResultVar> <bindir> <srcfile> [CMAKE_FLAGS <flags>...] [COMPILE_DEFINITIONS <defs>...] [LINK_OPTIONS <options>...] [LINK_LIBRARIES <libs>...] [COMPILE_OUTPUT_VARIABLE <var>] [RUN_OUTPUT_VARIABLE <var>] [OUTPUT_VARIABLE <var>] [WORKING_DIRECTORY <var>] [ARGS <args>...])
只有几个字段是编译和运行一个非常基础的测试文件所必需的。我还使用了可选的RUN_OUTPUT_VARIABLE
关键字来收集stdout
的输出。
下一步是使用我们在实际项目中将要使用的更现代的 C++特性来扩展这个简单的文件——也许通过添加一个可变模板来看目标机器上的编译器是否能消化它。每次我们在实际项目中引入一个新特性,我们可以在测试文件中放入这个特性的微小样本。但请记住——保持简洁。我们希望在最短的时间内检查编译是否成功。
最后,我们可以在条件块中检查收集的输出是否符合我们的期望,当有些不对劲时会打印message(SEND_ERROR)
。记住SEND_ERROR
会在配置阶段继续,但不会启动生成。这有助于在放弃构建之前显示所有遇到的错误。
禁用源内构建
在第1章,《CMake 的初步步骤》中,我们讨论了源内构建,以及建议始终指定为源外构建路径。这不仅允许更干净的构建树和更简单的.gitignore
文件,而且还减少了你意外覆盖或删除任何源文件的可能性。
在网上搜索解决方案时,你可能会偶然发现一个 StackOverflow 帖子,提出了同样的问题:stackoverflow.com/q/1208681/6659218
。在这里,作者注意到不管你做什么,似乎 CMake 仍然会创建一个CMakeFiles/
目录和一个CMakeCache.txt
文件。一些答案建议使用未记录的变量,以确保用户在任何情况下都不能在源目录中写入:
# add this options before PROJECT keyword set(CMAKE_DISABLE_SOURCE_CHANGES ON) set(CMAKE_DISABLE_IN_SOURCE_BUILD ON)
我认为在使用任何软件的未记录功能时要小心,因为它们可能会在没有警告的情况下消失。在 CMake 3.20 中设置前面的变量会导致构建失败,并出现相当丑陋的错误:
CMake Error at /opt/cmake/share/cmake-3.20/Modules/CMakeDetermineSystem.cmake:203 (file): file attempted to write a file: /root/examples/chapter03/09-in-source/CMakeFiles/CMakeOutput.log into a source directory.
然而,它仍然创建了提到的文件!因此,我的建议是使用更旧的——但完全支持——机制:
chapter03/09-in-source/CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0) project(NoInSource CXX) if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) message(FATAL_ERROR "In-source builds are not allowed") endif() message("Build successful!")
如果 Kitware(CMake 背后的公司)正式支持CMAKE_DISABLE_SOURCE_CHANGES
或CMAKE_DISABLE_IN_SOURCE_BUILD
,那么当然,切换到那个解决方案。
总结
我们在本章引入了许多有价值的概念,它们将为我们打下坚实的基础,以便我们向前发展并构建坚固、未来 proof 的项目。我们讨论了如何设置最低的 CMake 版本以及如何配置项目的关键方面,即名称、语言和元数据字段。
打下良好的基础将有助于确保我们的项目能够快速成长。这就是我们讨论项目分区的理由。我们分析了使用include()
的简单代码分区,并将其与add_subdirectory()
进行了比较。在此过程中,我们了解了管理变量目录作用域的好处,并探讨了使用更简单的路径和增加的模块性的好处。当需要逐步将代码分解为更独立的单元时,创建嵌套项目和分别构建它是非常有用的。
在概览了我们可用的分区机制之后,我们探讨了如何使用它们——例如,如何创建透明、有弹性且可扩展的项目结构。具体来说,我们分析了 CMake 如何遍历列表文件以及不同配置步骤的正确顺序。
接下来,我们研究了如何作用域化我们目标和宿主机器的环境,它们之间的区别是什么,以及通过不同的查询可以获取关于平台和系统的哪些信息。
最后,我们发现了如何配置工具链——例如,如何指定所需的 C++版本,如何解决特定编译器扩展的问题,以及如何启用重要的优化。我们最后了解了如何测试我们的编译器所需的特性以及编译测试文件。
虽然从技术上讲,项目所有这些就足够了,但它仍然不是一个非常有用的项目。为了改变这一点,我们需要引入目标。到目前为止,我们在这里那里提到了它们,但我试图在我们先了解一些基本概念之前避免这个话题。现在我们已经做到了,我们将详细查看它们。
进一步阅读
关于本章涵盖的议题的更多信息,你可以参考以下内容:
- 关注点分离:
nalexn.github.io/separation-of-concerns/
- CMake 变量完整参考:
cmake.org/cmake/help/latest/manual/cmake-variables.7.html
- 尝试编译和尝试运行的参考:
cmake.org/cmake/help/latest/command/try_compile.html
cmake.org/cmake/help/latest/command/try_run.html
第二部分:使用 CMake 构建
现在我们已经掌握了最基本的技能,是时候更深入一点学习了。下一部分将使你能够解决在使用 CMake 构建项目时遇到的大多数情况。
我们故意关注现代、优雅的实践,避免引入太多的遗留问题。具体来说,我们将处理逻辑构建目标,而不是操纵单个文件。
接下来,我们将详细解释工具链执行的二进制工件构建步骤。这是许多关于 C++的书籍所缺少的部分:如何配置和使用预处理器、编译器和链接器,以及如何优化它们的行为。
最后,本部分将涵盖 CMake 提供管理依赖关系的所有不同方式,并解释如何为您的特定用例选择最佳方法。
本部分包括以下章节:
- 第四章,与目标一起工作
- 第五章,使用 CMake 编译 C++源代码
- 第六章,用 CMake 进行链接
- 第七章,用 CMake 管理依赖关系
第四章:使用目标
在 CMake 中,我们可以构建的最基本目标是一个单一的二进制可执行文件,它包含了一个完整的应用程序。它可以由单一片源代码组成,如经典的helloworld.cpp
。或者它可以更复杂——由数百个甚至数千个文件构建而成。这就是许多初学者项目的外观——用一个源文件创建一个二进制文件,再添加另一个,在不知不觉中,一切都被链接到一个没有结构可言的二进制文件中。
作为软件开发者,我们故意划设定界线,并将组件指定为将一个或多个翻译单元(.cpp
文件)分组在一起。我们这样做有多个原因:增加代码的可读性,管理耦合和 connascence,加快构建过程,最后,提取可重用的组件。
每一个足够大的项目都会推动你引入某种形式的分区。CMake 中的目标正是为了解决这个问题——一个高级逻辑单元,为 CMake 形成一个单一目标。一个目标可能依赖于其他目标,它们以声明性方式生成。CMake 将负责确定目标需要以什么顺序构建,然后逐个执行必要的步骤。作为一个一般规则,构建一个目标将生成一个 artifact,这个 artifact 将被输送到其他目标中,或作为构建的最终产品交付。
我故意使用不确切的词汇artifact,因为 CMake 并没有限制你只能生成可执行文件或库。实际上,我们可以使用生成的构建系统来创建许多类型的输出:更多的源文件、头文件、对象文件、归档文件和配置文件——任何真正需要的。我们需要的只是一个命令行工具(如编译器)、可选的输入文件和一个输出路径。
目标是一个非常强大的概念,极大地简化了项目的构建。理解它们如何工作以及如何以最优雅、最清洁的方式配置它们是关键。
在本章中,我们将涵盖以下主要主题:
- 目标概念
- 编写自定义命令
- 理解生成器表达式
技术要求
您可以在 GitHub 上找到本章中存在的代码文件,地址为github.com/PacktPublishing/Modern-CMake-for-Cpp/tree/main/examples/chapter04
。
要构建本书中提供的示例,始终使用推荐的命令:
cmake -B <build tree> -S <source tree> cmake --build <build tree>
请确保将占位符和
替换为适当的路径。作为提醒:build tree是目标/输出目录的路径,source tree是源代码所在的位置路径。
目标概念
如果你曾经使用过 GNU Make,你已经看到了目标的概念。本质上,它是一个构建系统用来将一组文件编译成另一个文件的食谱。它可以是一个.cpp
实现文件编译成一个.o
对象文件,一组.o
文件打包成一个.a
静态库,还有许多其他组合。
CMake 允许你节省时间并跳过那些食谱的中间步骤;它在更高的抽象级别上工作。它理解如何直接从源文件构建可执行文件。所以,你不需要编写显式的食谱来编译任何对象文件。所需的就是一个add_executable()
命令,带有可执行目标的名字和要作为其元素的文件列表:
add_executable(app1 a.cpp b.cpp c.cpp)
我们在之前的章节中已经使用了这个命令,并且我们已经知道如何在实践中使用可执行目标——在生成步骤中,CMake 将创建一个构建系统并为其填充编译每个源文件并将它们链接在一起成一个单一的二进制可执行文件的食谱。
在 CMake 中,我们可以使用三个命令之一创建一个目标:
add_executable()
add_library()
add_custom_target()
前两个相对容易解释;我们已经在之前的章节中简要使用过它们来构建可执行文件和库(我们将在第五章深入讨论它们,使用 CMake 编译 C++源代码)。但那些自定义目标是什么?
它们允许你指定自己的命令行,该命令行将被执行而不检查产生的输出是否是最新的,例如:
- 计算其他二进制文件的校验和。
- 运行代码净化器并收集结果。
- 将编译报告发送到数据处理管道。
以下是add_custom_target()
命令的完整签名:
add_custom_target(Name [ALL] [command1 [args1...]] [COMMAND command2 [args2...] ...] [DEPENDS depend depend depend ... ] [BYPRODUCTS [files...]] [WORKING_DIRECTORY dir] [COMMENT comment] [JOB_POOL job_pool] [VERBATIM] [USES_TERMINAL] [COMMAND_EXPAND_LISTS] [SOURCES src1 [src2...]])
我们不会在这里讨论每一个选项,因为我们想快速继续其他目标,但可以说自定义目标不必一定以文件形式产生有形工件。
自定义目标的一个好用例可能是需要在每次构建时删除特定文件的需求——例如,确保代码覆盖报告不包含过时数据。我们只需要像这样定义一个自定义目标:
add_custom_target(clean_stale_coverage_files COMMAND find . -name "*.gcda" -type f -delete)
之前的命令将搜索所有具有.gcda
扩展名的文件并将它们删除。但是有一个问题;与可执行文件和库目标不同,自定义目标只有在它们被添加到依赖图时才会构建。我们来了解一下那是怎么回事。
依赖图
成熟的应用程序通常由许多组件组成,我这里不是指外部依赖。具体来说,我指的是内部库。从结构上讲,将它们添加到项目中是有用的,因为相关的事物被包装在单一的逻辑实体中。并且它们可以与其他目标链接——另一个库或一个可执行文件。当多个目标使用同一个库时,这尤其方便。看看图 4.1,它描述了一个示例依赖关系图:
图 4.1 – BankApp 项目中依赖关系的构建顺序
在这个项目中,我们有两个库,两个可执行文件和一个自定义目标。我们的用例是提供一个带有漂亮 GUI 的用户银行应用程序(GuiApp),以及一个作为自动化脚本一部分的命令行版本(TerminalApp)。两个可执行文件都依赖于同一个Calculations库,但只有其中一个需要Drawing库。为了确保我们的应用程序在用户从互联网下载时没有被修改,我们将计算一个校验和,将其存储在文件中,并通过单独的安全渠道分发它。CMake 在编写此类解决方案的列表文件方面相当灵活:
chapter04/01-targets/CMakeLists.txt
cmake_minimum_required(VERSION 3.19.2) project(BankApp CXX) add_executable(terminal_app terminal_app.cpp) add_executable(gui_app gui_app.cpp) target_link_libraries(terminal_app calculations) target_link_libraries(gui_app calculations drawing) add_library(calculations calculations.cpp) add_library(drawing drawing.cpp) add_custom_target(checksum ALL COMMAND sh -c "cksum terminal_app>terminal.ck" COMMAND sh -c "cksum gui_app>gui.ck" BYPRODUCTS terminal.ck gui.ck COMMENT "Checking the sums..." )
我们使用target_link_libraries()
命令将库和可执行文件连接起来。没有它,可执行文件的编译会失败,因为存在未定义的符号。你注意到我们在这个命令在实际上声明了任何库之前就调用了吗?当 CMake 配置项目时,它会收集有关目标和它们属性的信息——它们的名称、依赖关系、源文件和其他详细信息。
在解析完所有文件后,CMake 将尝试构建一个依赖关系图。和所有有效的依赖关系图一样,它们都是有向无环图。这意味着有一个明确的方向,即哪个目标依赖于哪个目标,并且这样的依赖关系不能形成循环。
当我们以构建模式执行cmake
时,生成的构建系统将检查我们定义了哪些顶层目标,并递归地构建它们的依赖关系。让我们考虑一下来自图 4.1的例子:
- 从顶部开始,为组 1 构建两个库。
- 当Calculations和Drawing库完成后,构建组 2——GuiApp和TerminalApp。
- 构建一个校验和目标;运行指定的命令行生成校验和(
cksum
是一个 Unix 校验和工具)。
不过有一个小问题——前面的解决方案并不能保证校验和目标在可执行文件之后构建。CMake 不知道校验和依赖于可执行二进制文件的存在,所以它可以先开始构建它。为了解决这个问题,我们可以把add_dependencies()
命令放在文件的末尾:
add_dependencies(checksum terminal_app gui_app)
这将确保 CMake 理解 Checksum 目标与可执行文件之间的关系。
很好,但target_link_libraries()
和add_dependencies()
之间有什么区别?第一个是用来与实际库一起使用,并允许你控制属性传播。第二个仅适用于顶级目标以设置它们的构建顺序。
随着项目复杂性的增加,依赖树变得越来越难以理解。我们如何简化这个过程?
可视化依赖关系
即使小型项目也难以推理和与其他开发人员共享。最简单的方法之一是通过一个好的图表。毕竟,一张图片胜过千言万语。我们可以自己动手绘制图表,就像我在图 4.1中做的那样。但这很繁琐,并且需要不断更新。幸运的是,CMake 有一个很好的模块,可以在dot/graphviz
格式中生成依赖图。而且它支持内部和外部依赖!
要使用它,我们可以简单地执行这个命令:
cmake --graphviz=test.dot .
该模块将生成一个文本文件,我们可以将其导入到 Graphviz 可视化软件中,该软件可以渲染图像或生成 PDF 或 SVG 文件,作为软件文档的一部分。每个人都喜欢伟大的文档,但几乎没有人喜欢创建它——现在,你不需要!
如果你急于求成,甚至可以直接从你的浏览器中运行 Graphviz,地址如下:
dreampuf.github.io/GraphvizOnline/
重要说明
自定义目标默认是不可见的,我们需要创建一个特殊的配置文件CMakeGraphVizOptions.cmake
,它将允许我们自定义图表。一个方便的自定义命令是set(GRAPHVIZ_CUSTOM_TARGETS TRUE)
;将其添加到特殊配置文件中以在您的图表中启用报告自定义目标。您可以在模块的文档中找到更多选项。
你只需要将test.dot
文件的内容复制并粘贴到左侧窗口中,你的项目就会被可视化。非常方便,不是吗?
图 4.2 —— 使用 Graphviz 可视化的 BankApp 示例
为了清晰起见,我已经从前面的图中移除了自动生成的图例部分。
使用这种方法,我们可以快速查看所有明确定义的目标。现在我们有了这个全局视角,让我们深入了解一下如何配置它们。
目标属性
目标具有类似于 C++对象字段的工作方式属性。我们可以修改其中的一些属性,而其他属性是只读的。CMake 定义了一个大量的“已知属性”(参见进一步阅读部分),这些属性取决于目标类型(可执行文件、库或自定义)。如果你愿意,你也可以添加你自己的属性。使用以下命令来操作目标属性:
get_target_property(<var> <target> <property-name>) set_target_properties(<target1> <target2> ... PROPERTIES <prop1-name> <value1> <prop2-name> <value2> ...)
为了在屏幕上打印目标属性,我们首先需要将其存储在变量中,然后将其传递给用户;我们必须一个一个地读取它们。另一方面,为目标设置属性允许我们同时指定多个属性,在多个目标上。
属性概念不仅限于目标;CMake 也支持设置其他范围属性的:GLOBAL
、DIRECTORY
、SOURCE
、INSTALL
、TEST
和CACHE
。为了操作各种各样的属性,有通用的get_property()
和set_property()
命令。你可以使用这些底层命令来做与set_target_properties()
命令完全相同的事情,只是需要更多的工作:
set_property(TARGET <target> PROPERTY <name> <value>)
通常,尽可能使用许多高级命令是更好的。CMake 提供更多这些,甚至范围更窄,例如为目标设置特定属性。例如,add_dependencies( )
是在MANUALLY_ADDED_DEPENDENCIES
目标属性上添加依赖项。在这种情况下,我们可以用get_target_property()
查询它,就像查询任何其他属性一样。然而,我们不能用set_target_property()
来更改它(它是只读的),因为 CMake 坚持使用add_dependencies()
命令来限制操作只是添加。
在接下来的章节中讨论编译和链接时,我们将介绍更多的属性设置命令。同时,让我们关注一个目标的属性如何传递到另一个目标。
面向 C++ 的现代 CMake 教程(二)(3)https://developer.aliyun.com/article/1525490