第31章:打包与CPack:使用CPack生成安装包(.deb/.rpm/.dmg/.exe)
说实话,很多C++开发者写完代码就以为完事了。编译通过,测试跑完,嗯,可以交付了。但你真的把东西交给别人时,对方一句「怎么装?」就能让你瞬间破防。
我早年给客户交付一个Linux下的工具库,直接扔了个压缩包过去。结果对方折腾了半天,说「头文件放哪?库路径怎么配?」——从那以后,我再也不敢轻视打包这一步。
CMake 里自带的 CPack 就是干这个的。它能把你的构建产物收拾得整整齐齐,生成 .deb、.rpm、.dmg、.exe 安装包。说白了,就是让用户「双击一下,完事」。
31.1 CPack 到底在做什么?
CPack 是 CMake 的打包模块。它不负责编译,只负责把编译好的东西打包成安装包。它的工作流程其实很简单:
- 你通过 CMake 的
install()命令定义好「哪些文件要装到哪」 - CPack 读取这些安装规则,调用对应的打包工具(如 dpkg、rpmbuild、NSIS)生成安装包
我习惯把 CPack 理解成一个「安装规则的翻译官」。你告诉它「这个可执行文件要放到 /usr/bin」,它就去跟 dpkg 说「给我打个 deb 包,里面有个文件放 /usr/bin」。
核心要点:CPack 本身不打包,它只是调用系统里的打包工具。所以你要生成 .deb,系统就得有 dpkg-deb;要生成 .rpm,就得有 rpmbuild。
下面这张图能帮你快速理解 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 下则是 NSIS 或 WIX:
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 测试你的安装包
包打好了,怎么验证?我一般会做三件事:
- 检查包内容:
dpkg -c hello-1.0.0-Linux.deb看看文件列表对不对 - 安装测试: 在干净的 Docker 容器里装一遍,看依赖是否完整
- 卸载测试: 装完再卸,看有没有残留文件
尤其是第三点,很多人容易忽略。我曾经打过一个包,安装时创建了 /var/log/myapp 目录,但卸载脚本没删它。结果用户重装时发现日志目录还在,权限不对,服务起不来。后来我在 postrm 脚本里加了 rm -rf /var/log/myapp,才彻底解决。
建议:把打包和测试流程写进 CI/CD 里。每次提交代码自动打包、自动安装测试。这样能第一时间发现打包配置的问题,不用等到交付时手忙脚乱。
好了,关于 CPack 打包的核心内容就这些。从配置生成器、定义安装规则,到处理依赖和自定义脚本,每一步都有讲究。你只要把 install() 和 CPACK_* 变量搞明白,剩下的就是调用 cpack 命令的事。说白了,CPack 就是个「配置驱动」的工具——你把规则写清楚,它帮你把包打好。
公众号:蓝海资料掘金营,微信deep3321