19、CPack生成器详解(下):DEB与RPM生成器配置、依赖声明与维护者脚本
好,咱们接着聊CPack生成器。上一章我把NSIS和IFW这两个安装向导型生成器讲透了,这一章咱们聚焦在Linux生态下两个最要命的打包格式——DEB和RPM。
说实话,我在做跨平台C++库分发的时候,最头疼的就是Linux打包。你想想看,同一个库,Ubuntu用户要.deb,CentOS用户要.rpm,而且依赖关系还各不相同。我早期吃过不少亏,比如打出来的deb包在用户机器上一安装,直接报依赖缺失,用户一脸懵。后来我才彻底搞明白CPack里这些配置项到底该怎么玩。
19.1 DEB生成器:从零开始打一个干净的deb包
DEB生成器,说白了就是帮我们把CMake项目变成.deb文件。你不需要手动写control文件,CPack替你干了大部分活。
先看一个最基础的配置:
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_NAME "my-awesome-lib")
set(CPACK_DEBIAN_PACKAGE_VERSION "1.2.3")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Zhang San <zhangsan@example.com>")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "A powerful C++ library for data processing")
嗯,这里要注意:CPACK_DEBIAN_PACKAGE_NAME这个变量,我建议你用全小写字母加短横线,不要用下划线。为什么?因为Debian社区的命名规范就是这样,你用下划线虽然也能打包成功,但lintian检查会报warning。我当年就因为这事被CI流水线卡了好几次。
19.2 依赖声明:CPACK_DEBIAN_PACKAGE_DEPENDS
依赖声明是deb打包里最容易翻车的地方。先看语法:
set(CPACK_DEBIAN_PACKAGE_DEPENDS
"libssl-dev (>= 1.1.0), libcurl4-openssl-dev, zlib1g-dev (>= 1.2.8)"
)
这里有几个坑,我一个个说。
第一,版本号写法。Debian的版本比较符必须用括号括起来,而且括号和版本号之间要有空格。写成libssl-dev(>=1.1.0)是不行的,必须写成libssl-dev (>= 1.1.0)。我刚开始写的时候漏了空格,结果生成的control文件直接语法错误。
第二,多依赖用逗号分隔。每个依赖项之间用逗号加空格。注意最后一个依赖后面不要加逗号,虽然加了也不报错,但lintian会提示。
第三,依赖的包名必须准确。你得知道目标系统上真正的包名叫什么。比如你想依赖OpenSSL,在Ubuntu上包名是libssl-dev,在Debian上也是libssl-dev,但在一些老版本系统上可能叫openssl-dev。我建议你直接去packages.ubuntu.com上查一下,别凭记忆写。
核心要点:依赖声明不是写你项目里用了什么库,而是写用户安装你的包之前,系统里必须已经装了什么包。这两个概念有本质区别。
19.3 维护者脚本:preinst、postinst、prerm、postrm
维护者脚本是deb包的高级玩法。说白了,就是在安装、升级、卸载的不同阶段,自动执行一些脚本。
CPack支持四种脚本:
| 脚本名称 | 触发时机 | 典型用途 |
|---|---|---|
| preinst | 安装前 | 停止服务、备份配置文件 |
| postinst | 安装后 | 启动服务、创建用户、设置权限 |
| prerm | 卸载前 | 停止服务、清理临时文件 |
| postrm | 卸载后 | 删除用户、清理日志 |
配置方法很简单:
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_SOURCE_DIR}/packaging/debian/postinst"
"${CMAKE_SOURCE_DIR}/packaging/debian/prerm"
)
这些脚本必须是可执行文件,而且不能有.sh后缀。我一开始没注意,写了个postinst.sh,结果CPack直接报错说找不到文件。后来才发现它只认不带后缀的脚本名。
个人经验:postinst脚本里我通常会做两件事:一是用ldconfig更新动态链接器缓存,二是用systemctl daemon-reload刷新systemd配置。这两个操作能避免很多运行时问题。
19.4 RPM生成器:Red Hat家族的打包之道
RPM生成器和DEB大同小异,但细节上有不少差异。先看基础配置:
set(CPACK_GENERATOR "RPM")
set(CPACK_RPM_PACKAGE_NAME "my-awesome-lib")
set(CPACK_RPM_PACKAGE_VERSION "1.2.3")
set(CPACK_RPM_PACKAGE_RELEASE "1")
set(CPACK_RPM_PACKAGE_ARCHITECTURE "x86_64")
set(CPACK_RPM_PACKAGE_VENDOR "My Company")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
注意CPACK_RPM_PACKAGE_RELEASE这个变量,它对应RPM包名里的-1部分。每次打包时如果代码没变但打包脚本变了,你应该递增这个数字,而不是改版本号。这是RPM社区的惯例。
19.5 RPM依赖声明
RPM的依赖声明语法和DEB不太一样:
set(CPACK_RPM_PACKAGE_REQUIRES
"openssl-devel >= 1:1.1.0, libcurl-devel, zlib-devel >= 1.2.8"
)
你看,RPM用的是REQUIRES而不是DEPENDS。版本比较符也不需要用括号括起来,直接写就行。但要注意RPM的版本号里可以有epoch前缀,比如1:1.1.0,这个1:就是epoch。DEB里没有这个机制。
我遇到过最坑的一件事:同一个库,在Ubuntu上叫libssl-dev,在CentOS上叫openssl-devel。如果你用CPACK_DEBIAN_PACKAGE_DEPENDS和CPACK_RPM_PACKAGE_REQUIRES分别声明,就得写两套包名。我后来干脆写了个CMake函数,根据生成器自动切换依赖名。
19.6 RPM的维护者脚本
RPM的维护者脚本和DEB类似,但变量名不同:
set(CPACK_RPM_PRE_INSTALL_SCRIPT_FILE "${CMAKE_SOURCE_DIR}/packaging/rpm/preinst")
set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${CMAKE_SOURCE_DIR}/packaging/rpm/postinst")
set(CPACK_RPM_PRE_UNINSTALL_SCRIPT_FILE "${CMAKE_SOURCE_DIR}/packaging/rpm/prerm")
set(CPACK_RPM_POST_UNINSTALL_SCRIPT_FILE "${CMAKE_SOURCE_DIR}/packaging/rpm/postrm")
这里有个细节:RPM的脚本执行时机和DEB不完全一样。比如在升级场景下,DEB是先执行新包的preinst,再执行旧包的prerm;而RPM的顺序可能不同。我建议你在写脚本时,尽量做到幂等——也就是不管执行多少次,结果都一样。这样能避免升级时的意外行为。
注意:维护者脚本里不要做耗时操作,比如下载文件、编译代码。包管理器在执行脚本时通常有超时限制,一旦超时,安装过程就会失败。我曾经在postinst里写了个git clone,结果在低配机器上直接超时,整个安装回滚了。
19.7 DEB与RPM的对比总结
我画了一张图,帮你快速对比这两个生成器的核心差异:
19.8 实战建议:跨发行版打包策略
如果你需要同时支持DEB和RPM,我建议你这样做:
- 用条件判断区分生成器:在CMakeLists.txt里判断
CPACK_GENERATOR,分别设置不同的依赖变量。 - 维护两套脚本:DEB和RPM的脚本虽然逻辑相似,但路径和变量名不同,分开维护更清晰。
- 用CI自动测试:在Docker容器里分别安装生成的deb和rpm包,验证依赖是否完整。我个人的CI流水线里会跑Ubuntu 20.04、22.04和CentOS 7、8四个容器。
- 注意包名冲突:同一个库在不同发行版上包名可能不同。我建议你维护一个映射表,用CMake的
if语句做分发。
一个小技巧:如果你不确定某个依赖在目标系统上叫什么名字,可以用apt-cache search或yum provides来反向查找。比如你想找libssl.so属于哪个包,在Ubuntu上跑apt-file search libssl.so,在CentOS上跑yum whatprovides libssl.so。
好了,DEB和RPM生成器的核心内容就这些。说白了,Linux打包没有想象中那么玄乎,只要把依赖声明写对、维护者脚本写稳,剩下的CPack都帮你搞定了。我当年花了整整一周才把这些细节摸透,你现在看完这一章,至少能省下五天时间。
公众号:蓝海资料掘金营,微信deep3321