RPM 构建产生的文件远不止一个 .rpm 包,源码包、调试符号、构建日志、SRPM 以及仓库元数据都需要被有序管理,否则升级和维护成本会迅速上升。在 Fedora 生态中,Mock、Koji 和 createrepo_c 构成了管理构建产物的核心工具链,但工具之间的衔接往往被忽视。很多人以为构建结束只要把二进制包拷贝到共享目录就万事大吉,结果后续发现调试符号缺失、旧版本残留导致依赖解析混乱,甚至因为元数据不更新而无法用 dnf 安装最新包。本文会把这些环节拆开,给出可落地的做法。

本地构建产物的隔离与收集:Mock 的实践
直接在宿主机上执行 rpmbuild 会把大量编译依赖和中间文件散落到系统目录,时间一长就会让开发机变得臃肿且不可复现。Mock 工具的作用是在 chroot 环境中完成构建,每个构建对应一个干净的根目录,宿主机的软件包状态不会受到任何影响。安装 Mock 之后,可以先把自己加入 mock 用户组,然后通过 mock -r fedora-39-x86_64 --init 初始化一个基础环境,再使用 mock -r fedora-39-x86_64 --rebuild /path/to/source.rpm 来生成二进制 RPM。构建结束后,产物默认位于 /var/lib/mock/fedora-39-x86_64/result/ 目录,里面除了 .rpm 文件之外,还包括 build.log、root.log 和 state.log。
这些日志的价值常常被低估。当构建失败时,build.log 记录了完整的编译过程,root.log 则展示了 chroot 内安装的软件包列表,能够帮助快速定位是依赖缺失还是配置错误。为了方便后续审计,建议在构建脚本里把 result 目录整体打包并附带一个包含构建时间、Git 提交哈希的 manifest 文件。下面这个示例展示了如何在构建完成后收集产物并生成清单:
#!/bin/bash
MOCK_CONFIG=fedora-39-x86_64
RESULT_DIR=/var/lib/mock/${MOCK_CONFIG}/result
OUTPUT_DIR=/srv/build-artifacts/$(date +%Y%m%d-%H%M%S)
mkdir -p ${OUTPUT_DIR}
mock -r ${MOCK_CONFIG} --rebuild $1
cp -a ${RESULT_DIR}/* ${OUTPUT_DIR}/
cd ${OUTPUT_DIR} && sha256sum *.rpm *.src.rpm > SHA256SUMS
echo "build-time: $(date -Iseconds)" > BUILD_INFO
echo "git-commit: $(git rev-parse HEAD)" >> BUILD_INFO
Mock 还支持通过配置文件定制构建依赖和挂载目录。创建 /etc/mock/fedora-39-x86_64.cfg 的副本后,可以添加 config_opts['plugin_conf']['bind_mount_opts']['dirs'].append(('/srv/cache', '/srv/cache')) 来挂载宿主机缓存目录,避免每次构建都重新下载相同的依赖包。这部分缓存虽然属于中间产物,但保留下来能显著缩短后续构建时间,因此不宜随 result 目录一并清理,而应该单独规划磁盘空间。
分布式构建与状态追踪:Koji 的使用
当项目规模变大,单机 Mock 构建就会成为瓶颈,尤其是需要同时产出 x86_64、aarch64 等多种架构的包时。Fedora 官方使用的 Koji 系统可以把构建任务分发到多台构建节点上,并且自动处理签名、仓库更新和状态通知。个人或小团队可以通过 Koji 的命令行客户端与现有 Koji 实例交互,例如 Fedora 的公共 Koji 允许开发者提交 SRPM 进行构建,但更常见的是在企业内部搭建一套私有 Koji 服务。
Koji 的核心概念包括 task、build、tag 和 target。提交构建时使用 koji build <target> <srpm-url>,例如 koji build f39-candidate /path/to/package.src.rpm。构建任务会被拆分成多个子任务,包括构建 SRPM、构建各架构 RPM、生成调试符号包以及最终打 tag。通过 koji watch-task <task-id> 可以实时跟踪状态,而 koji list-builds --package=myapp 可以查询历史构建记录。下面演示如何提交一个构建任务并轮询结果:
koji build f39-candidate myapp-1.2.3-1.fc39.src.rpm
TASK_ID=$(koji latest-build --package=myapp --tag=f39-candidate | awk '{print $1}')
koji watch-task ${TASK_ID}
if koji taskinfo ${TASK_ID} | grep -q 'SUCCESS'; then
koji download-build --arch=x86_64 myapp-1.2.3-1.fc39
else
koji buildlog ${TASK_ID}
fi
Koji 构建产物的一个优势是它会自动保留每个构建的完整上下文,包括构建根、使用的 SRPM、构建日志和所有子包的哈希值。对于需要长期存档的场景,可以利用 koji download-build --arch=noarch --arch=x86_64 --debuginfo myapp-1.2.3-1.fc39 把二进制包和调试符号一并下载到本地。需要注意的是,Koji 服务器上的构建产物会占用大量空间,尤其是调试符号包常常比主包大好几倍,因此要结合 tag 的保留策略定期清理旧构建,而不是无限制地累积。
仓库管理与生命周期清理:从 createrepo_c 到自动化
构建产物只有进入一个可用的 DNF 仓库,才能被其他机器或容器方便地安装。手工把 RPM 文件丢到 HTTP 目录里并不能让 dnf 识别,必须使用 createrepo_c 生成仓库元数据。基本用法是 createrepo_c /srv/repo/fedora/,它会扫描该目录下的 RPM 文件并创建 repodata/ 子目录,里面包含 repomd.xml 以及各个软件包的依赖关系信息。每次增加或删除 RPM 文件后,都需要重新执行该命令,或者加上 --update 参数以增量方式更新元数据。
一个常见的错误是在添加新构建的 RPM 时,没有先移走旧版本,导致仓库里同时存在多个版本的同一软件包。dnf 默认会根据版本和发布号选择最高的一个,所以通常不会装错,但仓库体积会迅速膨胀。为了维持仓库整洁,推荐在每次发布新版本后,自动删除上一个版本对应的 RPM 文件,并重新生成元数据。下面的脚本演示了如何只保留某个软件包的最新两个版本,其余移至归档目录:
REPO_DIR=/srv/repo/fedora
ARCHIVE_DIR=/srv/archive/fedora
PACKAGE_NAME=myapp
ls ${REPO_DIR}/${PACKAGE_NAME}-*.rpm | sort -V | head -n -2 | while read old_rpm; do
mv ${old_rpm} ${ARCHIVE_DIR}/
done
createrepo_c --update ${REPO_DIR}
仓库的客户端配置需要指向 file:// 或 https:// 地址,并设置合理的 gpgcheck 策略。如果构建产物经过 GPG 签名,可以在仓库配置中开启 gpgcheck=1,同时通过 gpgkey 指定公钥地址。未签名的包建议至少在内网环境中使用 gpgcheck=0,但不要把这种仓库暴露给不受信任的网络。另外,调试符号包虽然体积大,但对排障很有价值,可以将其单独放在一个仓库目录,避免影响主仓库的元数据更新速度。
对于构建产物占用的磁盘空间,可以通过 dnf repoquery 定期检查哪些包已经不再被任何依赖引用,进而决定是否归档或删除。结合 systemd timer 或 cron 任务,每晚自动执行一次清理和元数据更新,能够把维护成本降到最低。归档目录建议使用只读挂载或对象存储,防止误删,同时保留至少三个月的构建记录以便回滚。这样一套流程跑顺之后,发版时只需要关心新版本的 RPM 是否成功入库,而不用再手动处理散落的文件。