第31章:打包与CPack:使用CPack生成安装包(.deb/.rpm/.dmg/.exe)

说实话,很多C++开发者写完代码就以为完事了。编译通过,测试跑完,嗯,可以交付了。但你真的把东西交给别人时,对方一句「怎么装?」就能让你瞬间破防。

我早年给客户交付一个Linux下的工具库,直接扔了个压缩包过去。结果对方折腾了半天,说「头文件放哪?库路径怎么配?」——从那以后,我再也不敢轻视打包这一步。

CMake 里自带的 CPack 就是干这个的。它能把你的构建产物收拾得整整齐齐,生成 .deb、.rpm、.dmg、.exe 安装包。说白了,就是让用户「双击一下,完事」。

31.1 CPack 到底在做什么?

CPack 是 CMake 的打包模块。它不负责编译,只负责把编译好的东西打包成安装包。它的工作流程其实很简单:

  1. 你通过 CMake 的 install() 命令定义好「哪些文件要装到哪」
  2. CPack 读取这些安装规则,调用对应的打包工具(如 dpkg、rpmbuild、NSIS)生成安装包

我习惯把 CPack 理解成一个「安装规则的翻译官」。你告诉它「这个可执行文件要放到 /usr/bin」,它就去跟 dpkg 说「给我打个 deb 包,里面有个文件放 /usr/bin」。

核心要点:CPack 本身不打包,它只是调用系统里的打包工具。所以你要生成 .deb,系统就得有 dpkg-deb;要生成 .rpm,就得有 rpmbuild。

下面这张图能帮你快速理解 CPack 在整个构建流程中的位置:

CPack 打包流程 CMake 构建 编译 + install() 规则 CPack 配置 CPACK_GENERATOR 等变量 打包工具 dpkg / rpmbuild / NSIS 生成安装包 .deb / .rpm / .dmg / .exe 安装目录结构 bin/ lib/ include/ share/ 关键:install() 命令定义「装什么、装哪」 CPack 负责「怎么打包、用什么格式」

31.2 最基础的 CPack 配置

先来一个最简单的例子。假设你有一个可执行文件 hello,想把它打成 deb 包。

你的 CMakeLists.txt 大概长这样:

cmake_minimum_required(VERSION 3.15)
project(Hello VERSION 1.0.0 LANGUAGES CXX)

add_executable(hello main.cpp)

# 安装规则:把 hello 装到 /usr/bin
install(TARGETS hello DESTINATION bin)

# CPack 配置
set(CPACK_PACKAGE_NAME "hello")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A simple hello world")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_GENERATOR "DEB")

include(CPack)

然后执行:

mkdir build && cd build
cmake ..
cpack

你就会在当前目录下得到一个 hello-1.0.0-Linux.deb 文件。用户拿到后直接 sudo dpkg -i hello-1.0.0-Linux.deb 就装好了。

小技巧:我习惯在 CMakeLists.txt 里把 include(CPack) 放在最后。因为 CPack 会读取之前定义的所有 CPACK_* 变量,顺序搞反了变量就无效了。

31.3 同时生成多种格式

有时候你需要同时提供 deb 和 rpm 包。比如你的软件要部署到 Ubuntu 和 CentOS 上。

很简单,把 CPACK_GENERATOR 设成多个值就行:

set(CPACK_GENERATOR "DEB;RPM")

然后运行 cpack,它会同时生成 .deb 和 .rpm 两个包。如果你在 macOS 上,还可以加上 DragNDrop 生成 .dmg:

set(CPACK_GENERATOR "DEB;RPM;DragNDrop")

Windows 下则是 NSISWIX

set(CPACK_GENERATOR "NSIS")

嗯,这里要注意:不同平台的打包工具需要提前装好。比如在 Ubuntu 上生成 rpm 包,你得先装 rpm 包。我当年就踩过这个坑——在 Ubuntu 上配了半天,发现 rpmbuild 没装,cpack 直接报错。

31.4 更完整的配置示例

实际项目中,打包配置会比刚才那个复杂不少。我给你看一个我常用的模板:

# 基础信息
set(CPACK_PACKAGE_NAME "mytool")
set(CPACK_PACKAGE_VERSION "2.1.0")
set(CPACK_PACKAGE_VERSION_MAJOR "2")
set(CPACK_PACKAGE_VERSION_MINOR "1")
set(CPACK_PACKAGE_VERSION_PATCH "0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "My awesome tool")
set(CPACK_PACKAGE_DESCRIPTION "A longer description of my tool")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_CONTACT "support@mycompany.com")
set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/icons/logo.png")

# 许可证
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")

# 依赖关系(deb 专用)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl-dev (>= 1.1), zlib1g (>= 1.2)")

# 架构
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
set(CPACK_RPM_PACKAGE_ARCHITECTURE "x86_64")

# 生成器
set(CPACK_GENERATOR "DEB;RPM")

include(CPack)

注意:依赖声明只在 deb/rpm 包中生效。如果你生成的是压缩包或 NSIS 安装包,依赖信息不会被检查。用户装完缺库,那是他自己的事——但你的软件口碑就砸了。

31.5 安装规则怎么写才规范?

CPack 的打包内容完全取决于你的 install() 命令。我见过不少新手把所有文件都扔到 /usr/local 下,结果打包出来结构乱七八糟。

一个规范的安装结构应该是这样的:

文件类型 目标路径 install() 示例
可执行文件 bin/ install(TARGETS myapp DESTINATION bin)
动态库 lib/ 或 lib/x86_64-linux-gnu/ install(TARGETS mylib LIBRARY DESTINATION lib)
头文件 include/ install(FILES mylib.h DESTINATION include)
配置文件 etc/ install(FILES myapp.conf DESTINATION etc)
文档 share/doc/myapp/ install(FILES README DESTINATION share/doc/myapp)

我个人习惯把安装规则单独写到一个 cmake/InstallRules.cmake 文件里,然后在主 CMakeLists.txt 中 include() 进来。这样项目大了以后,打包逻辑不会跟构建逻辑混在一起。

31.6 避坑指南:我踩过的几个坑

坑一:版本号不一致。 我曾经把 CPACK_PACKAGE_VERSION 写成 1.0,但 project() 里写的是 1.0.0。结果 deb 包的版本号是 1.0,rpm 包却是 1.0.0。用户升级时发现「版本号对不上」,折腾了半天。后来我统一用 project(... VERSION 1.0.0) 定义版本,然后 CPack 变量从 project 变量里取:

set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")

坑二:忘记设置许可证文件。 很多 Linux 发行版对 deb/rpm 包有严格的检查。如果你没设置 CPACK_RESOURCE_FILE_LICENSE,打包可能失败,或者生成的包被标记为「不符合规范」。我建议每个项目都放一个 LICENSE 文件,并在 CPack 里引用它。

坑三:跨平台打包的路径问题。 在 Windows 上用 NSIS 打包时,默认安装路径是 C:/Program Files/,但你的 install() 里写的 DESTINATION bin 会被解释成 C:/Program Files/你的项目/bin。这没问题。但如果你在 Linux 上写死了 /usr/bin,到 Windows 上就尴尬了。解决办法是用 CMake 的 GNUInstallDirs 模块:

include(GNUInstallDirs)
install(TARGETS myapp DESTINATION ${CMAKE_INSTALL_BINDIR})

这样 CMake 会自动根据平台选择正确的路径。Linux 下是 bin,Windows 下是 .(因为 NSIS 默认路径已经包含了项目名)。

31.7 自定义安装包行为

有时候你需要在安装前后执行一些脚本。比如安装前检查依赖,安装后启动服务。CPack 支持通过 CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA 添加自定义脚本:

set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
    "${CMAKE_SOURCE_DIR}/packaging/debian/preinst"
    "${CMAKE_SOURCE_DIR}/packaging/debian/postinst"
    "${CMAKE_SOURCE_DIR}/packaging/debian/prerm"
    "${CMAKE_SOURCE_DIR}/packaging/debian/postrm"
)

这些脚本会被直接塞进 deb 包的 DEBIAN/ 目录下。rpm 包也有类似的机制,用 CPACK_RPM_PRE_INSTALL_SCRIPT_FILE 等变量。

我记得有一次给一个数据库中间件做打包,需要在安装后自动创建数据目录和日志目录。就是靠 postinst 脚本搞定的。用户装完包,目录自动建好,服务自动启动——体验感直接拉满。

31.8 测试你的安装包

包打好了,怎么验证?我一般会做三件事:

  1. 检查包内容: dpkg -c hello-1.0.0-Linux.deb 看看文件列表对不对
  2. 安装测试: 在干净的 Docker 容器里装一遍,看依赖是否完整
  3. 卸载测试: 装完再卸,看有没有残留文件

尤其是第三点,很多人容易忽略。我曾经打过一个包,安装时创建了 /var/log/myapp 目录,但卸载脚本没删它。结果用户重装时发现日志目录还在,权限不对,服务起不来。后来我在 postrm 脚本里加了 rm -rf /var/log/myapp,才彻底解决。

建议:把打包和测试流程写进 CI/CD 里。每次提交代码自动打包、自动安装测试。这样能第一时间发现打包配置的问题,不用等到交付时手忙脚乱。

好了,关于 CPack 打包的核心内容就这些。从配置生成器、定义安装规则,到处理依赖和自定义脚本,每一步都有讲究。你只要把 install()CPACK_* 变量搞明白,剩下的就是调用 cpack 命令的事。说白了,CPack 就是个「配置驱动」的工具——你把规则写清楚,它帮你把包打好。


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