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 来改进: