CMake 秘籍(八)(4)https://developer.aliyun.com/article/1525077
实现多步骤测试
在src/testdir/Makefile
中的目标表明 Vim 代码以多步骤测试运行:首先,vim
可执行文件处理一个脚本并生成一个输出文件,然后在第二步中,输出文件与参考文件进行比较,如果这些文件没有差异,则测试成功。临时文件随后在第三步中被删除。这可能无法以可移植的方式适应单个add_test
命令,因为add_test
只能执行一个命令。一个解决方案是将测试步骤定义在一个 Python 脚本中,并用一些参数执行该 Python 脚本。我们将在这里介绍的另一种替代方案也是跨平台的,即将测试步骤定义在一个单独的 CMake 脚本中,并从add_test
执行该脚本。我们将在src/testdir/test.cmake
中定义测试步骤:
function(execute_test _vim_executable _working_dir _test_script) # generates test.out execute_process( COMMAND ${_vim_executable} -f -u unix.vim -U NONE --noplugin --not-a-term -s dotest.in ${_test_script}.in WORKING_DIRECTORY ${_working_dir} ) # compares test*.ok and test.out execute_process( COMMAND ${CMAKE_COMMAND} -E compare_files ${_test_script}.ok test.out WORKING_DIRECTORY ${_working_dir} RESULT_VARIABLE files_differ OUTPUT_QUIET ERROR_QUIET ) # removes leftovers file(REMOVE ${_working_dir}/Xdotest) # we let the test fail if the files differ if(files_differ) message(SEND_ERROR "test ${_test_script} failed") endif() endfunction() execute_test(${VIM_EXECUTABLE} ${WORKING_DIR} ${TEST_SCRIPT})
再次,我们选择函数而非宏来确保变量不会逃逸函数作用域。我们将处理这个脚本,该脚本将调用execute_test
函数。然而,我们必须确保从外部定义了${VIM_EXECUTABLE}
、${WORKING_DIR}
和${TEST_SCRIPT}
。这些在src/testdir/CMakeLists.txt
中定义:
add_test( NAME test1 COMMAND ${CMAKE_COMMAND} -D VIM_EXECUTABLE=$<TARGET_FILE:vim> -D WORKING_DIR=${CMAKE_CURRENT_LIST_DIR} -D TEST_SCRIPT=test1 -P ${CMAKE_CURRENT_LIST_DIR}/test.cmake WORKING_DIRECTORY ${PROJECT_BINARY_DIR} )
Vim 项目有许多测试,但在本例中,我们只移植了一个(test1)作为概念验证。
测试建议
我们至少可以给出两个关于移植测试的建议。首先,确保测试不会总是报告成功,如果代码被破坏或参考数据被更改,请验证测试是否失败。其次,为测试添加COST
估计,以便在并行运行时,较长的测试首先启动,以最小化总测试时间(参见第四章,创建和运行测试,第 8 个配方,并行运行测试)。
移植安装目标
我们现在可以配置、编译、链接和测试代码,但我们缺少安装目标,我们将在本节中添加它。
这是 Autotools 构建和安装代码的方法:
$ ./configure --prefix=/some/install/path $ make $ make install
这就是 CMake 的方式:
$ mkdir -p build $ cd build $ cmake -D CMAKE_INSTALL_PREFIX=/some/install/path .. $ cmake --build . $ cmake --build . --target install
要添加安装目标,我们需在src/CMakeLists.txt
中添加以下代码片段:
install( TARGETS vim RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} )
在本例中,我们只安装了可执行文件。Vim 项目在安装二进制文件的同时安装了大量文件(符号链接和文档文件)。为了使本节易于理解,我们没有在本例迁移中安装所有其他文件。对于你自己的项目,你应该验证安装步骤的结果是否与遗留构建框架的安装目标相匹配。
进一步的步骤
成功移植到 CMake 后,下一步应该是进一步限定目标和变量的范围:考虑将选项、目标和变量移动到它们被使用和修改的位置附近。避免全局变量,因为它们会强制 CMake 命令的顺序,而这个顺序可能不明显,会导致脆弱的 CMake 代码。一种强制分离变量范围的方法是将大型项目划分为 CMake 项目,这些项目使用超级构建模式(参见第八章,超级构建模式)。考虑将大型CMakeLists.txt
文件拆分为较小的模块。
接下来的步骤可能是在其他平台和操作系统上测试配置和编译,以便使 CMake 代码更加通用和防弹,并使其更具可移植性。
最后,在将项目迁移到新的构建框架时,开发社区也需要适应它。通过培训、文档和代码审查帮助你的同事。在将代码移植到 CMake 时,最难的部分可能是改变人的习惯。
转换项目到 CMake 时的总结和常见陷阱
让我们总结一下本章我们取得了哪些成就以及我们学到了什么。
代码变更总结
在本章中,我们讨论了如何将项目移植到 CMake。我们以 Vim 项目为例,并添加了以下文件:
. ├── CMakeLists.txt └── src ├── autogenerate.cmake ├── CMakeLists.txt ├── config.h.cmake.in ├── libvterm │ └── CMakeLists.txt ├── pathdef.c.in └── testdir ├── CMakeLists.txt └── test.cmake
可以在线浏览变更:github.com/dev-cafe/vim/compare/b476cb7...cmake-support
。
这是一个不完整的 CMake 移植概念证明,我们省略了许多选项和调整以简化,并试图专注于最突出的特性和步骤。
常见陷阱
我们希望通过指出转向 CMake 时的一些常见陷阱来结束这次讨论。
- 全局变量是代码异味:这在任何编程语言中都是如此,CMake 也不例外。跨越 CMake 文件的变量,特别是从叶子到父级
CMakeLists.txt
文件“向上”传递的变量,表明代码存在问题。通常有更好的方式来传递依赖。理想情况下,依赖应该通过目标来导入。不要将一系列库组合成一个变量并在文件之间传递该变量,而是将库一个接一个地链接到它们定义的位置附近。不要将源文件组合成变量,而是使用target_sources
添加源文件。在链接库时,如果可用,使用导入的目标而不是变量。 - 最小化顺序影响:CMake 不是一种声明式语言,但我们也不应该用命令式范式来处理它。强制严格顺序的 CMake 源码往往比较脆弱。这也与变量的讨论有关(见前一段)。某些语句和模块的顺序是必要的,但为了得到稳健的 CMake 框架,我们应该避免不必要的顺序强制。使用
target_sources
、target_compile_definitions
、target_include_directories
和target_link_libraries
。避免全局范围的语句,如add_definitions
、include_directories
和link_libraries
。避免全局定义编译标志。如果可能,为每个目标定义编译标志。 - 不要将生成的文件放置在构建目录之外:强烈建议永远不要将生成的文件放置在构建目录之外。这样做的原因是,生成的文件通常依赖于所选的选项、编译器或构建类型,而将文件写入源代码树中,我们放弃了维护多个具有相同源代码的构建的可能性,并且使构建步骤的可重复性变得复杂。
- 优先使用函数而非宏:它们具有不同的作用域,函数作用域是有限的。所有变量修改都需要明确标记,这也向读者表明了变量重定义。当你必须使用宏时使用,但如果你能使用函数,则优先使用函数。
- 避免 shell 命令:它们可能不兼容其他平台(如 Windows)。优先使用 CMake 的等效命令。如果没有可用的 CMake 等效命令,考虑调用 Python 脚本。
- 在 Fortran 项目中,注意后缀大小写:需要预处理的 Fortran 源文件应具有大写的
.F90
后缀。不需要预处理的源文件应具有小写的.f90
后缀。 - 避免显式路径:无论是在定义目标时还是在引用文件时都是如此。使用
CMAKE_CURRENT_LIST_DIR
来引用当前路径。这样做的好处是,当你移动或重命名目录时,它仍然有效。 - 模块包含不应是函数调用:将 CMake 代码模块化是一个好的策略,但包含模块理想情况下不应执行 CMake 代码。相反,应将 CMake 代码封装到函数和宏中,并在包含模块后显式调用这些函数和宏。这可以防止无意中多次包含模块时产生的不良后果,并使执行 CMake 代码模块的动作对读者更加明确。