1. 项目概述:为什么 Spec 文件调试比想象中更考验人
只要你在 Linux 环境下打过包,就一定见过那个让人又爱又恨的 .spec 文件。它看起来就是一个普通的文本文件,几十行到几百行不等,但它在 RPM 构建体系里扮演的是“施工图纸”的角色:源码怎么解压、编译参数怎么传、安装到哪些路径、依赖声明哪些包、打包时哪些文件进 RPM,全部由它说了算。很多人第一次写 RPM 包时会觉得“不就是写个文本吗”,结果一跑 rpmbuild -ba,立刻被一堆报错打懵:%files 里写的文件不存在、BuildRequires 没写全导致编译中断、%install 阶段权限不对、Requires 自动生成的依赖跟预期不符……这些问题在写 Spec 的时候往往看不出来,只有跑到对应阶段才会暴露。
这篇文章不是讲 RPM 打包的八十种姿势,也不是把文档抄一遍。我想把这几年代维护 RPM 包、帮队友排查打包问题的经验整理成一个完整的调试框架,重点解决“写完 Spec 之后跑不通、跑通了打出的包又不符合预期”这两件事。文章按实际调试顺序展开:先讲清 Spec 文件的工作机制和调试思路,再拆解每个字段和阶段的坑点,最后用一个完整示例走一遍从报错到修复的全过程,并把高频问题整理成速查表。适合三类人看:刚入行要维护内部软件仓库的运维、需要给公司自研软件打 RPM 包的开发者、以及想系统理解 rpmbuild 阶段执行逻辑的打包爱好者。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计:先理解 rpmbuild 的阶段模型,再谈调试
调试是什么?本质上就是“把黑盒变成白盒”。Spec 文件之所以难调,是因为大多数人把它当成一个整体来跑,出了问题只能看到最后那一段报错,中间发生了什么完全不知道。要改变这种局面,就得先理解 rpmbuild 的执行模型:它把一次打包拆成多个独立阶段,并且分别提供对应的参数来单独执行某个阶段。
2.1 rpmbuild 的六个关键阶段
一次完整的 RPM 构建包含准备、编译、安装、二进制包生成、源码包生成等步骤,rpmbuild 用不同的参数让你可以精确控制:
rpmbuild -bp:只执行%prep阶段。解压源码、打补丁、生成BUILD目录下的源码树。rpmbuild -bc:执行%prep加%build。解压后并开始编译,但不安装。rpmbuild -bi:执行%prep、%build、%install。编译并把文件安装到BUILDROOT临时目录。rpmbuild -bb:执行完整流程并生成二进制 RPM 包。rpmbuild -bs:只生成源码 RPM 包(.src.rpm)。rpmbuild -ba:同时生成二进制 RPM 包和源码 RPM 包。
这个阶段模型是整个调试体系的地基。很多新手一上来就 rpmbuild -ba,报错了只能看到“打包失败”四个字,然后开始乱猜。正确做法是层层推进:先用 -bp 验证源码能否正常解压,再用 -bc 验证编译环境是否齐全,接着用 -bi 验证安装逻辑,最后才用 -bb 生成最终包。这样每推进一层,能确认的范围就更精确一点,排错效率高得多。
2.2 调试的核心原则:分层验证、逐步定位
我在实际调试中总结出的核心原则就八个字:分层验证,逐步定位。说得更直白一点,就是“永远让上一次成功的结果作为下一次的起点”。
举个例子。如果 %build 阶段报 gcc: command not found,你当然可以直接在 BuildRequires 里加上 gcc 再重跑。但如果你直接 rpmbuild -ba,每次都要把前面的 %prep 重新执行一遍,源码解压、补丁应用全重来,浪费时间不说,遇到大型项目可能一次构建要十几分钟,反复试错成本太高。而用 rpmbuild -bc,你只是把同样的步骤重复,定位就没那么直观了。
所以我的建议是:一开始就为调试建立一套“最小验证路径”。先把 Spec 写到能跑通 -bp 的程度,再逐步往后推。每个阶段一旦通过,就把该阶段的输出记下来,比如编译产物路径、安装目录结构,这些信息在后续排查时非常有用。
2.3 理解 %{buildroot} 与工作目录
很多人调试卡住,是因为没搞清楚 %{buildroot} 和当前工作目录的关系。简单来说:
%prep和%build阶段,工作目录是BUILD下的源码目录,比如~/rpmbuild/BUILD/hello-1.0/。此时你所有操作都发生在源码树里。%install阶段,工作目录同样是源码目录,但目标是把文件安装到BUILDROOT目录下。%{buildroot}指向类似~/rpmbuild/BUILDROOT/hello-1.0-1.x86_64/的路径,这个目录模拟了系统的根目录/,用于在打包前收集所有要进入 RPM 的文件。
理解这点很重要。比如在 %install 阶段看到 mkdir -p %{buildroot}/usr/bin,本质就是在临时根目录里建一个 usr/bin 目录。如果你直接写 mkdir -p /usr/bin,很有可能会写到宿主机的根目录里,这是绝对不允许的,正规做法是通过 DESTDIR 或 %{buildroot} 前缀来重定向安装路径。
调试时想确认 %{buildroot} 的真实值,可以用 rpm --eval '%{buildroot}',也可以临时在 Spec 里加一行 echo "buildroot is %{buildroot}"。我一般用后者,因为可以看到当前 Spec 上下文里的真实展开值。
3. 核心细节解析:Spec 文件的骨架与各阶段的坑点
3.1 头部字段:元信息不是小事
Spec 文件的头部字段负责描述包的基本信息。Name、Version、Release 三件套构成了 RPM 包版本标识的基础,任何一个字段出错都会直接影响包的安装、升级和卸载逻辑。比如 Release 忘了用 %{?dist} 宏,会导致同一个版本的包在不同发行版之间标记不一致,仓库同步时可能出现版本冲突。
Summary 和 %description 属于展示型信息,但有个值得注意的点:Summary 必须是一行,不能换行,否则会触发“Summary must not be empty”或解析错误;%description 则可以写多行,而且换行会被保留,最终用户用 rpm -qi 查看描述时会看到你写的排版。
License 字段不能随便填。现在很多发行版的审核体系会检查许可证合规性,常见值包括 GPLv3、MIT、Apache-2.0 等。如果你用的是 SPDX 标准术语,最好直接填 SPDX 标识符,比如 MIT 而不是 MIT License,这样更容易通过自动检查。
Source0 是源码文件位置,可以填本地文件名,也可以填 URL。调试时最常见的坑是:本地文件明明放在了 ~/rpmbuild/SOURCES/,但 Spec 里写的是 Source0: %{name}-%{version}.tar.gz,而实际文件名大小写或版本号不匹配。rpmbuild 在解析 %prep 时会提示源码文件找不到,问题不算难,但每次遇到都很烦。
3.2 依赖声明:BuildRequires 和 Requires 的调试重点
BuildRequires 是构建期依赖,Requires 是运行期依赖。很多打包失败的根源就藏在依赖声明里。
先说 BuildRequires。它的作用是告诉 rpmbuild 在构建环境中准备哪些编译工具和库。写少了,编译阶段会报“头文件找不到”、“库找不到”这类错误;写多了,构建环境被撑大,虽然不会直接失败,但在容器或 Mock 环境下会影响构建速度,也会让依赖分析变得复杂。
再说 Requires。现代 rpmbuild 会自动分析二进制文件的动态链接库依赖,比如 ldd 查出来的 .so 文件,以及脚本文件的解释器依赖,所以很多时候你不需要手动写 Requires。但自动分析也有失效的时候,比如:
- 程序运行时通过
dlopen加载库,动态链接分析抓不到。 - 脚本使用非标准解释器路径,或者依赖某个外部命令。
- 配置文件里引用了一个不存在的工具。
遇到这类情况,就需要手动在 Requires 里补上依赖。调试方法很简单:打出的 RPM 包用 rpm -qpR 查看自动生成的依赖列表,对照程序实际运行的行为,逐项核验。这一步我每次打包后必做。
3.3 %prep 阶段:源码解压与补丁应用的调试
%prep 是构建流程的第一步,负责把 SOURCES 目录里的源码包解压到 BUILD 目录,并应用补丁。
最常见的是 %setup -q 宏的使用。这个宏默认会解压 %{SOURCE0},并切换到解压后的目录。有几个细节需要特别注意:
- 默认情况下,
%setup假定解压出来的目录名是%{name}-%{version}。如果上游源码包的目录名不一样,比如hello-1.0.0或hello-master,就需要用%setup -q -n 实际目录名来指定。 - 如果源码包解压后不需要切换目录(比如是单文件脚本),可以用
%setup -c先建目录再解压,也可以干脆不用宏,直接用tar -xf手动处理。
补丁应用是另一个高频调试点。在 Spec 里写 Patch0: fix-xxx.patch,然后在 %prep 里写 %patch0 -p1。如果补丁文件放在 SOURCES 里,但忘了写 Patch0 声明,rpmbuild 不会报错,只是静默跳过;如果补丁路径或 -p 层级不对,会报“Reversed (or previously applied) patch detected”,这时候要看清楚补丁文件里的路径前缀,决定用 -p0 还是 -p1。
调试 %prep 时,我会在关键步骤前后加临时输出:
bash复制%prep
echo ">>> Current directory: $(pwd)"
%setup -q
echo ">>> After setup: $(pwd)"
ls -la
%patch0 -p1
echo ">>> After patch"
跑 rpmbuild -bp 时,这些 echo 会原样打印出来,配合 ls 的输出,能快速确认解压、目录切换、补丁应用是否都正确。确认无误后再把临时调试语句删掉。
3.4 %build 阶段:编译环境的调试
%build 阶段执行实际的编译命令,最常见的是 make 或 make %{?_smp_mflags}。%{?_smp_mflags} 会展开成 -jN,其中 N 是 CPU 核心数加一,用来并行编译加速。如果项目不支持并行编译,就得去掉这个宏,否则可能出现随机性的编译失败。
编译阶段排错,核心是看编译日志。日志信息量大,我一般这样处理:
- 先跑一次
rpmbuild -bc,把输出重定向到文件:rpmbuild -bc hello.spec > /tmp/build.log 2>&1。 - 然后从日志尾部往上翻,找到第一个
error:开头的行,这是最关键的线索。 - 根据错误内容决定是修改 Spec(比如补充
BuildRequires),还是修改源码(比如调整编译选项)。
常见编译错误类型包括:头文件缺失、链接库找不到、编译选项不兼容、Makefile 里路径写死。如果你发现编译能通过,但生成的可执行文件行为不对,那可能是编译参数的问题,比如没开某个宏开关、优化级别不对。调试这种问题,光看日志还不够,需要对比源码里的条件编译逻辑,确认传递的 CFLAGS 是否符合预期。
3.5 %install 阶段:安装脚本与目录结构的调试
%install 阶段负责把编译好的产物安装到 %{buildroot} 下。这是整个 Spec 文件中最容易写错也最需要小心的地方。
第一个大坑:用绝对路径安装。比如 make install PREFIX=/usr,虽然编译和安装都不会报错,但内容会直接装到系统根目录,污染你的构建机。正确做法是让安装命令支持 DESTDIR,比如:
bash复制%install
make install DESTDIR=%{buildroot}
如果你负责的项目 Makefile 不支持 DESTDIR,就得手动创建目录并拷贝文件:
bash复制%install
mkdir -p %{buildroot}%{_bindir}
install -m 755 src/hello %{buildroot}%{_bindir}/hello
第二个大坑:重复安装。有时候同一个文件被安装两次,或者一个安装步骤覆盖了另一个步骤的内容。rpmbuild 在 %install 阶段结束时会对 %{buildroot} 做完整性检查,如果有重复文件,会报“File listed twice”或者“File not found”之类的错误。排查时用 find %{buildroot} -type f 列出所有已安装文件,一目了然。
第三个大坑:权限和属主。RPM 包内文件的权限默认从 %{buildroot} 下文件的实际属性继承。如果你在 %install 里用了默认 umask,可能导致二进制文件没有执行权限。用 install -m 755 显式控制权限,是更稳妥的做法。
3.6 %files 阶段:文件列表与 %dir 的调试
%files 阶段列出最终 RPM 包要包含的所有文件。文件路径都是相对于 %{buildroot} 的,所以写成 /usr/bin/hello 而不是 %{buildroot}/usr/bin/hello。
常见的 %files 错误有这么几类:
- 文件列表里写的路径在
%{buildroot}下不存在,报“File not found”。 - 文件列表重复包含某个文件,报“File listed twice”。
- 目录被隐式包含,报“Directory not found”或者被自动忽略。
- 打包时发现某个未声明文件没有归属。
调试 %files 最直接的方式是先看 %{buildroot} 里到底有哪些文件。跑完 rpmbuild -bi 后,直接进入对应目录:
bash复制find ~/rpmbuild/BUILDROOT/ -type f | sort
对照这个列表,再检查 Spec 里的 %files 段,就能快速找出遗漏或多余。对于需要整个目录打包的场景,可以用:
spec复制%files
%{_datadir}/hello/*
或者把目录本身声明为 %dir:
spec复制%files
%dir %{_datadir}/hello
%{_datadir}/hello/README
需要特别说明的是,%{_datadir}/hello/* 这种写法默认不包含隐藏文件。如果你要包含目录内所有文件包括隐藏文件,需要用 %{_datadir}/hello/.* 补上,但这样 . 和 .. 又会被包含,得写排除规则。这种细节看起来琐碎,实际打包时踩中的人非常多。
3.7 宏的使用与调试:rpm --eval 是你的好帮手
RPM 的宏系统非常强大,但对新手也是一个理解障碍。%{name}、%{version} 这种内建宏会从 Spec 头部字段取值;%{_bindir}、%{_libdir}、%{_datadir} 这类路径宏则由 rpm 配置定义,在不同发行版上展开结果可能不一样。
调试宏可以用两个命令:
bash复制rpm --eval '%{_bindir}'
rpm --showrc | grep '_bindir'
--eval 可以直接展开宏的最终值,--showrc 可以看到宏的来源定义。我在写 Spec 时经常先跑一下 rpm --eval,确认路径宏展开结果符合预期,避免凭记忆写错路径。
自定义宏也需要注意作用域。Spec 里用 %define 定义的宏只在当前 Spec 文件内有效;在命令行用 --define 'macro value' 可以覆盖同名宏,这对调试很有用。比如我想测试不同 Release 值对包名的影响,可以这样干:
bash复制rpmbuild -ba hello.spec --define 'release 2'
这样不用改文件,就能快速试出不同配置的结果。
4. 实操过程:从零调试一个完整的 Spec 文件
理论讲了不少,现在用一个实际例子把整个调试流程串起来。假设我们要打包一个简单的 C 语言程序 hello,源码目录结构如下:
code复制hello-1.0/
Makefile
src/
hello.c
README.md
4.1 准备源码与目录
先把源码整理成 tar 包,放到 ~/rpmbuild/SOURCES/:
bash复制mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
tar -czf ~/rpmbuild/SOURCES/hello-1.0.tar.gz hello-1.0
如果 rpmbuild 命令提示找不到,说明还没有安装 rpm-build 包,先安装:
bash复制sudo dnf install rpm-build
注意,rpmbuild 的默认工作目录是 ~/rpmbuild,也可以通过在 ~/.rpmmacros 里写 %_topdir /path/to/your/rpmbuild 自定义目录。调试时我一般保持默认,减少变量。
4.2 编写第一个 Spec 文件
在 ~/rpmbuild/SPECS/hello.spec 写如下内容:
spec复制Name: hello
Version: 1.0
Release: 1%{?dist}
Summary: A simple hello program
License: GPLv3
URL: https://example.com/hello
Source0: hello-1.0.tar.gz
BuildRequires: gcc
Requires: glibc
%description
A minimal hello program used to demonstrate RPM Spec file debugging.
%prep
%setup -q
%build
make %{?_smp_mflags}
%install
make install DESTDIR=%{buildroot}
%files
%{_bindir}/hello
%license LICENSE
%doc README.md
%changelog
* Wed Jan 01 2025 Your Name <you@example.com> - 1.0-1
- Initial package
这里假设源码里带了 LICENSE 文件,Makefile 的 install 目标支持 DESTDIR。如果这两个假设不成立,后面的调试过程正好能展示怎么发现问题。
4.3 第一轮调试:rpmbuild -bp
我从来不在第一次就直接跑 -ba,而是先跑 -bp:
bash复制cd ~/rpmbuild/SPECS
rpmbuild -bp hello.spec
输出如果报“Bad source: ... No such file or directory”,说明 Source0 写错了,或者 tar 包没有放在 SOURCES 下。检查文件路径和文件名,修正后重跑。
如果 %setup -q 成功,BUILD 目录下会出现 hello-1.0 目录。这时候我习惯进目录看一眼:
bash复制cd ~/rpmbuild/BUILD/hello-1.0
ls -la
如果发现目录名不是 hello-1.0,比如源码 tar 包解压出来叫 hello-master,那 %setup -q 会失败。解决办法是在 %setup 后加 -n 参数:
spec复制%setup -q -n hello-master
这轮结束,%prep 阶段通过。
4.4 第二轮调试:rpmbuild -bc
接着跑编译阶段:
bash复制rpmbuild -bc hello.spec
这一步常见的问题是 Makefile 本身不健全。比如 Makefile 里 CFLAGS 没有定义,或者链接库缺失。我们的示例项目很简单,大概率能一次通过。但如果源码是外来的,编译报错可能五花八门。我的建议是不要急着在 Spec 层面找原因,先把报错信息贴到搜索引擎或项目 Issue 里查,确认是环境问题还是代码问题。只有在确认是构建环境缺少依赖时,才往 BuildRequires 里加内容。
比如报错:
code复制src/hello.c:2:10: fatal error: ncurses.h: No such file or directory
说明依赖 ncurses 开发库,在 BuildRequires 里加上:
spec复制BuildRequires: ncurses-devel
再重跑 -bc,直到编译通过。
4.5 第三轮调试:rpmbuild -bi
编译通过后,进入安装阶段验证:
bash复制rpmbuild -bi hello.spec
如果报“No such file or directory”且路径指向 %{buildroot}/usr/bin/hello,多半是 make install 没有按 DESTDIR 安装。比如 Makefile 里写的是:
make复制install:
cp hello /usr/bin/hello
这种写法完全无视 DESTDIR 变量,文件被直接装到构建机的 /usr/bin 下了。更隐蔽的是那种写了 DESTDIR 但路径拼接错误的 Makefile。解决办法有两种:
第一种,改 Makefile,让它支持 DESTDIR:
make复制install:
install -D -m 755 hello $(DESTDIR)/usr/bin/hello
第二种,不在 %install 里依赖 Makefile 的 install 目标,改为手动安装:
spec复制%install
mkdir -p %{buildroot}%{_bindir}
install -m 755 src/hello %{buildroot}%{_bindir}/hello
手动安装虽然啰嗦,但对调试阶段来说最可控。等确认整个流程没问题后,再考虑优化成 make install。
调试 %install 时,我习惯在阶段最后加一段临时输出:
spec复制%install
...
echo "===== BUILDROOT content ====="
find %{buildroot} -type f
这样每次跑 -bi 都能立刻看到安装结果,不用另外开终端。
4.6 第四轮调试:rpmbuild -bb
-bi 通过后,基本可以跑完整构建了:
bash复制rpmbuild -bb hello.spec
这阶段最常见的错误集中在 %files 列表,因为之前 -bi 只做安装,不做文件清单校验。报错示例:
code复制error: File not found: /home/user/rpmbuild/BUILDROOT/hello-1.0-1.x86_64/usr/bin/hello
error: File not found: /home/user/rpmbuild/BUILDROOT/hello-1.0-1.x86_64/usr/share/licenses/hello/LICENSE
第一个说明路径写错了,我实际安装到了别的位置;第二个说明 LICENSE 文件不存在,源码 tar 包里没有。根据实际文件位置修正 %files 段即可。
如果一切正常,RPMS/x86_64/ 下会生成 hello-1.0-1.el8.x86_64.rpm(发行版标识随系统不同而变)。
4.7 验证生成的 RPM 包
打包成功不等于包可用。我一般做四步验证:
bash复制# 查看包基本信息
rpm -qip hello-1.0-1.el8.x86_64.rpm
# 查看包内文件列表
rpm -qlp hello-1.0-1.el8.x86_64.rpm
# 查看包声明的依赖
rpm -qpR hello-1.0-1.el8.x86_64.rpm
# 查看包的脚本片段(如果有 %post 之类的)
rpm -qp --scripts hello-1.0-1.el8.x86_64.rpm
重点看 rpm -qpR 的输出是否包含预期依赖,有没有多余的依赖。如果发现自动生成的 Requires 里出现了 libfoo.so.1() 这种依赖,但程序实际不需要,可以通过 %global _use_internal_dependency_generator 0 关闭自动依赖生成,再手动声明 Requires。不过这属于高级操作,正常情况下不要轻易关闭自动依赖分析,否则很容易漏依赖。
4.8 使用 rpmlint 做静态检查
rpmlint 是调试 Spec 的利器,它会把 Spec 文件和生成的 RPM 包都检查一遍,提示格式问题、权限问题、重复文件等。打包前先跑一遍:
bash复制rpmlint hello.spec
rpmlint RPMS/x86_64/hello-*.rpm
输出里的 E: 是错误,会影响打包或安装;W: 是警告,不一定致命,但最好都看一眼。比如常见警告 no-documentation、no-changelogname-tag,都是因为元信息不全导致的。
5. 常见问题与排查技巧实录
我把自己遇到过的、以及帮别人排查过的高频问题整理成了一张速查表,每个问题都给出排查思路和解决方向,方便你直接对照。
| 问题现象 | 可能原因 | 排查手段 | 解决思路 |
|---|---|---|---|
rpmbuild 命令不存在 |
未安装 rpm-build | 执行 rpm -qa | grep rpm-build |
用 dnf/yum 安装 rpm-build |
Bad source: hello-1.0.tar.gz |
Source0 文件名或路径不对 |
检查 SOURCES 目录是否有该文件 |
修正 Source0 或把文件放到正确目录 |
%setup 解压后目录名不匹配 |
源码包解压目录不是 name-version |
在 BUILD 目录查看实际目录名 | 用 %setup -n 指定实际目录名 |
| 编译报缺少头文件 | BuildRequires 缺少 -devel 包 |
根据报错文件名反查属于哪个包 | 用 dnf provides */头文件 查找后添加依赖 |
| 编译通过但安装报文件不存在 | make install 未正确使用 DESTDIR |
查看 Makefile 的 install 目标 | 改用 install 命令手动安装,或修复 Makefile |
%files 报 File not found |
文件列表路径与 %{buildroot} 下实际路径不一致 |
用 find %{buildroot} 查看实际文件树 |
修正 %files 中的路径 |
| 安装时文件被莫名覆盖 | %install 里多次复制同一文件 |
检查安装命令和 Makefile 逻辑 | 统一安装方式,避免重复安装 |
| 生成的 RPM 依赖过多或过少 | 自动依赖分析结果不符合预期 | rpm -qpR 查看依赖列表 |
必要时手动调整依赖声明 |
%changelog 解析错误 |
日期格式错误、时间在未来 | 查看报错行 | 确保日期格式为 Day Mon DD YYYY,时间不超前 |
| 打出的包无法安装 | Requires 与系统实际版本冲突 |
rpm -qpR + rpm -q 对比 |
调低版本要求或修复版本声明 |
除了表里的内容,还有几个经验值得单独展开。
5.1 时间对不上:%changelog 格式问题
这个报错出现得非常频繁。%changelog 的日期格式必须是类似 Wed Jan 01 2025 的格式,不能是 2025-01-01。如果你的系统语言不是英文,可能还要注意月份和星期的英文缩写。写错之后 rpmbuild 会直接报:
code复制error: bad date in %changelog
解决方式很简单,打开命令行执行:
bash复制date "+%a %b %d %Y"
把输出结果抄到 %changelog 里,保证格式绝对正确。
5.2 rpm --eval 与宏排查
有时 Spec 里的宏展开结果跟预期不符,比如:
spec复制%{_unitdir}
展开后指向 /usr/lib/systemd/system,但在某个老版本系统上,这个宏没有定义,会原样输出 %{_unitdir} 导致路径错误。排查时用:
bash复制rpm --eval '%{_unitdir}'
如果输出不是期望路径,说明宏没定义或定义被覆盖。可以在 Spec 开头手动定义:
spec复制%global _unitdir /usr/lib/systemd/system
宏问题的另一个排查场景是 %{?dist}。这个宏用于在 Release 后加上发行版标识,比如 el8、fc37。用 rpm --eval '%{?dist}' 可以确认当前系统的发行版宏值。如果输出为空,说明没有定义,包名里就不会带发行版标识,这可能会影响仓库管理时的版本排序。
5.3 构建环境与 Mock 工具
有时候本地构建能通过,但交给 CI 或仓库构建系统时却失败,原因往往是构建环境不干净。rpmbuild 默认复用本机的库和工具,本机装了很多开发包,可能掩盖了 BuildRequires 声明不全的问题。这时候可以用 mock 工具在干净环境中构建:
bash复制mock -r epel-8-x86_64 --buildsrpm --spec hello.spec --sources ~/rpmbuild/SOURCES/
Mock 可以用 chroot 隔离构建环境,只安装 BuildRequires 声明的依赖。它能暴露出很多本地构建看不出来的问题,比如依赖声明缺失、路径硬编码、编译时依赖于本机已安装的库等。如果你在维护面向公开仓库的 RPM 包,我建议统一用 Mock 做最终验证。
5.4 使用 rpmbuild -bl 快速校验 %files
rpmbuild 提供了一个容易被忽略的参数 -bl,它只校验 %files 列表中的文件是否存在于 %{buildroot},不执行编译和安装。前提是你已经跑过一次 -bi,BUILDROOT 目录里有真实文件。用法:
bash复制rpmbuild -bl hello.spec
如果 %files 列表有问题,它会直接报错,速度非常快。我通常把 -bi 和 -bl 连用:先安装,再校验文件列表,最后跑 -bb。这样可以把“文件没装对”和“文件列表写错”这两类问题拆开,排错更清晰。
5.5 构建日志的保存与复盘
调试过程免不了反复试错,我强烈建议每次构建都把日志保存下来。比如:
bash复制rpmbuild -ba hello.spec > /tmp/rpmbuild-$(date +%Y%m%d-%H%M%S).log 2>&1
日志文件名带上时间戳,后面回溯问题时能清楚知道是哪次构建产生的。排查复杂错误时,对比多次构建日志之间的差异,往往能快速锁定改动引发的新问题。
最后说几句实操体会
做 RPM 打包这些年,我有一个很深的感受:Spec 文件调试的难点从来不是语法,而是“不知道当前阶段发生了什么”。很多人被报错搞晕,就是因为把整个构建过程当成一个黑盒,看不到中间状态。这篇文章希望帮你打破这个黑盒。我自己的调试节奏已经固定成一套流程:-bp 验证解压、-bc 验证编译、-bi 验证安装、-bl 校验文件列表、-bb 生成最终包,最后再用 rpmlint 和 rpm -qp* 做质量检查。这套流程看起来多跑了几次命令,但实际省下的排错时间远超这些开销。
每次打包之前还有一个习惯:先看一眼 SOURCES 目录里的文件列表,确认文件名和版本号没有写错。很多看起来莫名其妙的“源码找不到”错误,最后发现都是源文件名和 Spec 里的 Source0 差了一个字符。这种问题排查起来最不值。
如果你已经在维护不少 RPM 包,还可以考虑把 Spec 文件纳入版本管理,每次修改都留 commit 记录。这样某个包如果从“能构建”变成“构建失败”,用 git diff 看看 Spec 改了什么,排查效率会高很多。我踩过几次“明明没改为什么失败了”的坑之后,就养成了这个习惯,现在基本不靠猜了。
打包调试没有太多玄学,就是把大问题拆成小阶段,每一步都验证到位,问题自然会浮现出来。希望这篇文章能让你少走一些我走过的弯路。
