面向 C++ 的现代 CMake 教程(五)(2)

简介: 面向 C++ 的现代 CMake 教程(五)

面向 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 用户,我们可以轻松地解开里面正在发生的事情:

  1. 包含 CMake 模块以获取 FTXUI 依赖。
  2. 声明calc_console_static目标,其中包含业务代码,但不包括main()函数,以允许 GTest 定义自己的入口点。
  3. 添加一个头文件预编译——我们只是添加了一个标准的string头文件来证明一个观点,但对于更大的项目,我们可以添加更多(包括项目所属的头文件)。
  4. 将业务代码与共享的calc_shared库和 FTXUI 库链接。
  5. 添加所有要在该目标上执行的动作:生成构建信息、测试、程序分析和文档。
  6. 添加并链接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模块中定义的测试工具包括在这个级别,以允许两个目标组(来自calccalc_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 捆绑在一起,但CoverageMemcheck将由我们编写。然后我们提供了一个AddTests宏,该宏将准备一个测试目标、启用覆盖和内存检查。让我们详细看看它是如何工作的。

面向 C++ 的现代 CMake 教程(五)(3)https://developer.aliyun.com/article/1526953

相关文章
|
6天前
|
C++
Clion CMake C/C++程序输出乱码
Clion CMake C/C++程序输出乱码
9 0
|
7天前
|
存储 算法 编译器
C++ 函数式编程教程
C++ 函数式编程学习
|
7天前
|
存储 编译器 开发工具
C++语言教程分享
C++语言教程分享
|
7天前
|
存储 编译器 C++
|
28天前
|
C++ 存储 索引
面向 C++ 的现代 CMake 教程(一)(5)
面向 C++ 的现代 CMake 教程(一)
46 0
|
28天前
|
缓存 存储 C++
面向 C++ 的现代 CMake 教程(一)(4)
面向 C++ 的现代 CMake 教程(一)
45 0
|
28天前
|
C++ 缓存 存储
面向 C++ 的现代 CMake 教程(一)(3)
面向 C++ 的现代 CMake 教程(一)
43 0
|
28天前
|
缓存 C++ Windows
面向 C++ 的现代 CMake 教程(一)(2)
面向 C++ 的现代 CMake 教程(一)
57 0
|
28天前
|
C++ 容器 Docker
面向 C++ 的现代 CMake 教程(一)(1)
面向 C++ 的现代 CMake 教程(一)
67 0
|
Java 编译器 Linux
【CMake】CMake 引入 ( Android Studio 创建 Native C++ 工程 | C/C++ 源码编译过程 | Makefile 工具 | CMake 引入 )(二)
【CMake】CMake 引入 ( Android Studio 创建 Native C++ 工程 | C/C++ 源码编译过程 | Makefile 工具 | CMake 引入 )(二)
280 0
【CMake】CMake 引入 ( Android Studio 创建 Native C++ 工程 | C/C++ 源码编译过程 | Makefile 工具 | CMake 引入 )(二)