在 RPM 构建生态中,
gbp(Git-BuildPackage)是广泛使用的源码管理工具,它通过 Git 仓库维护上游源码和打包文件。然而,当上游组件拆分为多个压缩包(例如selinux-policy包含主策略、contrib 模块、容器策略等多个 tarball)时,gbp默认只处理主压缩包的导入,其余压缩包只能以普通 Source 文件形式存在,导致开发人员期望的“直接修改 Git 文件”工作流与构建时的文件分布产生冲突。本文以一次真实的编译失败为例,详细记录问题定位、根因分析及最终解决方案,并为类似多源组件提供可复用的处理思路。
1. 背景与构建流程
我们维护一个基于Koji的 RPM 构建系统,使用Mock提供干净的 buildroot。本次构建的组件是selinux-policy,版本为3.14.3-117.0.1,其源码包(SRPM)解压后,SOURCES目录下包含三个压缩包:
selinux-policy-426c028.tar.gz—— 主策略源码(核心策略模块)selinux-policy-contrib-c6da44c.tar.gz—— 社区贡献的模块(存放于policy/modules/contrib/)container-selinux.tgz—— 容器相关策略(通常放入其他目录)
在%prep阶段,spec 文件会通过%setup解压主压缩包,再手动解压其他压缩包到对应位置,最终合并成一个完整的策略源码树。
开发团队使用gbp进行源码管理:通过gbp import-orig将主压缩包导入 Git 仓库,生成上游分支(upstream/)和主分支(master/),并将 spec 文件和补丁作为额外的提交(“buildfiles”)。开发人员习惯在 Git 仓库中直接修改策略文件(如.te、.if),然后通过gbp buildpackage生成新的 SRPM 提交构建。
2. 问题现象
在一次日常构建中,Koji 任务在%build阶段执行make conf时失败,报错如下:
make: *** No rule to make target 'policy/modules/contrib/metadata.xml', needed by 'tmp/admin.xml'. Stop.构建日志显示,make试图生成tmp/admin.xml,但依赖的policy/modules/contrib/metadata.xml文件不存在,且 Makefile 中没有生成该文件的规则。手动登录 Mock chroot 检查构建目录:
bash-4.4# ls -l policy/modules/contrib/total0目录为空,显然contrib模块的内容没有被正确解压到构建目录中。
3. 初步排查与假设
3.1 检查 spec 文件的%prep段
我们首先检查 spec 文件,发现其中确实包含了解压selinux-policy-contrib-c6da44c.tar.gz的命令,例如:
%prep %setup -q -n selinux-policy-%{version} tar -xzf %{SOURCE1} -C policy/modules/ --strip-components=1 # 类似处理 container-selinux.tgz理论上,该命令会在%setup之后将 contrib 内容解压到policy/modules/contrib/。但为什么实际构建目录中却没有呢?
3.2 验证源码包内容
我们将 SRPM 下载到本地,用rpm -ivh解压查看SOURCES目录,发现selinux-policy-contrib-c6da44c.tar.gz确实存在。但再用rpmbuild -bp模拟%prep阶段,却发现解压命令失败或未执行。进一步检查 spec 中的宏定义,发现%{SOURCE1}并未正确指向该文件,因为 Source 编号可能有误。
但事实并非如此简单,因为构建在之前版本是成功的,本次失败发生在开发人员调整了源码管理方式之后。
4. 根因分析——gbp 导入机制与多压缩包冲突
4.1 gbp 的导入行为
gbp import-orig的核心功能是将一个上游压缩包(通常为主 tarball)导入 Git 仓库,其默认行为:
- 解压主压缩包到临时目录。
- 将解压后的内容作为新的上游提交(
upstream/分支)。 - 将当前工作目录(可能包含 spec、补丁等)作为构建文件提交(通常为
master分支的第二个提交)。
关键点:gbp只将主压缩包的内容纳入上游源码树。其他 Source 文件(如SOURCE1、SOURCE2)虽然被复制到SOURCES目录,但它们不会被自动解压并合并到 Git 工作树中。它们只是作为普通文件存在于仓库中(通常放在debian/或SOURCES/目录,或者通过gbp的--source选项指定),但开发人员在 Git 工作区中直接修改策略文件时,并不能直接修改这些压缩包内的文件——因为压缩包本身是二进制文件,不易修改。
4.2 我们的错误做法
为了能够直接在 Git 中修改contrib模块的文件,开发人员曾采用了一种不规范的方式:
- 手动解压
selinux-policy-contrib-c6da44c.tar.gz到policy/modules/contrib/。 - 将这些文件添加并提交到 Git 仓库(位于
buildfiles提交中,即 commit2)。 - 之后,开发人员直接修改这些文件,并提交变更。
然而,gbp buildpackage在生成 SRPM 时,默认行为是:
- 从
upstream/分支导出主源码(即 commit1 对应的树)作为.orig.tar.gz。 - 将
master分支上的差异(包括所有提交)生成为补丁文件(patch),但并不会将 buildfiles 提交中的新增文件(如 contrib 内容)合并到源码树中。除非使用--export-dir并合并,但默认不会。
因此,最终生成的 SRPM 中,%prep阶段解压的主 tarball 只包含 commit1 的内容(即没有 contrib),而 spec 中的tar -xzf %{SOURCE1}虽然试图解压,但该 Source 文件虽然存在于 SRPM 中(因为 spec 中声明了 Source1),可是在 gbp 构建时,Source1 并没有被正确打包进 SRPM?实际上,如果 spec 中正确声明了 Source1,gbp buildpackage会将其包含在 SRPM 中。但问题在于,我们的 spec 中 Source1 的路径或名称可能写死了,而 gbp 生成的 SRPM 中的 Source1 文件名可能与预期不符,或者解压路径错误。
更根本的矛盾在于:开发人员希望在 Git 中直接修改源码文件,但多压缩包的原始设计要求文件来源于多个 tarball,而这些 tarball 在 gbp 导入时并未被合并到工作树中,导致开发环境与构建环境文件分布不一致。
4.3 验证问题
在 Mock chroot 中,我们检查了 SRPM 解压后的SOURCES目录:
ls/builddir/build/SOURCES/ selinux-policy-426c028.tar.gz selinux-policy-contrib-c6da44c.tar.gz container-selinux.tgz这些文件都存在。但%prep执行后,为什么contrib还是空的?进一步检查 spec 中的%setup宏,发现主 tarball 被解压到selinux-policy-426c028目录,而tar -xzf %{SOURCE1}是在该目录内执行的,但%{SOURCE1}的值可能被宏定义为完整的路径,但在 Koji 构建环境中,%{SOURCE1}的展开可能不正确(例如,如果 Source1 的编号写错了)。
最终,我们定位到 spec 中 Source1 的声明和引用不一致,导致解压命令实际上解压到了一个错误的目录或根本没有执行。修复后,contrib文件得以正常出现,但这只是临时措施。
根本问题:只要源码来自多个压缩包,开发人员就无法在 Git 工作树中直接修改所有文件,因为其他压缩包的内容并未进入 Git 版本控制(或仅作为二进制文件存储),这既不利于代码审查,也难以追踪修改历史。
5. 解决方案的设计与选择
我们需要一个能够同时满足以下条件的方法:
- 构建时能够获得完整的源码树(所有压缩包内容均到位)。
- 开发人员可以在 Git 中直接修改任意策略文件(包括 contrib 模块),且修改能体现在最终的构建产物中。
- 不破坏 gbp 的工作流,保持与上游版本同步的便利性。
我们评估了三种思路:
5.1 方案一:保持多 Source,在 spec 中分别解压(现状修复)
做法:确保 spec 中的 Source 声明正确,并精确控制解压路径。构建时依然依赖多个压缩包。开发人员修改文件时,需要手动解压相关压缩包、修改、再重新打包,或者通过 git 管理压缩包内的文件(但难以追踪)。
缺点:开发流程繁琐,且修改容易遗漏,不符合我们的开发习惯。
结论:不采纳。
5.2 方案二:合并多个压缩包为单一主压缩包
做法:在导入上游时,先解压所有压缩包,合并成一个完整的源码树,然后再打包成单一 tarball,供gbp import-orig使用。这样,所有文件都进入同一个上游提交,开发人员可以直接修改任何文件。
优点:
- 开发体验一致。
- 构建时只需解压一个 tarball,简单可靠。
- 代码版本历史清晰。
缺点:
- 需要额外工作来合并多个压缩包(可能需要脚本)。
- 当上游发布新版本时,需要重新合并,增加了维护负担。
- 可能影响与官方上游的差异跟踪,但可以通过 patch 管理。
结论:可行,但需要团队投入精力维护合并脚本。
5.3 方案三:将其他压缩包内容作为补丁(patch)管理
做法:将contrib和container-selinux的内容解压后,作为一组新增文件,通过gbp的补丁机制(即master分支上的提交)来管理。具体来说:
- 使用
gbp import-orig仅导入主 tarball。 - 在
master分支上创建一个提交,将其他压缩包的内容以新增文件的形式加入(而非二进制压缩包)。 - 在 spec 中,不再需要
Source1,而是通过Patch来应用这些新增文件(或者直接利用gbp的补丁功能,在%prep中自动应用所有补丁)。
缺点:补丁集可能庞大,且新增文件数量多,补丁管理复杂度增加。但gbp本身支持补丁队列,可以接受。
优点:开发人员可以直接修改 Git 中的文件,无需额外打包。
结论:推荐采用此方案,因为它完全融入了 gbp 的工作流,且易于追踪变更。
5.4 最终决策
我们选择了方案三,并进行了具体实施:
- 导入主 tarball:
gbp import-orig selinux-policy-426c028.tar.gz - 解压
selinux-policy-contrib-c6da44c.tar.gz到policy/modules/contrib/,解压container-selinux.tgz到适当位置。 - 将所有新增文件添加到 Git,并提交(可拆分成多个逻辑补丁)。
- 在 spec 文件中,移除
Source1和Source2的声明及对应的%prep解压命令,因为所有文件已存在于源码树中。 - 配置
gbp使其在生成补丁时包含这些新增文件(默认会将其视为 patch 的一部分)。 - 后续开发人员直接在 Git 中修改这些文件,
gbp buildpackage会自动生成包含所有变更的补丁,构建时%setup解压主 tarball,然后%patch应用所有补丁(包括新增文件),最终得到完整的源码树。
6. 验证与实施结果
按照方案三调整后,我们进行了本地测试:
- 使用
gbp buildpackage生成 SRPM,成功将contrib和container文件包含在补丁中。 - 在 Mock 环境中执行
make conf,policy/modules/contrib/metadata.xml文件已存在,make顺利完成。 - 后续 Koji 构建全部通过。
开发团队也反映,现在可以直接在 Git 仓库中修改任何策略文件,提交后即生效,无需额外打包步骤,极大提升了效率。
7. 总结与经验提炼
7.1 问题根源
- 多压缩包结构与gbp 单压缩包导入模型存在冲突,导致部分源码无法进入版本控制的“可编辑”区域。
- 之前通过将其他压缩包内容混入 buildfiles 提交的做法,未能正确影响
gbp打包行为,造成构建环境源码缺失。
7.2 解决方案核心思路
- 将所有源码文件纳入 Git 版本控制,而非依赖外部 Source 压缩包。
- 利用 gbp 的补丁机制,将新增文件作为补丁应用,确保开发与构建环境一致。
7.3 可复用的方法论
当遇到类似多源组件时,可按以下步骤决策:
- 分析构建依赖:明确构建需要哪些文件,它们分别来自哪些压缩包。
- 评估 gbp 导入限制:理解
gbp默认只处理主 tarball,其他 Source 只能作为附加文件。 - 选择合并策略:
- 若上游官方提供单一 tarball,应尽量使用官方方式。
- 若必须多压缩包,优先考虑将其他包的内容转换为 Git 补丁,或合并为单一 tarball 后导入。
- 调整 spec:移除不必要的
%setup解压步骤,改用%patch或直接使用%autosetup配合补丁。 - 验证:在本地和 Koji 环境中分别测试,确保构建可复现。
7.4 对团队的长期建议
- 规范化上游源码获取方式:尽量从官方获取单一源码包,或由团队内部维护一个“合并版”的上游仓库。
- 充分利用 gbp 的补丁功能:将定制化修改以补丁形式管理,使上游升级时合并更清晰。
- 构建前添加文件完整性检查:在
%prep中增加条件判断,提前发现缺失文件,避免构建到一半失败。
结语
本次排查不仅解决了make报错的问题,更从根本上理顺了多压缩包组件在 gbp 工作流中的管理方式。通过将分散的源码统一纳入 Git 版本控制,我们实现了开发与构建环境的完美对齐,降低了维护成本,也为未来类似组件提供了可参考的模板。希望本文的分享能为遇到同样困境的开发者带来启示。
标签:#Makefile #RPM #Koji #gbp #多源码包 #构建问题