简介:面向Linux系统管理员、运维工程师及软件打包入门者,文档系统讲解RPM与DEB两大软件包管理体系的完整打包流程。内容依次覆盖两者适用的发行版差异、目录结构搭建、control/spec文件关键字段含义,以及preinst、postinst、prerm、postrm等生命周期脚本的作用;从DEB的DEBIAN控制目录和usr、opt等安装路径,到RPM的BUILD、SPECS、SOURCES等固定目录均有具体说明,还结合命令展示dpkg -b与rpmbuild的打包、安装、升级、卸载操作,帮助读者理解两种格式的核心机制并快速制作自己的安装包,避开依赖缺失、脚本权限等常见打包陷阱。资源包共1个doc文档,大小约267KB,以图文步骤和示例为主,适合边看边练。目前已有416人学习浏览,对需要为软件部署、分发和维护建立系统化认知的技术人员有直接参考价值。整体将DEB与RPM的目录结构、脚本时机、依赖配置等要点对照呈现,便于在单一文档中快速检索与对比学习。
1. RPM和DEB软件包打包教程在解决什么:从“能跑”到“能装、能升级、能卸载”
“RPM和DEB软件包打包教程”这类资料,核心讲的是一件事:把一段“本地能跑”的代码,变成用户能稳定安装、升级、卸载干净的软件包。RPM 服务于 Fedora、CentOS、openEuler 这条 Red Hat 谱系,DEB 服务于 Ubuntu、Debian 及大量衍生发行版,两者把二进制、配置文件、安装卸载脚本和依赖关系收进单一文件,交给系统的包管理器统一管理。适合谁读?给服务器写工具链的研发、给内网做软件分发的运维、准备把软件正式交付外部用户的产品团队。读完你能判断自己的软件该走哪条打包路线,并照着搭出第一条能跑通的流水线。
2. 打包前先分清两套体系:RPM 与 DEB 的包结构、生命周期与选型
先说结论:打包翻车的人里,十有八九不是命令记错,而是不理解“包”在系统里到底是个什么东西。RPM 和 DEB 表面都是“一个文件、一条命令装完”,底层结构却完全是两套设计思路。先把这两套结构摸清楚,后面 spec 和 control 里的每个字段你都能知道它去了哪里、在哪个环节生效。
2.1 RPM 的包体结构:header 元数据加 cpio 归档
RPM 文件本质上是由两段拼起来的:一段二进制 header,一段 cpio 归档。header 里存的是机器可读的元数据,包括包名、版本、Release、依赖关系、安装前后执行的脚本、文件清单的哈希等;cpio 归档里才是真正的程序文件、配置文件和文档。任何 .rpm 文件都可以用rpm -qip读取元数据,用rpm2cpio把归档内容原样解出来。
这种结构决定了 RPM 的核心能力是“事务管理”。安装一个 RPM 包时,rpm 会先做依赖检查,再按顺序执行%pre脚本、解包、执行%post脚本,整个过程写入 rpm 数据库;卸载时再反向执行%preun、删文件、执行%postun。%files清单负责登记“哪些文件属于这个包”,rpm 数据库里记录的是这些路径和校验值,这就是它能做到“卸载干净”的根基。
2.2 DEB 的包体结构:ar 容器加三个成员
DEB 走的是另一条路。一个 .deb 文件用古老的 ar 格式打包,里面固定有三个成员:debian-binary只是一个版本号文本;control.tar.gz里装着 control 元数据、维护者脚本(preinst、postinst、prerm、postrm)和 conffiles;data.tar.*装实际文件。用dpkg-deb -e可以把控制信息和脚本单独解出来,用dpkg-deb -x解开数据部分。
你会发现 DEB 的脚本命名和 RPM 不同:RPM 叫%pre、%post、%preun、%postun,DEB 叫 preinst、postinst、prerm、postrm,触发时机对应关系是:安装前、安装后、卸载前、卸载后。另有一个 conffiles,作用是标记“这是配置文件,升级时如果用户改过就不要覆盖”,它和 RPM 里的%config是同一个设计要解决的问题,但实现方式完全独立。
2.3 动手拆包:rpm2cpio 与 dpkg-deb 的实际验证
讲完结构,建议你立刻找两个现成的包拆开看看。手里没有就先用构建产物,拿个 nginx 或 MySQL 的安装包练手也行。这一套命令可以帮你把“黑匣子”变成透明的目录树:
# 拆 RPM rpm -qip ./hello.rpm # 查看元数据:名称、版本、依赖、脚本 rpm2cpio ./hello.rpm | cpio -t # 只列出归档内的文件名 mkdir -p /tmp/rpm-extract && cd /tmp/rpm-extract rpm2cpio ./hello.rpm | cpio -id # 完整解包到当前目录 # 拆 DEB dpkg-deb -I ./hello.deb # 查看 control 信息 dpkg-deb -c ./hello.deb # 列出 data 部分文件 mkdir -p /tmp/deb-extract && cd /tmp/deb-extract dpkg-deb -x ./hello.deb . # 解出 data 内容到当前目录 dpkg-deb -e ./hello.deb # 解出 control 和维护者脚本到 DEBIAN 目录关键看两个点:一是解出来的目录结构,原生包的%files清单不是乱写的,路径层级就是安装后的真实布局;二是维护者脚本存的位置,一个在 RPM header 里,一个在 control.tar.gz 里,但最终都服务于“安装、升级、卸载”这三个生命周期动作。
2.4 选型逻辑:用户跑什么发行版,就交付什么包
选型没有玄学,只有一条主规则:看你的用户群在什么发行版上跑。服务器内部以 CentOS、Rocky、openEuler 为主力,就打 RPM;桌面办公和教学场景多为 Ubuntu、Debian 衍生版,就打 DEB;对外商业发布则两种都给。观察一个现象就能理解——Chrome 官方提供的是 .deb 和 .rpm 两种安装包,没有 tar.gz;Thonny 这类教学 IDE 官网也按发行版给安装包,Ubuntu 用户拿到的是 .deb。为什么?因为对最终用户来说,双击或一条命令装完,远比“下载解压然后自己找路径”可靠。
| 对比项 | RPM | DEB |
|---|---|---|
| 构建入口 | spec 文件 + rpmbuild | debian/control + debhelper |
| 查询文件归属 | rpm -ql 包名 | dpkg -L 包名 |
| 安装本地包 | rpm -ivh ./x.rpm | apt install ./x.deb |
| 卸载 | rpm -e 包名 | dpkg -r 包名 |
| 配置标记 | %config、%config(noreplace) | conffiles |
| 依赖检查 | 安装时强制,rpmdb 记录 | 依赖 apt/dpkg 解析 |
还有一个常见问法:能不能用 alien 把 RPM 转成 DEB?能用,但我不会在正式交付里依赖它。alien 的转换对纯二进制文件问题不大,一旦包里有维护者脚本、配置文件、多架构支持,转换后的语义会变形,依赖关系也经常对不上。应急可以,长期还是要按原生体系重打。
3. 从零打一个 RPM 包:spec 文件、rpmbuild 与五个必调参数
现在开始动手。以 CentOS 系的服务器为例,目标是把一个最简单的 shell 脚本工具包装成 RPM。别看例子小,RPM 的完整流程都会走一遍,后面换成任何编译型项目只是增加%build段的复杂度。
3.1 先搭 rpmbuild 目录骨架
rpmbuild 对目录结构有约定,默认是家目录下的rpmbuild,里面五个目录各管一摊:SOURCES 放源码归档和补丁,SPECS 放 spec 文件,BUILD 是解压和编译的临时工作目录,BUILDROOT 是模拟安装根目录,RPMS 和 SRPMS 放构建产物。新手最容易犯的错是把源码直接丢进 BUILD,其实 SOURCES 才是 rpmbuild 真正去找文件的地方。
# 创建目录骨架 mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} # 定义 topdir,并关闭构建 ID 链接(避免调试符号目录干扰) cat > ~/.rpmmacros <<'EOF' %_topdir %(echo $HOME)/rpmbuild %_build_id_links none EOF # 验证宏是否生效 rpm --eval '%{_topdir}'最后一条rpm --eval是检查手段,能输出%_topdir的实际路径就说明 .rpmmacros 被正确读取了。一般不需要改默认目录,除非你的 CI 用户没有独立家目录。%_build_id_links none是省事选项,某些发行版会为每个包生成 build-id 符号链接,脚本包用不到,关掉能少踩一个“文件清单莫名多出几个链接”的坑。
3.2 写一个最小可用 spec:五个必调参数逐个拆
spec 文件是 RPM 打包的唯一入口,rpmbuild 所有行为都由它驱动。下面这份是我平时写工具型小包的起步模板,把注释读一遍再抄:
# ~/rpmbuild/SPECS/hello.spec Name: hello Version: 1.0 Release: 1%{?dist} Summary: A demo shell script packaged with rpmbuild License: GPLv3 URL: https://example.invalid/hello Source0: %{name}-%{version}.tar.gz BuildRequires: bash Requires: bash %description A minimal demonstration package that installs /opt/hello/hello.sh %prep %setup -q %build # 脚本包无需编译,保留空段便于以后扩展 %install mkdir -p %{buildroot}/opt/hello install -m 0755 hello.sh %{buildroot}/opt/hello/hello.sh %files /opt/hello/hello.sh %post echo "hello.sh installed, try /opt/hello/hello.sh"源码归档里的 hello.sh 内容很简单:
#!/usr/bin/env bash echo "Hello from RPM packaging"Source0必须和 SOURCES 目录里实际存在的文件同名,%setup -q会把它解压到 BUILD 目录并自动进入解压后的目录。%install段里的%{buildroot}是模拟安装根,所有文件必须先装到这里,再由%files清单决定哪些真正进入包。清单漏一个文件,rpmbuild 会直接报错,这个设计比手动 tar 可靠得多。
五个必调参数,按踩坑频率排:
| 参数 | 作用 | 不设的后果 |
|---|---|---|
| BuildRequires | 构建期依赖,yumbuild 时自动装 | 缺编译工具链,%build段直接失败 |
| Requires | 运行期依赖,安装时强制检查 | 用户装上后跑不起来 |
| %attr | 设置文件的权限、属主、属组 | 默认按 install 命令的权限走,容易过宽 |
| %config / %config(noreplace) | 标记配置文件 | 升级时配置文件被无差别覆盖,用户配置丢失 |
| AutoReqProv | 自动依赖探测 | 脚本包常被解析出 /bin/bash 等多余的依赖 |
AutoReqProv的坑最隐蔽:RPM 默认会扫描包内可执行文件,自动生成对解释器、共享库的依赖。一个#!/usr/bin/env bash的脚本,可能被自动加上对bash的依赖,这本身没错;但如果你的包依赖的是系统里没有的 Python 路径,自动探测就会生成一个谁也无法满足的依赖。脚本类包我会显式关掉自动依赖生成,改用Requires手写需要的运行时。
3.3 执行构建并做安装、查询、卸载闭环
构建、安装、验证三步走完,才算真正“跑通”了 RPM 打包。下面的命令从源码归档放位开始,到彻底卸载结束:
# 1. 源码归档放到位 cp hello-1.0.tar.gz ~/rpmbuild/SOURCES/ # 2. 构建二进制包和源码包 cd ~/rpmbuild/SPECS rpmbuild -ba hello.spec # 3. 查看产物 ls -l ~/rpmbuild/RPMS/x86_64/hello-1.0-1.x86_64.rpm # 4. 安装 sudo rpm -ivh ~/rpmbuild/RPMS/x86_64/hello-1.0-1.x86_64.rpm # 5. 查询文件归属并运行 rpm -ql hello /opt/hello/hello.sh # 6. 卸载 sudo rpm -e hello-ba表示同时构建二进制包和 SRPM 源码包,日常只需要二进制包时用-bb。rpm -ql查询的是已安装包的%files清单,和rpm -qpl查未安装包要区分开。卸载后可以再用rpm -qa | grep hello确认数据库里已经干净。如果构建失败,优先看 BUILDROOT 目录里有没有残留文件,再用-ba的完整日志定位,rpm 的报错信息基本都会指到具体文件和阶段,很少含糊。
4. 从零打一个 DEB 包:debhelper、control 文件与 dpkg-buildpackage
DEB 的流程比 RPM 散一点,因为 Debian 的工具链把“元数据”和“文件内容”拆得更明确。同样用 hello.sh 做例子,在 Ubuntu 或 Debian 上一路走到底,你会看到 control、rules、changelog 三者怎么配合。
4.1 debian/control:DEB 的元数据入口
DEB 打包的元数据集中在源码树顶层的debian/目录里。最重要的文件是 control,它同时描述“源码包”和“二进制包”两段信息。一个最小可用的 control 长这样:
Source: hello Section: utils Priority: optional Maintainer: Your Name <you@example.com> Build-Depends: debhelper (>= 10) Standards-Version: 4.5.0 Package: hello Architecture: all Depends: bash Description: A demo shell script packaged with deb A minimal demonstration package that installs /opt/hello/hello.sh注意Source:和Package:是两段,前者描述源码包,后者描述最终安装的二进制包。Architecture: all表示不依赖具体 CPU 架构,纯脚本包就该这么写;Depends对应 RPM 的Requires,安装时由 apt 负责解析。Description的第一行是短描述,后续行必须以一个空格开头做缩进,这是 Deb 格式的硬性要求,少一个空格dpkg-deb都会拒绝解析。
4.2 用 dh_make 生成骨架再改
手写整个 debian/ 目录容易漏文件,常见做法是用dh_make生成模板再裁剪。命令如下:
cd ~/hello-1.0 dh_make -p hello_1.0 --create --single --yes参数里-p指定包名和版本,--single表示单二进制包,--yes自动采用默认模板。生成后 debian/ 下会多出一堆 .EX 模板文件,我习惯只保留 control、rules、changelog、compat、source/format 五个,其余删掉避免干扰。rules 文件最简洁的写法是:
#!/usr/bin/make -f %: dh $@dh $@是 debhelper 提供的默认序列驱动器,一条规则把 clean、build、install、binary 全流程串起来。compat 文件写一个数字,10 是兼容性最稳的选择,新工具链环境可以用 11 或 12,取决于 CI 镜像里的 debhelper 版本。changelog 至少要有初始条目,否则 dpkg-buildpackage 会直接拒绝构建:
hello (1.0-1) unstable; urgency=medium * Initial release -- Your Name <you@example.com> Fri, 01 Jan 2024 00:00:00 +0000日期格式是小坑,必须符合 RFC 5322,否则 lintian 和构建都会报警。
4.3 构建二进制包并检查产物
源码目录准备好后,执行构建。这里有个新手上当率最高的点:产物不在当前目录,而在上一级目录。
cd ~/hello-1.0 dpkg-buildpackage -us -uc -b # 产物位置:上一级目录 ls -l ../hello_1.0_all.deb # 检查元数据和内容 dpkg-deb -I ../hello_1.0_all.deb dpkg-deb -c ../hello_1.0_all.deb-us -uc表示不签名源码包和 changes 文件,本地验证时省去配置 GPG 的麻烦;-b只构建二进制包,不生成 .dsc 和 .tar.xz 源码包,节奏更快。看到_all.deb就对了,如果构建机是 x86_64 且 Architecture 写了 amd64,产物名会是_amd64.deb。dpkg-deb -I的输出里重点核对 Depends 行,它决定用户安装时 apt 会额外拉什么包。
4.4 安装验证与本地依赖解析
DEB 的安装命令值得单独说,因为网上到处能看到cd ~/downloads然后执行那一串的命令。我自己的偏好是:
cd ~/downloads sudo apt install ./hello_1.0_all.deb路径前面的./不是装饰,是告诉 apt“这是一个本地文件”的明确信号。apt 会先解析这个 deb 的 Depends 字段,去已配置的源里拉缺失依赖,然后再把包本身装上。如果你直接dpkg -i,dpkg 只会做简单的依赖告警,不会自动装依赖,依赖缺失时包虽然装上了但运行直接报错。验证文件归属用dpkg -L hello,卸载用dpkg -r hello。这就是内网分发教程里常见的标准动作背后完整的逻辑:先解析,再安装,最后留足查询和卸载的退路。
5. 常见坑与排查:没找到 rpm 命令、安装失败、卸载残留的定位思路
打包的坑大多集中在环境误判、依赖错配、文件清单漏项。这一章按“现象 → 原因 → 解决”把出现率最高的五条捋一遍。
5.1 现象:Ubuntu 上执行 rpm 提示“没找到 rpm 命令”
原因是 Ubuntu 和 Debian 默认不带 RPM 工具链,rpm 本身是 Red Hat 系的包管理工具,在 Debian 系里即使apt install rpm装上了,也只能做查询和转换,完整打包环境仍然不具备。解决方法是不要在 Debian 系里硬打 RPM,换一个 RPM 系的容器环境:
docker run --rm -v ~/rpmbuild:/rpmbuild:Z -w /rpmbuild/SPECS \ rockylinux:9 bash -c "yum install -y rpm-build && rpmbuild -ba hello.spec"容器挂载时加了:Z是因为 SELinux 环境会拦截容器读写宿主目录,这在云主机上尤其常见。如果不用容器,也可以准备一台 CentOS 虚拟机专门跑 rpmbuild,总之别在 Ubuntu 上跟 rpm 硬刚。
5.2 现象:在 Windows cmd 里执行 deb 安装命令失败
不要笑,这个检索量还真不小。有人在 Windows 的 cmd 里敲sudo apt install ./xxx.deb,当然报“不是内部或外部命令”。原因是 deb 的安装工具 dpkg、apt 只存在于 Linux 环境,cmd 和 PowerShell 里没有。解决也简单:用 WSL 进入 Linux 子系统执行;或者把 deb 文件拷到一台 Linux 机器上,cd ~/downloads后执行sudo apt install ./xxx.deb。这不算打包本身的坑,但属于“执行环境没对上号”的典型,排查顺序永远是:先确认你在什么系统里,再谈命令为什么不对。
5.3 现象:安装 deb 报 wrong architecture 或 dependency not satisfiable
前者是架构不匹配,后者是依赖找不到。先各自排查:
# DEB 系:看当前系统架构 dpkg --print-architecture dpkg --print-foreign-architectures apt-cache policy 依赖包名 # RPM 系:看包元数据里的架构 rpm -qp --qf '%{ARCH}\n' ./hello.rpm uname -mArchitecture 写了 amd64 的包装到 arm64 机器上必然报 wrong architecture,这类问题往往是打包机架构和发布平台不一致造成的。依赖不满足的原因通常是 Depends 里写的包名在当前已启用的源里不存在,或源里只有更高版本不符合版本要求。内网场景里,从发行版文件池里直接拷一个 deb 硬装最容易撞上这条——文件本身没问题,但它的依赖链在你这台机器上没有对应源头,应该用 apt 源的方式引入整个目录,而不是单文件强制安装。
5.4 现象:卸载后文件残留,/usr/local 下到处是垃圾
这是对打包机制理解不到位的最典型表现。RPM 只删%files清单里登记过的文件,DEB 只删 data.tar 归档里的文件;如果你在%install或 install 阶段直接cp到系统目录,绕过了%{buildroot}和暂存区,包管理器根本不知道那些文件的存在。解决方法是强制纪律:RPM 侧所有文件先进%{buildroot},再由%files收口;DEB 侧使用 dh_install 把文件放到debian/hello/usr/等对应位置,由 dh 自动收集。另外注意%config(noreplace)在升级时保留用户修改并按 .rpmsave 后缀保存旧文件,这是正常设计,不是残留。
5.5 现象:rpmbuild 报“Installed (but unpackaged) file(s) found”
这个报错直接告诉你:%install阶段向 buildroot 里放入了文件,但%files清单漏写了。rpmbuild 是对完整性有强迫症的工具,它不允许“装了却没人登记”的文件存在。解决分两步:
# 1. 打开日志,看最后的 unpackaged files 列表,逐个补进 %files rpmbuild -ba hello.spec 2>&1 | tail -30 # 2. 用 -bl 只校验文件清单,快速迭代 rpmbuild -bl hello.spec经常连带的还有依赖误报:脚本包里的%files补全后,rpm 自动依赖收集可能扫出一个Requires: /bin/bash之类的噪音。纯脚本工具包我一般显式禁止自动依赖生成,改写Requires,这样打出来的包依赖面才干净。
6. 进阶:包签名、软件仓库与 CI 里的一次性打包
6.1 给包签名,别让 yum 或 apt 一直警告无签名
RPM 用 GPG 密钥签名:构建机生成密钥后,对产物执行rpm --addsign hello.rpm;DEB 侧用dpkg-sig -s builder hello.deb或debsign。签名后的包在客户端安装时会被信任链检查,内网仓库尤其需要。私钥一旦泄露,整个仓库的信任就归零,所以密钥要单独保管,不能跟着构建机一起漂移。
6.2 自建仓库,一条源解决内网分发
散装安装包只能救急,内网批量分发还是要建仓库。RPM 侧用createrepo生成 repodata,然后让客户端指向这个目录;DEB 侧用dpkg-scanpackages生成索引。最小命令:
# RPM 仓库 createrepo /srv/repo/rpm # DEB 仓库 dpkg-scanpackages /srv/repo/deb /dev/null | gzip > /srv/repo/deb/Packages.gz生成后配合 nginx 暴露目录,客户端分别写好 .repo 文件和 sources.list 条目。有了仓库,你才能把“下载一个包硬装”升级成“yum install 一条命令带依赖装完”。
6.3 在 CI 里两条命令打双格式
有了容器化,不再需要维护两台打包机。我在流水线里用的就是两个镜像各跑一条命令:
docker run --rm -v "$PWD":/src -w /src rockylinux:9 bash -c "rpmbuild -ba hello.spec" docker run --rm -v "$PWD":/src -w /src ubuntu:22.04 bash -c "dpkg-buildpackage -us -uc -b"每次构建都从干净镜像开始,彻底杜绝“昨天在打包机上手工装过什么”导致的玄学问题。产物固定留到 CI 的 artifacts 目录,打出来的包再进签名、进仓库,形成一条完整链路。
我自己最早做打包,就是把 tar 解压再压回去,结果第一个正式给团队的包就翻车:卸载残留、依赖乱跳、签名缺失,运维半夜抓狂。后来的习惯是:新包永远先做最小闭环,一个文件装上再卸掉,确认干净才补元数据;构建环境一旦被手工改过就不再信任,立刻换成容器重跑。希望帮到你。
本文还有配套的精品资源,点击获取