CMake 和 C++ 是永远的兄弟

CMake 和 C++ 是永远的兄弟

在开发过程中,我喜欢更改编译器、构建模式、依赖版本、执行静态分析、测量性能、收集覆盖率、生成文档等。 我真的很喜欢 CMake,因为它允许我做我想做的一切。

很多人批评 CMake,而且常常是当之无愧的,但如果你仔细看看,你会发现并不是一切都那么糟糕,最近 一点也不差,而且发展方向是相当积极的。

在这篇笔记中,我想告诉你如何在CMake系统中简单地组织一个C++的头库来获得以下功能:

  1. 集会;
  2. 自动运行测试;
  3. 代码覆盖率测量;
  4. 安装;
  5. 自动记录;
  6. 在线沙箱生成;
  7. 静态分析。

任何已经了解优势和 C-make 的人都可以简单地 下载项目模板 并开始使用它。


内容

  1. 从内部进行项目
    1. 项目结构
    2. 主 CMake 文件 (./CMakeLists.txt)
      1. 项目信息
      2. 项目选项
      3. 编译选项
      4. 的主要目标
      5. 安装
      6. 测试
      7. Документация
      8. 在线沙箱
    3. 测试脚本(test/CMakeLists.txt)
      1. 测试
      2. 覆盖范围
    4. 文档脚本 (doc/CMakeLists.txt)
    5. 在线沙箱脚本(online/CMakeLists.txt)
  2. 外部项目
    1. 装配
      1. 装配
    2. 选项
      1. MYLIB_COVERAGE
      2. MYLIB_测试
      3. MYLIB_DOXYGEN_LANGUAGE
    3. 装配目标
      1. 默认情况下,
      2. mylib 单元测试
      3. 覆盖
      4. DOC
      5. 魔杖盒
    4. Примеры
  3. 工具
  4. 静态分析
  5. 后记

从内部进行项目

项目结构

.
├── CMakeLists.txt
├── README.en.md
├── README.md
├── doc
│   ├── CMakeLists.txt
│   └── Doxyfile.in
├── include
│   └── mylib
│       └── myfeature.hpp
├── online
│   ├── CMakeLists.txt
│   ├── mylib-example.cpp
│   └── wandbox.py
└── test
    ├── CMakeLists.txt
    ├── mylib
    │   └── myfeature.cpp
    └── test_main.cpp

我们主要讲如何组织CMake脚本,所以会详细讨论。 任何人都可以直接查看其余文件 在模板项目页面上.

主 CMake 文件 (./CMakeLists.txt)

项目信息

首先,需要请求所需的CMake系统版本。 CMake 正在不断发展,不同条件下的命令签名和行为正在发生变化。 为了让 CMake 立即了解我们想要从中得到什么,我们需要立即记录我们对它的要求。

cmake_minimum_required(VERSION 3.13)

然后我们将指定我们的项目,它的名称、版本、使用的语言等(参见. команду project).

在这种情况下,我们指定语言 CXX (这意味着 C++),这样 CMake 就不会紧张并搜索 C 语言编译器(默认情况下,CMake 包括两种语言:C 和 C++)。

project(Mylib VERSION 1.0 LANGUAGES CXX)

在这里您可以立即检查我们的项目是否作为子项目包含在另一个项目中。 这对未来会有很大帮助。

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

项目选项

我们将提供两种选择。

第一个选项是 MYLIB_TESTING — 禁用单元测试。 如果我们确定测试一切正常,那么这可能是必要的,但我们只想安装或打包我们的项目。 或者我们的项目作为子项目包含在内 - 在这种情况下,我们项目的用户对运行我们的测试不感兴趣。 您没有测试您使用的依赖项,对吗?

option(MYLIB_TESTING "Включить модульное тестирование" ON)

另外,我们会单独做一个选项 MYLIB_COVERAGE 用于通过测试测量代码覆盖率,但它需要额外的工具,因此需要显式启用。

option(MYLIB_COVERAGE "Включить измерение покрытия кода тестами" OFF)

编译选项

当然,我们是酷程序员,因此我们希望编译器能够进行最高级别的编译时诊断。 没有一只老鼠会溜过去。

add_compile_options(
    -Werror

    -Wall
    -Wextra
    -Wpedantic

    -Wcast-align
    -Wcast-qual
    -Wconversion
    -Wctor-dtor-privacy
    -Wenum-compare
    -Wfloat-equal
    -Wnon-virtual-dtor
    -Wold-style-cast
    -Woverloaded-virtual
    -Wredundant-decls
    -Wsign-conversion
    -Wsign-promo
)

我们还将禁用扩展以完全符合 C++ 语言标准。 它们在 CMake 中默认启用。

if(NOT CMAKE_CXX_EXTENSIONS)
    set(CMAKE_CXX_EXTENSIONS OFF)
endif()

的主要目标

我们的库仅由头文件组成,这意味着我们没有静态或动态库形式的任何消耗。 另一方面,为了在外部使用我们的库,需要安装它,需要在系统中可检测到它并连接到您的项目,同时这些相同的标头,以及可能的一些附加标头,附加到它的属性。

为此,我们创建了一个界面库。

add_library(mylib INTERFACE)

我们将标头绑定到我们的接口库。

CMake 的现代、时尚、年轻的使用意味着标头、属性等。 通过单一目标传输。 所以只要说一下就够了 target_link_libraries(target PRIVATE dependency),以及与目标关联的所有标头 dependency,将可用于属于目标的源 target。 而且你不需要任何 [target_]include_directories。 这将在下面的分析中得到证明 用于单元测试的 CMake 脚本.

所谓的也值得关注。 выражения-генераторы: $<...>.

此命令将我们需要的标头与接口库相关联,如果我们的库连接到同一 CMake 层次结构中的任何目标,则目录中的标头将与其关联 ${CMAKE_CURRENT_SOURCE_DIR}/include,如果我们的库安装在系统上并使用命令连接到另一个项目 find_package,然后目录中的标头将与其关联 include 相对于安装目录。

target_include_directories(mylib INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

让我们制定一个语言标准。 当然是最后一张了。 同时,我们不仅包含该标准,还将其扩展到那些将使用我们的库的人。 这是由于 set 属性具有类别这一事实而实现的 INTERFACE (见。 target_compile_features 命令).

target_compile_features(mylib INTERFACE cxx_std_17)

让我们为我们的库创建一个别名。 而且,为了美观,它将位于一个特殊的“命名空间”中。 当我们的库中出现不同的模块并且我们将它们彼此独立地连接时,这将很有用。 例如在布斯塔.

add_library(Mylib::mylib ALIAS mylib)

安装

将我们的标头安装到系统中。 这里一切都很简单。 我们说包含所有标题的文件夹应该进入目录 include 相对于安装位置。

install(DIRECTORY include/mylib DESTINATION include)

接下来,我们通知构建系统我们希望能够在第三方项目中调用该命令 find_package(Mylib) 并获得一个目标 Mylib::mylib.

install(TARGETS mylib EXPORT MylibConfig)
install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)

接下来的咒语应该是这样理解的。 当在第三方项目中我们调用命令 find_package(Mylib 1.2.3 REQUIRED),并且安装的库的真实版本会与版本不兼容 1.2.3CMake会自动生成错误。 也就是说,您不需要手动跟踪版本。

include(CMakePackageConfigHelpers)
write_basic_package_version_file("${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake"
    VERSION
        ${PROJECT_VERSION}
    COMPATIBILITY
        AnyNewerVersion
)
install(FILES "${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" DESTINATION share/Mylib/cmake)

测试

如果使用显式禁用测试 相应选项 或者我们的项目是一个子项目,即使用命令连接到另一个CMake项目 add_subdirectory,我们不会沿着层次结构进一步移动,并且描述用于生成和运行测试的命令的脚本根本不会运行。

if(NOT MYLIB_TESTING)
    message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib не тестируется в режиме подмодуля")
else()
    add_subdirectory(test)
endif()

Документация

如果是子项目,也不会生成文档。

if(NOT IS_SUBPROJECT)
    add_subdirectory(doc)
endif()

在线沙箱

同样,子项目也不会有在线沙箱。

if(NOT IS_SUBPROJECT)
    add_subdirectory(online)
endif()

测试脚本(test/CMakeLists.txt)

测试

首先,我们找到一个包含所需测试框架的包(替换为您最喜欢的框架)。

find_package(doctest 2.3.3 REQUIRED)

让我们通过测试创建可执行文件。 通常我只将包含该函数的文件直接添加到可执行二进制文件中 main.

add_executable(mylib-unit-tests test_main.cpp)

我添加了稍后描述测试本身的文件。 但你不必这样做。

target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)

我们连接依赖关系。 请注意,我们仅将所需的 CMake 目标链接到二进制文件,并且没有调用命令 target_include_directories。 来自测试框架和我们的标题 Mylib::mylib以及构建参数(在我们的例子中,这是 C++ 语言标准)与这些目标一起实现。

target_link_libraries(mylib-unit-tests
    PRIVATE
        Mylib::mylib
        doctest::doctest
)

最后,我们创建一个虚拟目标,其“构建”相当于运行测试,并将此目标添加到默认构建中(该属性负责此操作) ALL)。 这意味着默认构建会触发测试运行,这意味着我们永远不会忘记运行它们。

add_custom_target(check ALL COMMAND mylib-unit-tests)

覆盖范围

接下来,如果指定了适当的选项,我们将启用代码覆盖率测量。 我不会详细介绍,因为它们更多地与测量覆盖率的工具相关,而不是与 CMake 相关。 唯一重要的是要注意,将根据结果创建目标 coverage,可以方便地开始测量覆盖范围。

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Измерение покрытия кода тестами включено")

    target_compile_options(mylib-unit-tests PRIVATE --coverage)
    target_link_libraries(mylib-unit-tests PRIVATE gcov)

    add_custom_target(coverage
        COMMAND
            ${GCOVR_EXECUTABLE}
                --root=${PROJECT_SOURCE_DIR}/include/
                --object-directory=${CMAKE_CURRENT_BINARY_DIR}
        DEPENDS
            check
    )
elseif(MYLIB_COVERAGE AND NOT GCOVR_EXECUTABLE)
    set(MYLIB_COVERAGE OFF)
    message(WARNING "Для замеров покрытия кода тестами требуется программа gcovr")
endif()

文档脚本 (doc/CMakeLists.txt)

发现 Doxygen.

find_package(Doxygen)

接下来,我们检查用户是否设置了语言变量。 如果是,那么我们就不碰它,如果不是,那么我们就学俄语。 然后我们配置 Doxygen 系统文件。 所有必要的变量,包括语言,都会在配置过程中到达那里(请参阅。 команду configure_file).

然后我们创建一个目标 doc,这将开始生成文档。 由于生成文档并不是开发过程中最大的需求,因此默认情况下不会启用该目标;必须显式启动它。

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "Doxygen documentation will be generated in ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

在线沙箱脚本(online/CMakeLists.txt)

这里我们找到第三个Python并创建一个目标 wandbox,生成对应服务API的请求 魔杖盒,并把他送走。 响应带有指向已完成沙箱的链接。

find_program(PYTHON3_EXECUTABLE python3)
if(PYTHON3_EXECUTABLE)
    set(WANDBOX_URL "https://wandbox.org/api/compile.json")

    add_custom_target(wandbox
        COMMAND
            ${PYTHON3_EXECUTABLE} wandbox.py mylib-example.cpp "${PROJECT_SOURCE_DIR}" include |
            curl -H "Content-type: application/json" -d @- ${WANDBOX_URL}
        WORKING_DIRECTORY
            ${CMAKE_CURRENT_SOURCE_DIR}
        DEPENDS
            mylib-unit-tests
    )
else()
    message(WARNING "Для создания онлайн-песочницы требуется интерпретатор ЯП python 3-й версии")
endif()

外部项目

现在让我们看看如何使用这一切。

装配

与 CMake 构建系统上的任何其他项目一样,构建此项目由两个阶段组成:

cmake -S путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

如果由于旧版本的 CMake,上述命令不起作用,请尝试省略 -S:

cmake путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

有关选项的更多信息.

构建项目

cmake --build путь/к/сборочной/директории [--target target]

有关装配目标的更多信息.

选项

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]

包括目标 coverage,您可以开始通过测试来测量代码覆盖率。

MYLIB_测试

cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]

提供禁用单元测试构建和目标的能力 check。 结果,测试对代码覆盖率的测量被关闭(请参阅。 MYLIB_COVERAGE).

如果使用命令将项目作为子项目连接到另一个项目,测试也会自动禁用 add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]

切换目标生成的文档的语言 doc 到给定的一个。 有关可用语言的列表,请参阅 Doxygen系统网站.

默认情况下启用俄语。

装配目标

默认情况下,

cmake --build path/to/build/directory
cmake --build path/to/build/directory --target all

如果未指定目标(相当于目标 all),收集它能收集的一切,并且还调用目标 check.

mylib 单元测试

cmake --build path/to/build/directory --target mylib-unit-tests

编译单元测试。 默认启用。

cmake --build путь/к/сборочной/директории --target check

运行收集的(收集的,如果尚未收集的)单元测试。 默认启用。

См。 также mylib-unit-tests.

覆盖

cmake --build путь/к/сборочной/директории --target coverage

通过使用程序的测试来分析正在运行(如果尚未运行)单元测试的代码覆盖率 冠状病毒.

涂层排气看起来像这样:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

仅当启用该选项时目标才可用 MYLIB_COVERAGE.

См。 также check.

DOC

cmake --build путь/к/сборочной/директории --target doc

使用系统开始生成代码文档 Doxygen的.

魔杖盒

cmake --build путь/к/сборочной/директории --target wandbox

该服务的响应如下所示:

{
    "permlink" :    "QElvxuMzHgL9fqci",
    "status" :  "0",
    "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}

该服务用于此目的 魔杖盒。 我不知道他们的服务器有多灵活,但我认为这个机会不应该被滥用。

Примеры

通过覆盖率测量在调试模式下构建项目

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build путь/к/сборочной/директории --target coverage --parallel 16

无需预先组装和测试即可安装项目

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=путь/к/установойной/директории
cmake --build путь/к/сборочной/директории --target install

使用给定的编译器以发布模式构建

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=путь/к/директории/куда/установлены/зависимости
cmake --build путь/к/сборочной/директории --parallel 4

生成英文文档

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build путь/к/сборочной/директории --target doc

工具

  1. CMake的 3.13

    事实上,CMake 版本 3.13 仅需要运行本帮助中描述的一些控制台命令。 从CMake脚本语法的角度来看,如果以其他方式调用生成,3.8版本就足够了。

  2. 测试库 文档测试

    可以禁用测试(请参阅 опцию MYLIB_TESTING).

  3. Doxygen的

    要切换生成文档​​的语言,提供了一个选项 MYLIB_DOXYGEN_LANGUAGE.

  4. 语言翻译 Python的3

    用于自动生成 在线沙箱.

静态分析

借助 CMake 和一些优秀的工具,您可以轻松地提供静态分析。

Cpp检查

CMake 内置了对静态分析工具的支持 Cpp检查.

为此,您需要使用该选项 CMAKE_CXX_CPPCHECK:

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iпуть/к/исходникам/include"

此后,每次编译和重新编译源代码时都会自动启动静态分析。 无需执行任何额外操作。

在一个很棒的工具的帮助下 scan-build 您还可以立即运行静态分析:

scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директории

在这里,与 Cppcheck 的情况不同,您需要每次都运行构建 scan-build.

后记

CMake 是一个非常强大且灵活的系统,可让您实现适合各种品味和颜色的功能。 而且,尽管语法有时还有很多不足之处,但魔鬼仍然并不像描绘的那么可怕。 使用 CMake 构建系统造福社会和健康。

下载项目模板

来源: habr.com

添加评论