面向 C++ 的现代 CMake 教程(五)(1)https://developer.aliyun.com/article/1526950
构建和管理依赖项
所有的构建过程都是相同的。我们从顶层列表文件开始,向下导航到项目源树。图 12.4 显示了哪些项目文件参与构建。圆括号中的数字表示 CMake 脚本执行的顺序:
图 12.4 – 构建阶段使用的文件
我们的顶层列表文件将配置项目并加载嵌套元素:
chapter-12/01-full-project/CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0) project(Calc VERSION 1.0.0 LANGUAGES CXX) list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") include(NoInSourceBuilds) add_subdirectory(src bin) add_subdirectory(test) include(Install)
我们首先提供关键项目详情,并添加到 CMake 实用模块的路径(我们项目中的cmake
目录)。然后我们禁用源内构建(通过自定义模块),并包括两个关键目录:
src
,包含项目源代码(在构建树中将被命名为bin
)test
,包含所有的测试工具
最后,我们包含另一个模块,将设置项目的安装。这将在另一节中讨论。与此同时,让我们看看NoInSourceBuilds
模块是如何工作的:
chapter-12/01-full-project/cmake/NoInSourceBuilds.cmake
if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) message(FATAL_ERROR "\n" "In-source builds are not allowed.\n" "Instead, provide a path to build tree like so:\n" "cmake -B <destination>\n" "\n" "To remove files you accidentally created execute:\n" "rm -rf CMakeFiles CMakeCache.txt\n" ) endif()
这里没有惊喜——我们只是检查用户是否为cmake
命令提供了目标目录作为参数来存储生成的文件。这必须与项目源树不同的路径。如果不是这样,我们告知用户如何提供它,以及如何在犯错误后清理仓库。
我们的顶级列表文件然后包含了src
子目录,指示 CMake 读取其中的列表文件:
chapter-12/01-full-project/src/CMakeLists.txt
add_subdirectory(calc) add_subdirectory(calc_console)
这个文件非常微妙——它只是进入嵌套目录,执行其中的列表文件。让我们跟随calc
库的列表文件——它有点复杂,所以我们将分部分讨论。
构建 Calc 库
calc
的列表文件包含一些测试配置,但我们现在将关注构建;其余内容将在测试和程序分析部分讨论:
chapter-12/01-full-project/src/calc/CMakeLists.txt(片段)
add_library(calc_obj OBJECT calc.cpp) target_include_directories(calc_obj INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>" "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>" ) set_target_properties(calc_obj PROPERTIES PUBLIC_HEADER src/calc/include/calc/calc.h POSITION_INDEPENDENT_CODE 1 ) add_library(calc_shared SHARED) target_link_libraries(calc_shared calc_obj) add_library(calc_static STATIC) target_link_libraries(calc_static calc_obj) # ... testing and program analysis modules # ... documentation generation
我们声明了三个目标:
calc_obj
,一个对象库,编译一个calc.cpp
实现文件。它还通过PUBLIC_HEADER
属性引用calc.h
头文件,该属性可以在配置的包含目录中找到(感谢生成器表达式提供特定模式的适当路径——构建或安装)。使用这个库,我们避免了其他目标的重复编译,但我们也需要启用POSITION_INDEPENDENT_CODE
,以便生成的对象文件可以被共享库使用。calc_shared
,一个依赖于calc_obj
的共享库。calc_static
,一个依赖于calc_obj
的静态库。
为了完整性,我们将添加calc
库的 C++ 代码清单:
chapter-12/01-full-project/src/calc/include/calc/calc.h
#pragma once namespace Calc { int Sum(int a, int b); int Multiply(int a, int b); } // namespace Calc
这段代码相当基础:它声明了两个全局函数,包含在Calc
命名空间中(C++命名空间在库中非常有用,帮助避免名称冲突)。
实现文件也非常直接:
chapter-12/01-full-project/src/calc/calc.cpp
namespace Calc { int Sum(int a, int b) { return a + b; } int Multiply(int a, int b) { return a * b; } } // namespace Calc
这结束了src/calc
目录中文件的解释。接下来是src/calc_console
,以及使用这个库构建控制台计算器的可执行文件。
构建 Calc 控制台可执行文件
calc_console
的源目录包含几个文件:一个列表文件,两个实现文件(业务代码和引导程序),和一个头文件。列表文件如下所示:
chapter-12/01-full-project/src/calc_console/CMakeLists.txt(片段)
include(GetFTXUI) add_library(calc_console_static STATIC tui.cpp) target_include_directories(calc_console_static PUBLIC include) target_precompile_headers(calc_console_static PUBLIC <string>) target_link_libraries(calc_console_static PUBLIC calc_shared ftxui::screen ftxui::dom ftxui::component) include(BuildInfo) BuildInfo(calc_console_static) # … testing and program analysis modules # ... documentation generation add_executable(calc_console bootstrap.cpp) target_link_libraries(calc_console calc_console_static)
列表文件看起来很忙,但现在,作为有经验的 CMake 用户,我们可以轻松地解开里面正在发生的事情:
- 包含 CMake 模块以获取 FTXUI 依赖。
- 声明
calc_console_static
目标,其中包含业务代码,但不包括main()
函数,以允许 GTest 定义自己的入口点。 - 添加一个头文件预编译——我们只是添加了一个标准的
string
头文件来证明一个观点,但对于更大的项目,我们可以添加更多(包括项目所属的头文件)。 - 将业务代码与共享的
calc_shared
库和 FTXUI 库链接。 - 添加所有要在该目标上执行的动作:生成构建信息、测试、程序分析和文档。
- 添加并链接
calc_console
引导可执行文件,该文件提供了入口点。
再次,我们将推迟讨论测试和文档,而是查看依赖管理和构建信息生成。
请注意,我们倾向于使用实用模块而不是 find-module 来引入 FTXUI。这是因为这个依赖项不太可能已经存在于系统中。与其希望找到它,不如我们获取并安装它:
chapter-12/01-full-project/cmake/GetFTXUI.cmake
include(FetchContent) FetchContent_Declare( FTXTUI GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git GIT_TAG v0.11 ) option(FTXUI_ENABLE_INSTALL "" OFF) option(FTXUI_BUILD_EXAMPLES "" OFF) option(FTXUI_BUILD_DOCS "" OFF) FetchContent_MakeAvailable(FTXTUI)
我们使用了推荐的FetchContent
方法,在第七章,《使用 CMake 管理依赖项》中有详细描述。唯一的异常添加是option()
命令的调用。它们允许我们跳过 FTXUI 构建的繁琐步骤,并使它的安装配置与这个项目的安装分离。对于 GTest 依赖项也需要这样做。option()
命令在进一步阅读部分中有参考。
calc_command
的列表文件包含了一个更多的自定义实用模块,与构建相关:BuildInfo
。我们将使用它来记录三个可以在可执行文件中显示的值:
- 当前 Git 提交的 SHA
- 构建的时间戳
- 顶级列表文件中指定的项目版本
你可能还记得从第五章,《使用 CMake 编译 C++源码》,我们可以使用 CMake 捕获一些构建时的值,并通过模板文件将它们提供给 C++代码——例如,使用一个方便的 C++结构体:
chapter-12/01-full-project/cmake/buildinfo.h.in
struct BuildInfo { static inline const std::string CommitSHA = "@COMMIT_SHA@"; static inline const std::string Timestamp = "@TIMESTAMP@"; static inline const std::string Version = "@PROJECT_VERSION@"; };
为了在配置阶段填充该结构,我们将使用以下代码:
chapter-12/01-full-project/cmake/BuildInfo.cmake
set(BUILDINFO_TEMPLATE_DIR ${CMAKE_CURRENT_LIST_DIR}) set(DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/buildinfo") string(TIMESTAMP TIMESTAMP) find_program(GIT_PATH git REQUIRED) execute_process(COMMAND ${GIT_PATH} log --pretty=format:'%h' -n 1 OUTPUT_VARIABLE COMMIT_SHA) configure_file( "${BUILDINFO_TEMPLATE_DIR}/buildinfo.h.in" "${DESTINATION}/buildinfo.h" @ONLY ) function(BuildInfo target) target_include_directories(${target} PRIVATE ${DESTINATION}) endfunction()
包含模块将设置包含我们所需信息的变量,然后我们将调用configure_file()
来生成buildinfo.h
。剩下要做的就是调用BuildInfo
函数,并将生成的文件目录添加到所需目标include directories中。如果需要,该文件可以与多个不同的消费者共享。在这种情况下,你可能需要在列表文件的顶部添加include_guard(GLOBAL)
,以避免为每个目标运行git
命令。
在深入讨论控制台计算器的实现之前,我想强调你不必太担心tui.cpp
文件的复杂性。要完全理解它,你将需要对 FXTUI 库有一定的了解——我们在这里不想深入讲解。相反,让我们关注高亮的行:
chapter-12/01-full-project/src/calc_console/tui.cpp
#include "tui.h" #include <ftxui/dom/elements.hpp> #include "buildinfo.h" #include "calc/calc.h" using namespace ftxui; using namespace std; string a{"12"}, b{"90"}; auto input_a = Input(&a, ""); auto input_b = Input(&b, ""); auto component = Container::Vertical({input_a, input_b}); Component getTui() { return Renderer(component, [&] { auto sum = Calc::Sum(stoi(a), stoi(b)); return vbox({ text("CalcConsole " + BuildInfo::Version), text("Built: " + BuildInfo::Timestamp), text("SHA: " + BuildInfo::CommitSHA), separator(), input_a->Render(), input_b->Render(), separator(), text("Sum: " + to_string(sum)), }) | border; }); }
这段代码提供了一个getTui()
函数,它返回一个ftxui::Component
,一个封装了标签、文本字段、分隔符和边框的交互式 UI 元素的对象。如果你对它是如何工作的细节感兴趣,你会在进一步阅读部分找到合适的参考资料。
更重要的是,看看包含指令:它们指的是我们之前通过calc_obj
目标和BuildInfo
模块提供的头文件。lambda 函数构造函数提供的第一个调用库的Calc::Sum
方法,并使用结果值打印带有sum
的标签(通过调用下面的text()
函数)。
同样,标签用于呈现在构建时通过连续三次调用text()
收集的BuildInfo::
值。
这个方法在其相关的头文件中声明:
chapter-12/01-full-project/src/calc_console/include/tui.h
#include <ftxui/component/component.hpp> ftxui::Component getTui();
然后由calc_console
目标中的引导程序使用:
chapter-12/01-full-project/src/calc_console/bootstrap.cpp
#include <ftxui/component/screen_interactive.hpp> #include "tui.h" int main(int argc, char** argv) { ftxui::ScreenInteractive::FitComponent().Loop(getTui()); }
这段简短的代码利用了ftxui
来创建一个交互式控制台屏幕,它接收getTui()
返回的Component
对象,使其对用户可见,并在循环中收集键盘事件,创建一个界面,如图12.1所示。再次强调,完全理解这一点并不是非常关键,因为ftxui
的主要目的是为我们提供一个外部依赖,我们可以用它来练习 CMake 技术。
我们已经覆盖了src
目录中的所有文件。让我们继续讨论前面提到的测试和分析程序的主题。
测试与程序分析
程序分析与测试相辅相成,确保我们解决方案的质量。例如,当使用测试代码时,运行 Valgrind 变得更加一致。为此,我们将配置这两件事在一起。图12.5展示了执行流程和设置所需的文件(将在src
目录中添加几个片段):
](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/mdn-cmk-cpp/img/Figure_12.5_B17205.jpg)
图 12.5 – 用于启用测试和程序分析的文件
正如我们所知,测试文件位于test
目录中,它们的列表文件通过add_subdirectory()
命令从顶层列表文件执行。让我们看看里面有什么:
chapter-12/01-full-project/test/CMakeLists.txt
include(Testing) add_subdirectory(calc) add_subdirectory(calc_console)
在Testing
模块中定义的测试工具包括在这个级别,以允许两个目标组(来自calc
和calc_console
目录)使用它们:
chapter-12/01-full-project/cmake/Testing.cmake (片段)
enable_testing() include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG release-1.11.0 ) # For Windows: Prevent overriding the parent project's # compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) option(INSTALL_GMOCK "Install GMock" OFF) option(INSTALL_GTEST "Install GTest" OFF) FetchContent_MakeAvailable(googletest) ...
我们启用了测试并包含了FetchContent
模块以获取 GTest 和 GMock。在这个项目中我们实际上并没有使用 GMock,但这两个框架都包含在一个单一的存储库中,因此我们也需要配置 GMock。此配置的突出部分使这两个框架的安装与我们的项目的安装分离(通过将适当的option()
设置为OFF
)。
接下来,我们需要创建一个函数,以启用对业务目标的彻底测试。我们将其保存在同一个文件中:
chapter-12/01-full-project/cmake/Testing.cmake(继续)
... include(GoogleTest) include(Coverage) include(Memcheck) macro(AddTests target) target_link_libraries(${target} PRIVATE gtest_main gmock) gtest_discover_tests(${target}) AddCoverage(${target}) AddMemcheck(${target}) endmacro()
在这里,我们首先包含必要的模块:GoogleTest
与 CMake 捆绑在一起,但Coverage
和Memcheck
将由我们编写。然后我们提供了一个AddTests
宏,该宏将准备一个测试目标、启用覆盖和内存检查。让我们详细看看它是如何工作的。
面向 C++ 的现代 CMake 教程(五)(3)https://developer.aliyun.com/article/1526953