ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RPM与DEB打包完全指南:格式差异、实战示例与避坑技巧

RPM与DEB打包完全指南:格式差异、实战示例与避坑技巧

简介:RPM与DEB是Linux世界两大主流软件包管理体系,分别广泛用于RHEL/Fedora/CentOS和Debian/Ubuntu等发行版。文档系统梳理了两类软件包的完整打包流程,从目录结构搭建、control文件及spec文件编写,到preinst/postinst/prerm/postrm等各阶段脚本的职责,再到打包命令、安装升级与卸载操作,适合Linux运维、系统管理员及需要定制内部软件的开发人员学习参考。内容以图文和示例配置相结合,突出DEB打包时DEBIAN目录的组织方式,以及RPM打包时BUILD/BUILDROOT/RPMS/SOURCES/SPECS/SRPMS标准目录的规划要点,并给出dpkg -b、rpmbuild等常用打包命令与安装、升级、卸载的具体用法,帮助读者快速上手产出可用的软件包。全部内容汇集在一份doc文档中,大小267KB,内容集中、层次清晰,便于按需查阅,目前已有417人学习浏览。掌握这两类打包机制,学会编写规范的控制文件和spec脚本,能有效提升软件分发、部署和版本维护效率,是管理Linux服务器和构建自动化交付体系的重要基础技能。

1. 为什么要学 RPM 和 DEB 打包:两种软件包格式,一套交付逻辑

把源码丢给用户自己编译,是分发软件最省事、也最伤用户体验的方式。真正到了交付环节,你会发现 Linux 世界里基本只剩两种主流软件包格式:rpm 系发行版认准.rpm,deb 系发行版认准.deb。RPM 和 DEB 软件包打包,就是把可执行文件、动态库、配置、文档和安装卸载脚本组织成系统能识别的归档,同时写清依赖、冲突、升级与卸载规则,让用户一条命令装上、一条命令卸干净。这篇笔记把这条链路拆成几步走:先讲清楚两种格式到底差在哪,再分别用一个最小示例完成.rpm和.deb的构建,接着把最容易翻车的坑逐个拆穿,最后落到验证与版本同步。适合正在做内部工具分发、或准备把自己的软件正经交付给 Linux 用户的人,新手能照步骤复现,熟手也能在边界参数上少走弯路。

2. RPM 和 DEB 的格式差异:从用户命令到包内结构都要分开理解

很多人以为 RPM 和 DEB 只是压缩格式不同,换把工具就能产出来。实际接触之后你会发现,这是两套独立设计的打包规范,用户侧的安装命令、包内的元数据结构、依赖表达方式、配置文件的保护机制全都不一样。这一章先把这些差异摊开,后面写 spec 和 debian 目录时你才知道每个字段在替谁说话。

2.1 从用户视角看两种包:安装、升级、卸载的动静差异

用户装一个 rpm 包,常见命令是rpm -ivh安装、rpm -Uvh升级、rpm -e卸载;装 deb 则常见dpkg -i安装、dpkg -r卸载。直接用底层命令时,两者都不会主动去解析依赖,缺依赖就报错让你自己想办法。日常大家用dnf、yum或apt,是因为包管理器在底层命令之上做了仓库和依赖解析。

升级行为差异更大。rpm 的升级本质上是先装新版本再删旧版本的组合动作,旧包里的%preun会执行,新包的%post也会执行,脚本顺序如果写错,升级过程中可能把正在运行的服务停掉又拉不起来。deb 的升级则依赖dpkg的脚本时机:preinst、postinst、prerm、postrm,安装、升级、删除各自对应不同的参数,脚本里必须按参数区分动作。

卸载时用户观察到的差别:rpm 卸载默认不会删配置文件,除非你用rpm -e --purge;deb 的dpkg -P才会清配置文件。这类“包管理器默认行为”决定了你怎么组织安装脚本:RPM 系更依赖%config标记保护配置,deb 系则靠conffiles列出名单。所以做双格式打包,不能只准备一套脚本,要把每种格式的默认行为先摸清。

2.2 包内部结构拆解:rpm 的 header+payload 与 deb 的 control+data

rpm 包从结构上看由两大部分组成:前面是 header,保存了包名、版本、依赖关系、安装脚本、文件清单等元数据;后面是 payload,即用 cpio 归档后再压缩的实际文件。这也是为什么rpm -qip看信息很快,因为只需要读 header,不需要解压整个包。

deb 包则是标准的 ar 归档,里面固定有三个成员:debian-binary记录格式版本号,control.tar存放 control 文件、维护脚本和校验信息,data.tar存放实际安装的文件。用户用dpkg-deb -I看元数据,用dpkg-deb -c看文件清单,本质上就是直接读归档里的不同成员。

这里给一套常用的查看命令,打包前和打包后都用得上:

# 查看 RPM 包的元数据与文件清单 rpm -qip demo-tool-0.1.0-1.x86_64.rpm rpm -qpl demo-tool-0.1.0-1.x86_64.rpm # 查看 DEB 包的元数据与文件清单 dpkg-deb -I demo-tool_0.1.0-1_amd64.deb dpkg-deb -c demo-tool_0.1.0-1_amd64.deb

rpm -qip的i表示 info,p表示对包文件操作而不是查本地数据库,l表示列出文件。dpkg-deb -I里的I是 info,输出 control 和脚本内容;-c是 list 的简写。多一步rpm -qp --scripts或dpkg-deb -e解出控制目录,还能看到安装脚本是否和你写的一致。这些是上线前最直接的验证手段。

两种格式的元数据字段,习惯做成一张对照表来记忆:

用途RPM 字段DEB 字段
包名NamePackage:(二进制包名)
版本Version+ReleaseVersion(含修订号)
源码包名包名即源码名Source:
编译依赖BuildRequiresBuild-Depends
运行依赖RequiresDepends
冲突声明ConflictsConflicts
替换旧包ObsoletesReplaces
配置文件标记%configconffiles文件

rpm 的版本由Version和Release拼起来,所以重打包只升Release很方便;deb 的版本在Version一个字段里,习惯写成0.1.0-1,后面的修订号对应 rpm 的Release。架构名也不一样,rpm 是x86_64,deb 是amd64。写跨格式脚本时最容易漏的就是这些细节。

2.3 环境准备:安装工具链并初始化构建目录

rpm 打包含rpm-build和rpmdevtools就够了。安装后执行rpmdev-setuptree,会在家目录下建好标准目录:BUILD、RPMS、SOURCES、SPECS、SRPMS。这个目录结构是 rpmbuild 默认的工作区约定,源码压缩包放进SOURCES,spec 文件放进SPECS,构建产物输出到RPMS和SRPMS。

deb 打包不依赖固定目录,只要你把源码目录和debian/子目录放好即可,构建工具用build-essential、debhelper和devscripts。下面是两边的初始化命令:

# rpm 系发行版 dnf install rpm-build rpmdevtools rpmdev-setuptree ls ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} # deb 系发行版 apt install build-essential debhelper devscripts

rpmdev-setuptree仅仅是创建目录,不会自动配置任何宏。如果你不想用家目录默认路径,可以自己写~/.rpmmacros,把%_topdir指到项目目录,这样 CI 里便于整体清理。同一台机器上同时维护两套工具链没有冲突,rpmbuild 和 dpkg-buildpackage 互不认识,各管各的。

配置自定义构建根的示例:

echo "%_topdir /home/packager/rpmbuild" > ~/.rpmmacros

%_topdir是 rpmbuild 最核心的宏之一,所有中间目录都挂在它下面。改它之前先确认目录有写权限,否则构建时会在mkdir阶段就失败。deb 侧没有对应的全局配置需求,debian/目录里的文件就是全部配置,这也是 deb 打包相对轻量的原因之一。

3. 用 spec 文件打一个 RPM 包:最小示例与三个必调参数

RPM 打包的全部逻辑都写在 spec 文件里。它既描述包是什么,也描述怎么编译、装哪些文件、装完执行什么脚本。第一次写 spec 的人容易被段落数量吓到,其实核心执行段只有四段:%prep、%build、%install、%files,其余都是声明性内容。

3.1 spec 文件骨架:从 Name 到 %changelog 的分段职责

spec 开头是包头字段,Name、Version、Release三个字段组合出完整的包名。Release里常见的%{?dist}表示发行版后缀,比如在 rpm 系发行版上构建会自动附加el9或fc40之类的标记,方便同一版本在不同发行版上各自维护修订号。Source0指向源码压缩包,文件名要能被rpmbuild在SOURCES目录里找到。

BuildRequires声明编译期依赖,Requires声明运行期依赖。很多人习惯把两者写成一样的,实际编译依赖往往多于运行依赖:比如构建时需要gcc,但运行时只需要glibc。依赖写太多会拖累用户安装,写太少会装完跑不起来,后面第 5 章专门讲这个坑。

执行段里,%prep负责解压源码并应用补丁,%setup -q是最常用的解压宏,它会自动进入解压出的目录。%build通常执行make;%install负责把文件安装到%{buildroot}目录下,这里千万不能直接安装到系统的/usr/bin,而是要先安到一个临时的构建根,rpmbuild 打包时以这个构建根的内容为准。%files列出最终要装进包的文件,可以是具体文件,也可以是目录通配符。

3.2 完整示例:给一个 C 语言命令行工具打 RPM

用一个最简单的 C 工具演示流程。假设demo-tool-0.1.0目录下有demo-tool.c和Makefile,Makefile 的 install 目标支持DESTDIR变量:

PREFIX ?= /usr all: cc -o demo-tool demo-tool.c install: install -d $(DESTDIR)$(PREFIX)/bin install -m 0755 demo-tool $(DESTDIR)$(PREFIX)/bin

DESTDIR是打包的标准接口,没有它,make install会把文件直接装到系统目录,rpmbuild 无法收集文件列表。示例里PREFIX定义为/usr,后续 deb 打包时同样走这个接口。

对应的 spec 文件长这样:

Name: demo-tool Version: 0.1.0 Release: 1%{?dist} Summary: A command line tool for demonstration License: MIT URL: https://example.invalid/demo-tool Source0: %{name}-%{version}.tar.gz BuildRequires: gcc Requires: glibc %description A minimal demo package used to walk through RPM packaging. %prep %setup -q %build make %install make install DESTDIR=%{buildroot} %files %{_bindir}/demo-tool %changelog * Fri Jan 01 2025 Packager <packager@example.invalid> - 0.1.0-1 - Initial package

把源码目录打成 tar.gz,放进SOURCES,再执行rpmbuild -ba:

tar czf ~/rpmbuild/SOURCES/demo-tool-0.1.0.tar.gz demo-tool-0.1.0/ rpmbuild -ba ~/rpmbuild/SPECS/demo-tool.spec

tar czf里的源码包顶层目录名必须与%{name}-%{version}完全一致,%setup -q解压后要能直接cd进去。-ba表示同时构建二进制包和源码包(src.rpm),构建成功后二进制包在~/rpmbuild/RPMS/x86_64/,源码包在~/rpmbuild/SRPMS/。如果只想快速确认文件列表对不对,可以先rpmbuild -bp只跑%prep阶段,避免每次全量编译。

3.3 rpmbuild 构建参数:-ba 与 -bb 的区别还有 --define 的用途

rpmbuild的子命令对应不同阶段:-bp只解压源码,-bc执行到编译,-bi安装到构建根,-bb只产出二进制包,-ba同时产出二进制包和 src.rpm。平时提交给 CI 用-ba最稳妥,因为 src.rpm 是完整的可复现原料,别人拿到它能重新构建。

--define用于临时覆盖宏,最常见的场景是指定额外变量。比如手动指定构建版本后缀:

rpmbuild -bb --define "dist .el9" ~/rpmbuild/SPECS/demo-tool.spec

--define的格式是键 值,键值之间必须用空格,不能用等号。它只对当前命令有效,不会写进~/.rpmmacros。调试时可以多传几个--define覆盖 Release、覆盖%_topdir,方便在非默认目录做试验。

还有一个容易忽略的参数是--target。它能强制指定构建架构,比如在 x86_64 机器上交叉构建 arm64 包时配合工具链使用。普通场景不用碰它,但你要知道 rpmbuild 不是只能构建本机架构的包。验证产物时,rpm -Uvh装上后立刻which demo-tool确认路径,再跑一次程序确认动态库依赖没有缺失。

4. 用 debian 目录打一个 DEB 包:control、rules 与 dh 工具链

deb 打包的思路和 RPM 完全不同:rpm 把一切写进一个 spec 文件,deb 则把信息拆进debian/目录的多个文件里。好处是职责清楚,坏处是文件多而杂,新手容易搞不清哪个文件在什么时机被谁读取。

4.1 debian/ 目录里每个文件干什么:control 与 rules 分权

最关键的是control和rules。control声明包的元数据和依赖,分两段:Source段描述源码包,Package段描述二进制包。Build-Depends对应 rpm 的BuildRequires,Depends对应 rpm 的Requires。rules是构建脚本,本质上是一个可执行的 Makefile,它定义build、install、binary等目标。

changelog记录版本历史,格式有严格规定,dpkg-buildpackage会根据它生成debian/changelog的版本信息,包的版本号由此而来。compat文件声明 debhelper 兼容级别;也可以不写这个文件,而在Build-Depends里写debhelper-compat (= 13),这是新版推荐做法,少维护一个文件。

其余文件按需出现:install用来把构建产物复制进包目录,不写则依赖dh_auto_install的默认行为;conffiles列出需要保护的配置文件;postinst、prerm等维护脚本负责安装、删除前后的动作。注意维护脚本必须带可执行权限,否则 dpkg 会直接拒收。

4.2 最小可复现示例:同一份源码打成 .deb

继续用上一章的demo-tool作为示例源码,目录结构如下:

demo-tool-0.1.0/ ├── Makefile ├── demo-tool.c └── debian/ ├── changelog ├── compat ├── control └── rules

debian/control内容:

Source: demo-tool Section: utils Priority: optional Maintainer: Packager <packager@example.invalid> Build-Depends: debhelper (>= 13), gcc Standards-Version: 4.6.2 Package: demo-tool Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} Description: A command line tool for demonstration A minimal demo package used to walk through Debian packaging.

${shlibs:Depends}是输入变量,构建时dh_shlibdeps会自动分析二进制文件的动态库依赖并填充成实际的Depends。${misc:Depends}由 debhelper 填充。手动写依赖容易漏,这两个变量是 deb 系推荐的标准写法。

debian/rules只写一行主规则:

%: dh $@

dh是 debhelper 的驱动脚本,$@是 make 自动变量,代表当前目标的名称。这一行让dh自动执行一串预设命令,包括dh_auto_configure、dh_auto_build、dh_auto_install、dh_install、dh_makeshlibs、dh_shlibdeps等。对这个普通 Makefile 项目,dh_auto_install会执行make install DESTDIR=debian/demo-tool,所以 Makefile 里支持DESTDIR这一点,两种打包格式必须同时满足。

debian/changelog的格式是硬性的:

demo-tool (0.1.0-1) unstable; urgency=medium * Initial package -- Packager <packager@example.invalid> Fri, 01 Jan 2025 00:00:00 +0000

首行括号里的内容就是包版本号,不能写错,因为它是dpkg-gencontrol读版本的唯一来源。然后执行构建命令:

dpkg-buildpackage -us -uc

-us跳过源码签名,-uc跳过 changes 文件的签名。不加这两个参数,本地没有 GPG 密钥时构建会在签名环节失败。产物在源码目录的上一级,.deb文件命名格式是包名_版本号_架构.deb,例如demo-tool_0.1.0-1_amd64.deb。

4.3 dpkg-buildpackage 参数与 debhelper 的兼容级别

dpkg-buildpackage的参数决定构建范围和行为:-b只构建二进制包,-S只构建源码包,-us -uc控制签名。不加参数时默认同时做源码包和二进制包。第一次构建建议直接-b -us -uc,产出一个.deb文件,流程短,也方便排查。

debhelper 兼容级别写在compat文件里,常见值为 10、11、12、13。级别影响dh_*命令的默认行为。比如 compat 9 及以下对.install文件的支持和文件名匹配规则略有差异,compat 13 则默认启用更严格的路径检查。新项目直接用 13,老项目升级前先看dh的变更说明,盲目改 compat 会导致dh_auto_install的行为变化、文件装错位置。

调试时给 rules 加一个环境变量能看得更清楚:

DH_VERBOSE=1 dpkg-buildpackage -us -uc

DH_VERBOSE=1让dh把实际执行的每个命令都打印出来,对排查dh_auto_install到底装了什么、装到哪,是最直接的入口。构建完成后用dpkg -I和dpkg -c验证元数据与文件列表,再dpkg -i安装测试一遍,确认Depends被正确填充。拿到Depends实际值挺重要,因为它能告诉你dh_shlibdeps有没有正常工作、动态库依赖有没有被正确抓取。

5. 打包避坑与排查:五个高频翻车现场的现象、原因与解法

打包这事儿,表面上命令跑通就有产物,实际上细节全在“装上之后能不能跑、升级时会不会丢配置、卸载时干不干净”这些验证里。这一章记录我反复踩过的五个典型问题,每条都按现象、原因、解决的顺序展开。

5.1 现象一:装上之后找不到动态库,编译时明明好好的

在打包机上程序跑得正常,拷到另一台全新系统,装完包一执行就报error while loading shared libraries: libfoo.so.1。

原因是构建机环境太“脏”:编译时某个依赖库系统里已经装了,Requires没有声明,真正干净的运行环境里库不存在。rpm 侧还有一个隐蔽原因:%files忘了把软件自带的.so文件打进去,或者只打了.so没打版本化文件名,导致运行时按libfoo.so.1找不到。

解决方法是编译结束后用readelf -d查看可执行文件的 NEEDED 段:

readelf -d demo-tool | grep NEEDED

逐条对照Requires声明。rpm 侧用rpm -qpR看成品包的依赖,必要时把软件自带的库文件作为%{_libdir}/libfoo.so.*写进%files。deb 侧尽量别手动写Depends,让dh_shlibdeps帮你生成,它会读取NEEDED并映射到系统内对应的包名。

5.2 现象二:升级时用户改过的配置被静默覆盖

用户改了/etc/demo-tool.conf里的参数,升级新版本后配置文件变成默认值,用户改的配置全没了,直接在线上翻车。

原因是配置文件被打成了普通文件,安装脚本在升级时会用新文件覆盖旧文件,没有做配置保护。rpm 侧需要在%files里把该路径标记为%config(noreplace);deb 侧需要在debian/conffiles里列出该绝对路径,一行一个。

解决的关键是理解两种标记的差异:rpm 的%config在旧配置被修改时会生成.rpmnew保留新内容,而%config(noreplace)则直接保留旧文件、把新配置写成.rpmnew;deb 的conffiles机制类似,dpkg 在升级时对比校验和,发现用户改过就询问或保留。凡是用户可能手改的文件,都用这套机制保护,别怕麻烦。

5.3 现象三:DEB 包依赖写对却报“依赖关系无法满足”

dpkg -i安装时报依赖关系无法满足: libfoo (>= 2.0),但apt-cache policy libfoo查到的已装版本明明大于 2.0。

常见原因有三个:一是Depends里写了libfoo(>= 2.0),依赖包名和版本之间缺空格,dpkg 解析时把整个字符串当成了包名;二是把编译机才有、运行机没有的Build-Depends也抄进了Depends;三是包名带了架构后缀,比如写libfoo:amd64,而实际包名不含:amd64。

解决方法是先统一依赖写法:包名 (>= 版本),括号前后按规范留空格,多个依赖用逗号分隔。然后逐条确认运行机真的需要这个库,能用dpkg -s验证版本。临时用--force-depends装上是自救手段,不是长期方案,依赖关系还是要改对。

5.4 现象四:同一份代码打出两种包,文件清单对不上

rpm 包和 deb 包各装一台机器,demo-tool能跑,但一边有 man 手册另一边没有,或者一边多了个空目录,两边文件列表总有微妙的差异。

原因在于%files和 deb 的文件收集机制是两套独立逻辑,人肉维护必然漏。rpm 的%files要你一条条写,deb 的dh_auto_install则把make install DESTDIR=debian/demo-tool的内容全部收进包。两边的“事实来源”看似相同,实际维护时各写各的,就分叉了。

解决方法是只维护一份文件清单。我一般把Makefile的 install 目标作为唯一事实来源,rpm 侧%files直接引用%{buildroot}下装出的文件,deb 侧直接信任dh_auto_install的结果。如果有额外文件要收录,用 deb 的.install文件或 rpm 的%files全局通配符,尽量少在两边同时写具体文件路径。每次构建后,各跑一次rpm -qpl和dpkg-deb -c对比输出,不一致的立刻修。

5.5 现象五:打包脚本里写死了本机路径,换机器就翻车

构建设备上有/home/user01/thirdparty/libxxx,spec 里%build或%install直接引用这个绝对路径,构建时没问题,拿到别的机器上运行就找不到库、找不到配置。

原因是%install里直接对系统路径操作,或者编译期把本地绝对路径编进了二进制,没做 staging 隔离。rpm 的%{buildroot}和 deb 的debian/包名目录就是用来隔离的,所有文件必须先落到这个隔离目录,再被工具收集。

解决方法是统一约束:任何make install必须支持DESTDIR,任何手工install -d必须拼上构建根变量,绝不允许直接写/usr/bin、/etc这类裸路径。rpm 侧写成install -d %{buildroot}%{_bindir},deb 侧交给dh_auto_install并确认 Makefile 用的是$(DESTDIR)。编译参数里如果必须引用第三方库路径,用--define或环境变量传入,不要硬编码。

6. 让打包可复现:容器构建、静态检查与版本同步三个技巧

打包脚本写好后,最大的敌人是环境漂移。今天构建能过,三个月后同一份 spec 因为系统升级了一个编译依赖就崩了;本机能过,同事机器上却缺库。我的习惯是:一旦项目进入正式交付,就把打包丢进容器环境,所有依赖通过构建脚本显式安装,禁止在容器里手敲dnf install或者apt install凑依赖。这样每次构建从空白环境开始,BuildRequires少了哪个包,构建立即报错而不是侥幸通过。

# 进入容器后,按构建文档逐条安装依赖,然后执行: rpmbuild -ba ~/rpmbuild/SPECS/demo-tool.spec dpkg-buildpackage -us -uc

容器跑完构建后,顺手跑一次静态检查。rpm 侧用rpmlint,deb 侧用lintian,这两个工具能查出元数据缺失、权限错误、重复文件、脚本语法不合法等问题:

rpmlint ~/rpmbuild/RPMS/x86_64/demo-tool-0.1.0-1.x86_64.rpm lintian demo-tool_0.1.0-1_amd64.deb

输出里的E:是错误必须改,W:是警告建议改,I:是信息类提示。第一次跑会看到不少噪音,比如缺少文档、changelog 缺失这类提醒,不用全部消除,重点盯E:和与文件路径、依赖相关的W:。

版本同步也值得写进流程。rpm 用Version + Release,deb 用Version里的0.1.0-1,两边经常出现 rpm 升到 0.2.0、deb 还停在 0.1.0 的情况。我的做法是在项目根目录放一个VERSION文件,Makefile 里读它,再通过 sed 生成或更新 spec 和debian/changelog:

VERSION := $(shell cat VERSION)

发布时只改VERSION,然后重新生成两份打包元数据,从源头杜绝版本不一致。打包这件事,最不值钱的是让rpmbuild -ba跑通,最值钱的是让「换一台机器、隔半年、换个人」都能稳定产出一样的结果。我现在接到任何要交付的 Linux 工具,第一件事是先把Makefile install目标和DESTDIR支持补上,再在这套地基上套 spec 和 debian 目录,省得每次打包都在文件清单上找补。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进