按:
这个系列与 cmake 有关的记录也好,笔记也好,成书也好,目前暂且归属在 cmake-hello 标记之下。
源码请访问:
https://github.com/hedzr/study-cmake
请注意源码仍在跟随我的笔记内容的迭代而更新中。
我们做了辣么多的准备工作了,早就想要介入具体项目的编写工作了。
z11-m1
以前我们做了 z01 到 z04 等一组以传统 cmake 风格编写的具体项目,现在我们将要开始名为 z11-m1 的新的子项目,它包含至少一个 library Target,以及使用这个 library 的相应的 executables。
首先我们总览一下 z11-m1 的目录结构:
在截图里没有展现上级目录(即 study-cmake 的根目录),因为现在我们只关心下级子目录中的这些子项目应该怎么布局。
你已经看到了我们的 z11-m1
包含 app, app-auto, libs/sm-lib 这几个子目录,并且全都有一个 CMakeLists.txt
负责添加和管理各自的下级子目录。
z11-m1/CMakeLists.txt
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-lib
现在我们要编写一个 library,且令其具有 Modern CMake 的风格,不但时一个具有自我意识的 Target,而且能够提供自己的定位信息,可以很容易地嵌入到一个 cmake hosted 的构建主机环境中。
定义 project
首先还是看看 CMakeLists.txt
的内容,其开始部分如下:
1
2
cmake_minimum_required(VERSION 3.5)
project(sm-lib VERSION 1.0.0 LANGUAGES CXX)
查找 package (依赖库)
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 Target
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-lib
。include
之所以不带有 CMAKE_INSTALL_PREFIX 前缀,是为了让 cmake 能够根据实际情况解决前缀,即所谓的 Relocatable Package。
-
加入测试节
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 做单元测试的。
加入安装方案
现在加入安装脚本用于将 sm-lib 安装到目标位置。
首先我们要了解的是 CMAKE_INSTALL_PREFIX 是所谓的 install-tree 目标位置。一个 Library 的公开头文件将被安装到 <prefix>/include
,库文件(.a)将被安装到 <prefix>/lib
(对于不同的发行版不同的处理器架构,也可能安装到 <prefix>/lib64
等等位置)。另外如果你想要,还可以提供 man 手册,附加文件等等。
install 指令
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> [...])
它们的精确语法已经使用细节可以查看官网文档。
我们的实例
我们为 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.cmake
在 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 相关 - CTest
利用 CTest 进行单元测试
- ctest(1) - https://cmake.org/cmake/help/latest/manual/ctest.1.html
- CTest Module - https://cmake.org/cmake/help/latest/module/CTest.html#module:CTest
- Testing With CTest
CTest 是 CMake 的内建工具。
你可以在命令行直接执行 ctest --help
。但在一个项目工程中,更便利的方法是:
1
cmake --build build/ --target test
简介
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 中的 shortcut
在 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。
背景
这一切有赖于这些预先配置的内容:
cmake/prerequisites.cmake
在 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.cmake
在 utils.cmake 中我们定义了两个宏指令:add_unit_test
和 apply_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.txt
在 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 去执行,从而达到了自动执行单元测试用例的效果。
小结
我们的这一套装备不算复杂,但很有效地解决了初学者面对 CTest 时可能遇到的各种千奇百怪的问题。不过,等到你迈过这个阶段时,还是有必要去研究 CTest 相关文档,做到深入理解和自定义也不困难。另外一点,项目变得大型了之后,每次 build 时都执行全部测试用例,那也是相当浪费生命的,此时你需要掌握 ctest 命令行的能力,以便能够有选择地执行部分用例,或者在 build 时禁用单元测试运行。
对于我们的 shortcut 来说,注释掉 apply_all_unit_tests 指令行就能达到禁用的目的了。
其它测试工具
CDash
CDash 是 CMake 提供的与 CTest 配套的工具,它是一个web 服务器,CTest 的结果可以传递给它之后在 web 页面上显示一个 Dashboard,可视化效果较佳。
不过你需要 docker 安装它并运行才行。或者你可以使用 CMake 提供的在线 CDash 服务:https://my.cdash.org。
CMake Cookbook 中介绍了有关内容,请自行搜索并研究。
其它测试工具
Catch2
Google Test
Boost Test
这些都是有规模的测试工具
🔚
留下评论