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_DEPENDSCPACK_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的对比总结

我画了一张图,帮你快速对比这两个生成器的核心差异:

DEB vs RPM 生成器核心对比 DEB 生成器 • 依赖变量:CPACK_DEBIAN_PACKAGE_DEPENDS • 版本语法:libssl-dev (>= 1.1.0) • 脚本变量:CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA • 包名规范:全小写+短横线 • 无epoch机制 • 脚本不带后缀 典型场景 Ubuntu / Debian / Deepin / UOS 桌面应用、服务器工具、库文件 RPM 生成器 • 依赖变量:CPACK_RPM_PACKAGE_REQUIRES • 版本语法:openssl-devel >= 1.1.0 • 脚本变量:CPACK_RPM_*_SCRIPT_FILE • 包名规范:通常带-devel后缀 • 支持epoch前缀 • 脚本执行顺序不同 典型场景 CentOS / RHEL / Fedora / openEuler 企业级服务器、云原生基础设施

19.8 实战建议:跨发行版打包策略

如果你需要同时支持DEB和RPM,我建议你这样做:

  1. 用条件判断区分生成器:在CMakeLists.txt里判断CPACK_GENERATOR,分别设置不同的依赖变量。
  2. 维护两套脚本:DEB和RPM的脚本虽然逻辑相似,但路径和变量名不同,分开维护更清晰。
  3. 用CI自动测试:在Docker容器里分别安装生成的deb和rpm包,验证依赖是否完整。我个人的CI流水线里会跑Ubuntu 20.04、22.04和CentOS 7、8四个容器。
  4. 注意包名冲突:同一个库在不同发行版上包名可能不同。我建议你维护一个映射表,用CMake的if语句做分发。

一个小技巧:如果你不确定某个依赖在目标系统上叫什么名字,可以用apt-cache searchyum provides来反向查找。比如你想找libssl.so属于哪个包,在Ubuntu上跑apt-file search libssl.so,在CentOS上跑yum whatprovides libssl.so

好了,DEB和RPM生成器的核心内容就这些。说白了,Linux打包没有想象中那么玄乎,只要把依赖声明写对、维护者脚本写稳,剩下的CPack都帮你搞定了。我当年花了整整一周才把这些细节摸透,你现在看完这一章,至少能省下五天时间。


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