12

cmake 07 - z11_m1 细节

 3 years ago
source link: https://hedzr.github.io/cmake/notes/cmake-07/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

这个系列与 cmake 有关的记录也好,笔记也好,成书也好,目前暂且归属在 cmake-hello 标记之下。

源码请访问:

https://github.com/hedzr/study-cmake
请注意源码仍在跟随我的笔记内容的迭代而更新中。

我们做了辣么多的准备工作了,早就想要介入具体项目的编写工作了。

z11-m1Permalink

以前我们做了 z01 到 z04 等一组以传统 cmake 风格编写的具体项目,现在我们将要开始名为 z11-m1 的新的子项目,它包含至少一个 library Target,以及使用这个 library 的相应的 executables。

首先我们总览一下 z11-m1 的目录结构:

image-20201124152515280

在截图里没有展现上级目录(即 study-cmake 的根目录),因为现在我们只关心下级子目录中的这些子项目应该怎么布局。

你已经看到了我们的 z11-m1 包含 app, app-auto, libs/sm-lib 这几个子目录,并且全都有一个 CMakeLists.txt 负责添加和管理各自的下级子目录。

z11-m1/CMakeLists.txtPermalink

z11-m1/CMakeLists.txt 的内容是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cmake_minimum_required(VERSION 3.5..3.19)
set(CMAKE_SCRIPTS "../cmake")
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS}/modules;${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS};${CMAKE_SOURCE_DIR};${CMAKE_SOURCE_DIR}/..;${CMAKE_MODULE_PATH}")
include(prerequisites)
include(version-def)
include(add-policies)     # ${CMAKE_SOURCE_DIR}/${CMAKE_SCRIPTS}/
include(detect-systems)
#include(target-dirs)
include(utils)


project(z11-m1)

include(cxx-standard-def)

add_subdirectory(libs/sm-lib)
add_subdirectory(app-auto)
# add_subdirectory(app)

这些代码不复杂,为了能够不依靠 study-cmake 的顶级 CMakeLists.txt 所提供的基本环境也能独立编译 app/ 子项目,所以 z11-m1/CMakeLists.txt 冗余了相应的初始化结构,注意在设置 CMAKE_MODULE_PATH 时有矫正 CMAKE_SCRIPTS 的值以保证能够搜索到父目录中的 cmake/ 文件夹。

z11-m1/CMakeLists.txt 的真正目的只是包含 libs/sm-lib 和 app-auto 子项目。

z11-m1/libs/sm-libPermalink

现在我们要编写一个 library,且令其具有 Modern CMake 的风格,不但时一个具有自我意识的 Target,而且能够提供自己的定位信息,可以很容易地嵌入到一个 cmake hosted 的构建主机环境中。

定义 projectPermalink

首先还是看看 CMakeLists.txt 的内容,其开始部分如下:

1
2
cmake_minimum_required(VERSION 3.5)
project(sm-lib VERSION 1.0.0 LANGUAGES CXX)

查找 package (依赖库)Permalink

1
2
3
4
5
6
7
8
9
10
11
find_package(ZLIB REQUIRED)

#if ( ZLIB_FOUND )
#    include_directories( ${ZLIB_INCLUDE_DIRS} )
#    target_link_libraries( YourProject ${ZLIB_LIBRARIES} )
#endif( ZLIB_FOUND )

#if (ZLIB_FOUND)
#    debug_print_value(ZLIB_INCLUDE_DIRS)
#    debug_print_value(ZLIB_LIBRARIES)
#endif ()

我们会查找 zlib package 的安装信息(在大多数系统中它应该都是有效的,如果没有,你可能需要 apt install zlib1-dev 这样的命令去安装它)。

像 zlib 这样的 package 并不需要做到 Modern CMake 风格兼容,因为它实在是太常见了,而且它被作为各种发行版的 bundle 组成元素之一的历史甚至远超过 cmake。所以我们需要 find_package(ZLIB REQUIRED) 指令去查找 zlib 的安装位置。并且我们也需要将查找的结果加入到 Target Properties 中。

更多关于查找 package 的内容在后面章节专门讨论。

定义 sm-lib TargetPermalink

1
2
3
4
5
6
7
8
9
10
11
12
13
add_library(${PROJECT_NAME})
add_library(libs::${PROJECT_NAME} ALIAS ${PROJECT_NAME})

target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_11)
target_sources(${PROJECT_NAME} PRIVATE src/${PROJECT_NAME}.cc)
target_link_libraries(${PROJECT_NAME} PRIVATE ${ZLIB_LIBRARIES})
target_include_directories(${PROJECT_NAME}
        PUBLIC
          $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
          $<INSTALL_INTERFACE:include>
        PRIVATE
          ${ZLIB_INCLUDE_DIRS}
        )

接下来我们通过 add_library(${PROJECT_NAME}) 声明名为 sm-lib 的 Library Target。

请注意下一语句 add_library(libs::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) 为 sm-lib Library Target 建立一个别名 libs::sm-lib。这个别名很重要,它符合我们建立的 libs/sm-lib 文件夹结构,因为 app-auto 子模块将会通过此别名直接引用 sm-lib。理论上说,内部的依赖关系是可以直接使用 sm-lib 这个 Target 名字来解决的,但我们使用 libs:: 名字空间可以具有更好的可读性。

接下来的 target_xxx 语句负责为 sm-lib Library Target 设定CXX标准、源文件、链入依赖库以及包含文件搜索路径:

  • ${ZLIB_LIBRARIES}${ZLIB_INCLUDE_DIRS}find_package(ZLIB) 的结果。

    • ${ZLIB_LIBRARIES} 作为链接依赖库,对使用者透明,所以采用了 PRIVATE 限定。
    • 同样道理,${ZLIB_INCLUDE_DIRS}target_include_directories 中也是被 PRIVATE 所修饰的。
  • 采用 PRIVATE 限定的源文件是因为 sm-lib 的 cpp 文件对于使用者来说是无意义的。sm-lib 编译后只有头文件和 .a 文件将被分发给使用者。

  • 将被公布给类库使用者的头文件,也即采用 PUBLIC 宣告的有:

    • $<INSTALL_INTERFACE:include> 是一个 generator expression,它意味着 install-tree 中的include 目录会被包含在头文件搜索路径中。

    • $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> 是一个 generator expression,它意味着 build-tree 中的 sm-lib/include 文件夹,将被包含在头文件搜索路径中。

    • 为了能够在两种场景下都能正确地完成头文件搜索并正确编译 sm-lib 库,所以我们需要同时声明以上两条记录。

      其中 $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> 将会在库作者编译 app-auto 时起作用,这时实际上是一种内部库引用关系。

      $<INSTALL_INTERFACE:include> 是提供给 sm-lib 的使用者的,因为对于使用者来说,sm-lib 的头文件将会被安装在 ${CMAKE_INSTALL_PREFIX}/include/sm-libinclude 之所以不带有 CMAKE_INSTALL_PREFIX 前缀,是为了让 cmake 能够根据实际情况解决前缀,即所谓的 Relocatable Package

加入测试节Permalink

1
2
3
4
if (BUILD_TESTING)
    message("tests enabled")
    add_subdirectory(tests)
endif ()

我们会在一个子目录 tests 中编写测试用例(子项目)。

具体的内容交由 Testing 章节详解。

由于 CTest 要求 add_test 应该是基于一个 executable 才能附着,所以在 sm-lib 上我们并不能直接通过 add_test 的方式为其建立测试 Target。

取而代之的是,我们是在 app-auto 模块中建立相应的 test target 来对 sm-lib 做单元测试的。

加入安装方案Permalink

现在加入安装脚本用于将 sm-lib 安装到目标位置。

首先我们要了解的是 CMAKE_INSTALL_PREFIX 是所谓的 install-tree 目标位置。一个 Library 的公开头文件将被安装到 <prefix>/include,库文件(.a)将被安装到 <prefix>/lib (对于不同的发行版不同的处理器架构,也可能安装到 <prefix>/lib64 等等位置)。另外如果你想要,还可以提供 man 手册,附加文件等等。

install 指令Permalink

install 是一个规则组,可用于指定安装时刻将会被运行的规则。

这些规则包括;

1
2
3
4
5
6
install(TARGETS <target>... [...])
install({FILES | PROGRAMS} <file>... [...])
install(DIRECTORY <dir>... [...])
install(SCRIPT <file> [...])
install(CODE <code> [...])
install(EXPORT <export-name> [...])

它们的精确语法已经使用细节可以查看官网文档。

我们的实例Permalink

我们为 sm-lib 这样安排其安装方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
install(TARGETS ${PROJECT_NAME}
        EXPORT ${PROJECT_NAME}Targets
        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
        ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
        )

install(EXPORT ${PROJECT_NAME}Targets
        FILE ${PROJECT_NAME}Targets.cmake
        NAMESPACE ${PROJECT_NAME}::
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})

install(DIRECTORY include/${PROJECT_NAME}
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
        )

install(FILES
        ${CMAKE_CURRENT_LIST_DIR}/cmake/${PROJECT_NAME}Config.cmake
        DESTINATION
        ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})

理解我们编写的安装规则集也很容易:

  • 第一条命令定义了 sm-lib 库 这个 Target 具有一个控制文件,名为 sm-libTargets.cmake 。此外我们也指定 install-tree 与目标机文件夹的相对关系,在这里实际上我们指明了 install-tree 的根目录应该等价于 ${CMAKE_INSTALL_LIBDIR}。与此同时,我们也指明了库的构建结果将被安装到何处:

    • ARCHIVE DESTINATION 表示静态库的安装位置,另外,动态库都会具有一个 import lib 用于链接目的,也会被安装到该位置
    • LIBRARY DESTINATION 表示动态库,例如 .so, .dylib, .dll 等,的安装位置
    • 动态库或静态库都是在构建中生成的,它们都是本 Target 的构建结果

    你可以指定不同的 install-tree 根目录,例如 /temp/x86 之类,这是被允许的,尽管它不符合 GNU 路径规范,可能影响到兼容性。

    注意 CMAKE_INSTALL_DIR 不加限定时,我们都认为它具有默认值 /usr/local

  • 第二条命令描述了我们的 sm-libTargets.cmake 应该被安装到 install-tree 中何处被找到。当前的配置指定了会安装到 ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME},典型的真实路径常常是 /usr/local/cmake/sm-lib/sm-libTargets.cmake

    此外也指明了应该使用 “sm-lib” 作为其名字空间。

    因此,库的使用者通过 cmake 引用 sm-lib 时,将会采用 sm-lib::sm-lib 这个全限定名。具体用法请参考 app 项目中的引用语句。

  • 第三条命令表示说我们需要安装 sm-lib 的公开头文件到 install-tree 的头文件目录中。此处的 include/${PROJECT_NAME} 代表着 source-tree 中的 sm-lib 文件夹中的 include/sm-lib 子目录。

  • 第四条命令负责将 source-tree 中的 sm-libConfig.cmake 复制到 install-tree 中。sm-libConfig.cmake 是我们作为库作者需要维护和编写的一个控制文件,其中包含在目标机上我们的库被安装在什么地方,应该被如何找到。

由于没有涉及到文档构建,man 手册构建,以及项目附加文件的安装问题,因为当前只需要上面列举的四条 install 指令就足以完成库的安装与分发了。

我们没有给出关于 executable 的 installing 实例,但它并不比 library Target 的 install 脚本更难,因而当前限于精力就暂且搁置了。

你可能已经注意到在 internal-library 和 external-library 的引用名称上,我们采用了不同的名字空间:在 internal-library 引用时,我们使用 libs::sm-lib 名字,请查阅 定义 sm-lib Target 章节;在 external-library 引用时,我们使用 sm-lib::sm-lib 名字。这样做有好处也有坏处。但你可以自行决定你的命名策略。

我们的 sm-libConfig.cmakePermalink

在 libs/sm-lib/cmake 子目录中,我们准备了 sm-libConfig.cmake 控制文件:

1
2
3
4
5
6
7
include(CMakeFindDependencyMacro)
find_dependency(ZLIB REQUIRED)

## Import the targets
if (NOT TARGET sm-lib::sm-lib)
  include("${CMAKE_CURRENT_LIST_DIR}/sm-libTargets.cmake")
endif ()

对于一个静态库来说,需要做的事并不多,我们只需要解决内部对 ZLIB 的依赖即可。

这个文件在第四条 install 指令规则中被定义为从 source-tree 复制到 install-tree。

Testing 相关 - CTestPermalink

利用 CTest 进行单元测试

CTest 是 CMake 的内建工具。

你可以在命令行直接执行 ctest --help。但在一个项目工程中,更便利的方法是:

1
cmake --build build/ --target test

简介Permalink

enable_testing 为当前目录及其所有子目录启用测试能力。所谓的测试能力是由 CTest module 提供的。如果你执行了 include(CTest) 指令,则 enable_testing 已经被自动执行了,此时可以通过一个 OPTION 变量 BUILD_TESTING 来判定启用与否。不过反过来一样成立,如果不加以额外设置,在 enable_testing() 之后,CTest 就已经处于已载入就绪的状态。

1
2
3
4
5
6
include(CTest)
if(BUILD_TESTING)
  # ... CMake code to create tests ...
  message("tests enabled")
  add_subdirectory(tests)
endif()

一旦启用了测试能力之后,我们可以通过 add_test 指令来添加测试命令行。add_test 指令的语法为:

1
2
3
4
add_test(NAME <name> COMMAND <command> [<arg>...]
         [CONFIGURATIONS <config>...]
         [WORKING_DIRECTORY <dir>]
         [COMMAND_EXPAND_LISTS])

我们可以这样编写实例:

1
2
3
add_test(NAME mytest
         COMMAND testDriver --config $<CONFIGURATION>
                            --exe $<TARGET_FILE:myexe>)

或者当我们有一个 executable 时为其附加一个test:

1
2
3
4
5
6
7
8
9
10
11
project(app)
add_executable(app main.cc)

enable_testing()
add_subdirectory(tests)

# in tests/CMakeLists.txt

project(app_test)
add_executable(app_test test.cc)
add_test(app_test app)

ctest提供了丰富的命令行参数。其中一些内容将在以后的示例中探讨。要获得完整的列表,需要使用ctest --help来查看。命令cmake --help-manual ctest会将向屏幕输出完整的ctest手册。*

study-cmake 中的 shortcutPermalink

在 study-cmake 项目中,我们简化了和 testing 相关的内容,因此对于 z11-m1/app-auto 子模块来说,我们可以很简单地在其 CMakeLists.txt 加入如下指令来使能 CTest:

1
2
3
4
5
6
7
8
project(sm-app-auto
        VERSION 1.0.0
        LANGUAGES CXX)
add_executable(sm-app-auto main.cc)
...
if (BUILD_TESTING)
    add_unit_test(${PROJECT_NAME} tests sm_lib_app_tests)
endif ()

然后我们在 z11-m1/app-auto 中加入 tests 子目录和相应的测试代码,测试代码被挂接在 sm_lib_app_tests 这个 Target 中形成了一个 executable,相应的 tests/CMakeLists.txt 为:

1
2
3
4
5
6
7
8
9
10
11
cmake_minimum_required(VERSION 3.5)
project(sm_lib_app_tests)

add_executable(${PROJECT_NAME}
        main.cc
        )

target_link_libraries(${PROJECT_NAME}
        PRIVATE
        libs::sm-lib
        )

你可以注意到 tests 子目录没有任何特殊的 CTest 内容,它就是一个简单的 executable Target 而已。

所有的工作都隐藏在 app-auto 中的 add_unit_test(${PROJECT_NAME} tests sm_lib_app_tests) 指令里。

然后我们构建整个项目成功之后,CTest 测试流程将被自动调用而无需人工介入,你也不必关心什么样的命令行才能启动 ctest。

背景Permalink

这一切有赖于这些预先配置的内容:

cmake/prerequisites.cmakePermalink

在 prerequisites.cmake 中我们有一组 CTest 初始化指令:

1
2
3
4
5
6
# for testing
set(ENV{CTEST_OUTPUT_ON_FAILURE} 1)
set_property(GLOBAL PROPERTY UNIT_TEST_TARGETS)
mark_as_advanced(UNIT_TEST_TARGETS)
enable_testing()
# include(CTest)
cmake/utils.cmakePermalink

在 utils.cmake 中我们定义了两个宏指令:add_unit_testapply_all_unit_tests

1
add_unit_test(target target_dirname target_test)

add_unit_test 有三个参数,target_test 是测试用例的 Target 名字,也就是 “sm_lib_app_tests”,target 是上级 executable 的 Target 名字,也就是 “sm-app-auto”,我们用了 ${PROJECT-NAME} 来隐藏该硬编码的字符串,target_dirname 是 tests 子目录的名字。

add_unit_test 会将 target_dirname 所指示的子目录通过 add_subdirectory 指令加载进来,然后用 add_test 指令将 target_test 挂接到 target 之上,然后注册 target_test 的名字。

CMakeLists.txtPermalink

在 source-tree 根目录的 CMakeLists.txt 的末尾,我们有一条指令调用:

1
2
# invoke CTest unittests automatically.
apply_all_unit_tests(all_tests)

通过 add_unit_test 注册的所有 target_test(s) 在此时被编制为 CTest 命令行,交给 POST_BUILD 去执行,从而达到了自动执行单元测试用例的效果。

小结Permalink

我们的这一套装备不算复杂,但很有效地解决了初学者面对 CTest 时可能遇到的各种千奇百怪的问题。不过,等到你迈过这个阶段时,还是有必要去研究 CTest 相关文档,做到深入理解和自定义也不困难。另外一点,项目变得大型了之后,每次 build 时都执行全部测试用例,那也是相当浪费生命的,此时你需要掌握 ctest 命令行的能力,以便能够有选择地执行部分用例,或者在 build 时禁用单元测试运行。

对于我们的 shortcut 来说,注释掉 apply_all_unit_tests 指令行就能达到禁用的目的了。

其它测试工具Permalink

CDashPermalink

CDash 是 CMake 提供的与 CTest 配套的工具,它是一个web 服务器,CTest 的结果可以传递给它之后在 web 页面上显示一个 Dashboard,可视化效果较佳。

不过你需要 docker 安装它并运行才行。或者你可以使用 CMake 提供的在线 CDash 服务:https://my.cdash.org

CMake Cookbook 中介绍了有关内容,请自行搜索并研究。

其它测试工具Permalink

Catch2

Google Test

Boost Test

这些都是有规模的测试工具


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK