CMake Tutorial (3.30-rc3版) 练习和点评

CMake Tutorial 练习和点评

Author: ChrisZZ
Time: 2024.06.16 23:37:00

CMake 官方文档提供了 CMake Tutorial, 目前最新版是 CMake-3.30-rc3, 有12个Step供用户练习。 CMake Tutorial 是从 CMake 3.16 版本开始能从官方网页找到, 并且每一版都有改进 Tutorial 内容。

作为有实际C/C++项目中大幅使用 CMake 构建经验的程序员, 我认为这套教程不适合入门, 有一定 CMake 经验后可以尝试练习, 但目前的教程内容主次不当, 在一些细节上过于精雕细琢, 而基本的软件包发布流程迟迟没有走通, 让人难以捉摸。 前面4个半 Step 还算可以接受, 后面7个半Step,大部分很糟糕, 不能说没用, 但是不合适。

Anyway,总的来说有一些收获, 也有很多不足; 而广为流传的一份中文 CMake 教程, https://www.hahack.com/codes/cmake, 我怀疑作者只是简单翻译了官方例子,没做太多改进, 毕竟很多内容相同; 而它在 github 上的 star 接近1500了,说明大部分卢瑟连官方 CMake Tutorial 都不知道去看, 只会看中文翻译的,真的挺唏嘘的。 简单记录一下吧。

介绍

这份教程使用的源代码是在 cmake-3.30.0-rc1-tutorial-source.zip 中, 每个步骤对应一个子目录, 提供了起点代码。 这份教程里的例子是逐步递进的, 也就是说每个 step 的初始代码, 都是前一个 step 的完整解决方案。

Step 1: 基础起点

本节共有12个TODO, 按顺序逐一实现即可, 包含了:

  • 创建项目
  • 指定项目版本号
  • 指定 C++ 标准
  • 创建可执行目标
  • 指定版本号
  • 通过 configure 文件使用版本号
  • C++ 代码中使用版本号

文档里也提到了一个细节,尽可能遵循:

cmake 命令, 应当使用小写而不是大写, 更不是混合大小写

执行构建, 运行:

cd Step1
cmake -S . -B build
cmake --build build
.\build\Debug\Tutorial.exe 42

个人认为版本号、 configure 文件, 初学者可以跳过, 实际项目中遇到使用的并没有很多。

Step 2: 创建库

本节共有14个TODO, 坦白说如果让初学者一次完全学会和写出, 还是有难度。

  • 使用了 add_library() 创建库
  • 使用了 add_subdirectory() 引入子目录
  • 使用了 target_include_directories() 设置包含目录
  • 使用了 target_link_libraries() 设置链接库
  • 使用了 option() 定义选项变量
  • 使用了 target_compile_defitions() 定义 target 专属(私有)宏
  • 在 C/C++ 代码中, 使用宏区分不同代码

个人认为小白用户第一次使用 CMake 时, 最多掌握本节50%左右的内容。

Step 3: 添加库的使用需求

仅仅设定头文件包含目录、 链接库, 比较粗放, 更好的控制, 则是设定如下内容:

  • target_compile_definitions()
  • target_compile_options()
  • target_include_directories()
  • target_link_directories()
  • target_link_options()
  • target_precompile_headers()
  • target_sources()
    上述常见函数, 除了设定具体的“需求”, 还设定了“传递属性”(transitive property),也就是 PUBLIC/PRIVATE/INTERFACE。 实际项目中, 很多初级算法工程师缺乏“传递属性”的概念, 依赖关系永远是平铺式的, 而不是树状的。 平铺式依赖关系的问题在于, 没有做抽象层级划分, 也就是抽象泄漏, 或者说, 一锅粥乱炖瞎几把搞。

这一节, 学到的第一个技巧是, 创建的 INTERFACE 库可以没有任何源文件, 然后能为它设定属性:

add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

由于没有任何源文件, 生成的 .sln 中并不会存在 tutorial_compiler_flags 的 project:

而在使用 tutorial_compiler_flags 这一链接库时, TODO 5~7 描述的有问题:

Link A to B

按我理解是把 A 链接到 B 上, 而官方给的答案则是把 B 链接到 A 上。

# 正确的描述: TODO 5: Link tutorial_compiler_flags to Tutorial
target_link_libraries(Tutorial PUBLIC MathFunctions tutorial_compiler_flags)

# 正确的描述: TODO 6: Link tutorial_compiler_flags to SqrtLibrary 
target_link_libraries(SqrtLibrary INTERFACE tutorial_compiler_flags)

# 正确的描述: TODO 7: Link tutorial_compiler_flags to MathFunctions
target_link_libraries(MathFunctions INTERFACE tutorial_compiler_flags)

在链接 tutorial_compiler_flags 时, 教程中的 TODO 题目是让我们分别链接到三个 target 上的:

  • SqrtLibrary
  • MathFunctions
  • Tutorial
    显然, 每次链接时的传递属性都是 PRIVATE, 否则只需在 SqrtLibrary 上链接即可。

此时,如果是使用 VS2022, 由于默认是C++14标准, 会影响我们观察到各个 target 上经由依赖关系传递过来的 C++ 标准, 可以尝试改为 C++20 后再观察。

这里还有另外一个坑: 如果改为由 SqrtLibrary 执行 PUBLIC 方式链接 tutorial_compiler_flags,在 build 阶段是很丝滑了, 但在 INSTALL 阶段, 需要让每个 target 对应生成的 xxx-config.cmake 中, 都标记出 C++11 标准吗? 应该也不需要, 也是通过传递方式即可。 但要传递, 就需要好用的包管理器。。。

Step 4: 生成器表达式

这一节是 generator expression 的讲解和练习, 需要先查看 https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html 的内容。

本节内容比较难, 初学者可能没法直接掌握, 包括 generator expression 的形式, 以及 BUILD_INTERFACE 的理解。

困难的原因是,没找到官方专门对于 configure/generate/build/install/export 阶段的讲解。

Step 5: 安装

这一节包含了两个 Exercise, 个人认为不相关, 应该拆分存放:

  • Exercise1:安装 MathFunctions 库, 以及 Tutorial 可执行程序
  • Exercise2: 执行单元测试

5.1 基本安装

教程里缺乏设定 CMAKE_INSTALL_PREFIX, 它默认值是 C:/Program Files (x86)/Tutorial。 我们将它做修改:

set(CMAKE_INSTALL_PREFIX "C:/pkgs/tutorial/1.0)

如果觉得上述设定过于武断, 想要先判断是否定义了 CMAKE_INSTALL_PREFIX 后再设定, 那么需要在 project() 之前判断:

if(DEFINED CMAKE_INSTALL_PREFIX)
  message(STATUS "[debug] CMAKE_INSTALL_PREFIX is defined: ${CMAKE_INSTALL_PREFIX}")
else()
  message(STATUS "[debug] CMAKE_INSTALL_PREFIX is not defined")
endif()
project(Tutorial VERSION 1.0)

因为 project() 命令会在没有设定 CMAKE_INSTALL_PREFIX 的前提下填入默认值:

  • c:/Program Files/${PROJECT_NAME} on Windows.
  • /usr/local on UNIX platforms.

然而实际运行, 发现 Windows 下默认的 CMAKE_INSTALL_PREFIX 是 c:/Program Files (x86)/Tutorial 而不是 c:/Program Files/Tutorial, 多了一个 (x86).

实际上最佳写法是这样的:先判断当前 CMAKE_INSTALL_PREFIX 是不是和默认值一样, 如果是,那就改掉:

project(Tutorial VERSION 1.0)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
  set(CMAKE_INSTALL_PREFIX "C:/pkg/Tutorial" CACHE PATH "..." FORCE)
endif()

还有一个问题: 安装的库, 没有体现出版本号来:

  • 每个 target 的安装路径都是 ${CMAKE_INSTALL_PREFIX}/Tutorial。 如果足够 modern cmake, install 阶段也应该能分别指定。
  • 使用者怎样区分不同版本的 mathfuncion 库?

使用者目前能在运行或编译阶段区分, 但是, 更好的做法应当能让使用者, 在 cmake configure 阶段, 在 CMakeLists.txt 里, 就能区分不同版本的 mathfuncions 库的版本。

进一步的问题: Tutorial 的版本号是1.0,但是 MathFunctions 库和 SqrtLibrary 库没有版本号。

5.2 安装到带有版本信息的目录

本来是是 Testing, 但是我觉得主题混乱, 改成安装到带有版本信息的目录了。

5.3 安装 export 相关文件并适配

本小结内容源自:

Step 11: Adding Export Configuration

export, 是说专门安装 xxx.cmake 文件, 本节我们安装的是:

  • MathFunctionsTargets.cmake
  • MathFunctionsTargets-debug.cmake
  • MathFunctionsConfig.cmake

其他内容见原版的 Step11.

由于我们选择安装到 带有版本信息的目录, 因此每次出现的 DESTINATION 参数,都要注意手动加一个子目录前缀。

5.4 导入安装的包

这一节是原版教程没有的。 原版教程的问题在于, 详略不当。 既然 find_package() 是 CMake 的一大特色, 那为什么在费了九牛二虎之力完成 “安装” 后, 从来都没使用过安装的包? 再简单也要展示下啊。

其他/补充/吐槽

Installing, 细节还得打磨:

  • 官方默认的 CMAKE_INSTALL_PREFIX 文档写错了, 而且 C 盘的那个目录, 用户是没有权限写入的好吗?
  • 在 project() 命令前和命令后, CMAKE_INSTALL_PREFIX 取值是不一样的,能说说吗?
  • 如果 CMAKE_INSTALL_PREFIX 取得了默认值, 就为用户自定义值, 这个能否在文档里说的更直白写,而不是丢一个链接, 点进去看不懂

Packing, 感觉没必要单独搞一个 cpack 命令。 tar cvf 就够了。

Testing, 感觉讲了又和没讲差不多, ctest 命令还算有点用, 但在 CMakeLists.txt 里写单元测试显然不实用, 是个糟粕。 正经项目显然用 C++ 的单元测试框架如 gtest, catch2。

Export, 前面提到了, 没必要单独放到一个章节说, 它就应当作为 install 的一部分, 只不过是中级的、 高级的, 而不是初级的部分。

给debug库设置postfix,同时编译安装debug和release库, 这是 Windows 平台经常用到的东西, 但可惜, 目前的 CMake Tutorial 写的很烂:

  • 设置的 postfix 值不对劲(不能区分库本是 d 结尾的情况)
  • Debug 和 Release 库同时编译的 Step12, 竟然不支持 Multi-Config, 而 Visual Studio 最广为人使用的 MSBuild 构建方式就是 Multi-Config, 这直接劝退用户了
  • 建议增加 Single-Config 和 Multi-Config 的专门的 Exercise, 毕竟 CMAKE_BUILD_TYPES 和 CMAKE_CONFIGURATION_TYPES 不是同一个东西

Configure_file(), 这个东西虽然说不难吧, 但是放到第一节, 不合适。 先学会走路再学习跳绳, 而不是一上来就让用户学习“双编”的花式跳绳技巧,然后再让用户慢慢学走路。

Generator Expression, 和 Configure_file() 类似, 有过之而无不及。

Usage Requirements, 这个名字就很不清晰, 改名为 transitive property 传递属性会更加直白。 这个也是阳春白雪的东西, 先让人吃口饭别饿死, 恢复体力了再跳绳吧。

Select Static or Shared Library, 这个需求个人觉得是来添乱的, 就应该做成两个包, 一个放到 xxx-static.zip, 一个放到 xxx-shared.zip, 不应该在同一个 build-tree 里生成。

Interface target, 一上来就让用户把 C++ 标准等 flags 放到 Interface target 中, 而不是介绍最广泛使用的 header-only 方式, 重点搞反了, 一点都不实用。

看到这里的读者, 你想必也很想让官方改一改吧? 我提交了3个 issue, 你可以关注; 或者再多提几个 issue, 甚至提交 MR 来改进:

posted @ 2024-06-16 23:35  ChrisZZ  阅读(133)  评论(0编辑  收藏  举报