28、与 CPack 集成:通过 packagePresets 自动化打包流程。

说实话,CMake Presets 这个功能刚出来的时候,我第一反应是:嗯,又是个配置规范化的东西。但真正让我觉得「这玩意儿真香」的,是它和 CPack 的集成。你想想看,以前我们打包项目,要么写一堆 shell 脚本,要么在 CI 里硬编码各种参数。每次换平台、换编译器,打包脚本就得跟着改,烦不烦?

packagePresets 的出现,说白了就是把打包这件事也纳入到 Presets 的统一管理里。你不需要再记住 CPack 那一堆长长的命令行参数,也不用担心团队成员用的打包参数不一致。一切都在 CMakePresets.json 里定义好,清晰、可复现。

packagePresets 的基本结构

packagePresets 的写法和 configurePresets、buildPresets 很像。它也是 CMakePresets.json 里的一个顶级数组。我习惯把它放在 buildPresets 后面,因为打包通常是构建完成后的下一步。

{
  "version": 6,
  "configurePresets": [...],
  "buildPresets": [...],
  "packagePresets": [
    {
      "name": "default",
      "hidden": false,
      "description": "默认打包配置",
      "generator": "TGZ",
      "configurations": ["Release"],
      "installDir": "$env{HOME}/packages",
      "packageName": "MyApp-${presetName}",
      "packageVersion": "1.0.0",
      "variables": {
        "CPACK_PACKAGE_CONTACT": "dev@example.com",
        "CPACK_PACKAGE_DESCRIPTION_SUMMARY": "My Application"
      }
    }
  ]
}

这里有几个关键字段,我简单说一下:

  • generator:指定打包格式。TGZ 是 tar.gz,还有 DEB、RPM、NSIS 等。具体用哪个,看你目标平台。
  • configurations:指定构建配置。Release、Debug 都行。我一般只打包 Release,毕竟没人想用 Debug 版本上线。
  • installDir:打包产物的输出目录。注意,这个目录是相对于构建目录的。
  • variables:这里可以设置 CPack 的变量。说白了,就是你在 CPackConfig.cmake 里写的那堆东西,现在可以直接在 Presets 里定义。
小提示:packagePresets 里的变量会覆盖 CPackConfig.cmake 里的同名变量。所以如果你在 Presets 里定义了 CPACK_PACKAGE_VERSION,那 CMakeLists.txt 里 set 的版本号就不生效了。我个人建议,版本号这种经常变的东西,还是放在 Presets 里管理比较方便。

如何触发打包

定义好 packagePresets 之后,怎么用呢?很简单,一条命令搞定:

cmake --build --preset default
cmake --package --preset default

嗯,你没看错。CMake 3.23 之后引入了 --package 选项,专门用来触发打包。它会自动读取当前构建目录下的 CMakePresets.json,找到对应的 packagePreset,然后调用 CPack。

我记得第一次用这个命令的时候,还特意去查了文档,生怕它找不到 Preset。结果一试,发现 CMake 会自动关联构建目录和 Preset 文件。说白了,只要你用 --build --preset 构建过,构建目录里就会生成一个 CMakePresets.json 的副本,--package --preset 直接读它就行。

多平台打包的实战经验

在实际项目中,我们经常需要为不同平台生成不同的包。比如 Linux 下打 DEB,Windows 下打 NSIS 安装包。这时候 packagePresets 的继承机制就派上用场了。

{
  "packagePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "TGZ",
      "configurations": ["Release"],
      "variables": {
        "CPACK_PACKAGE_VENDOR": "MyCompany"
      }
    },
    {
      "name": "linux-deb",
      "inherits": ["base"],
      "generator": "DEB",
      "variables": {
        "CPACK_DEBIAN_PACKAGE_MAINTAINER": "dev@example.com"
      }
    },
    {
      "name": "win-installer",
      "inherits": ["base"],
      "generator": "NSIS",
      "variables": {
        "CPACK_NSIS_DISPLAY_NAME": "MyApp Installer"
      }
    }
  ]
}

你看,我定义了一个隐藏的 base Preset,里面放通用的配置。然后 linux-deb 和 win-installer 分别继承它,只覆盖平台相关的部分。这样既减少了重复代码,又保证了配置的一致性。

注意:继承时,如果子 Preset 和父 Preset 定义了同一个变量,子 Preset 的会覆盖父 Preset 的。但如果是数组类型的变量(比如 configurations),子 Preset 会完全替换父 Preset 的值,而不是合并。我曾经在这里踩过坑,以为会合并,结果打出来的包少了 Debug 符号。

与 CI/CD 的集成

packagePresets 最大的价值,其实是在 CI/CD 流水线里。你想想看,以前我们在 Jenkins 或 GitLab CI 里写打包脚本,每个项目一套,维护起来头大。现在好了,只要在 CMakePresets.json 里定义好,CI 里只需要一行命令:

cmake --package --preset linux-deb

而且,因为 Preset 是版本控制的,所以每次代码变更,打包配置也跟着变。不会出现「CI 脚本改了,但代码没更新」这种尴尬情况。

我曾经在一个跨平台项目里,用 packagePresets 同时管理了 Linux 的 DEB、RPM,Windows 的 NSIS,还有 macOS 的 DMG。每个平台一个 Preset,CI 里根据操作系统选择对应的 Preset。整个打包流程从原来的 200 多行 shell 脚本,缩减到不到 10 行。嗯,那感觉,就像把一堆乱麻理成了整齐的线团。

SVG 流程图:packagePresets 的核心逻辑

packagePresets 核心逻辑流程图 CMakePresets.json packagePresets 数组 linux-deb Generator: DEB win-installer Generator: NSIS macos-dmg Generator: DragNDrop cmake --package --preset <preset-name> 自动读取构建目录中的 CMakePresets.json 输出:.deb / .exe / .dmg 等安装包

一些实用技巧

最后,分享几个我在实际项目中积累的小技巧:

  • 版本号统一管理:我习惯在 packagePresets 里定义 packageVersion,而不是在 CMakeLists.txt 里写死。这样换版本时,只需要改 Presets 文件,不用动 CMake 代码。
  • 条件打包:如果你需要根据条件选择不同的打包配置,可以用 condition 字段。比如只在 Release 模式下打 DEB 包,Debug 模式打 TGZ 包。
  • 测试打包产物:每次打包后,我建议在 CI 里加一个步骤,验证包的内容是否正确。比如检查 DEB 包里的二进制文件是否是可执行的,或者 NSIS 安装包是否能静默安装。我曾经遇到过打包后少了动态库的情况,还好在 CI 里发现了,没发到客户手里。
核心要点:packagePresets 不是 CPack 的替代品,而是 CPack 的配置管理器。它把分散在 CMakeLists.txt、CPackConfig.cmake、CI 脚本里的打包参数,统一到一个地方管理。说白了,就是让打包这件事变得可预测、可复现、可共享。

好了,关于 packagePresets 的内容就这些。如果你在项目中遇到了打包相关的坑,欢迎随时交流。毕竟,打包这件事,看着简单,但真正做好,还是需要不少经验的。

公众号:蓝海资料掘金营,微信 deep3321