首页
/ Homebrew 维护者专项故障处理:bottle 发布失败后的回滚重放与陌生构建失败排查

Homebrew 维护者专项故障处理:bottle 发布失败后的回滚重放与陌生构建失败排查

2026-09-07 20:25:49作者:瞿蔚英Wynne

本文面向拥有 homebrew/core(或任意 tap)写权限的 Homebrew 维护者,系统梳理 docs/Common-Issues-for-Maintainers.md 中记录的两类维护者专有恢复流程:一类是「formula 提交已生成、但 bottle 发布失败」时的止血与重放方案;另一类是面对"陌生构建失败"时应遵循的排查纪律。这两类流程不属于普通用户的日常排障范畴,通常需要操作规范化的 tap 仓库规范检出(canonical tap checkout)并拥有 GitHub 写权限。读完本文,你将能:在 brew pr-pull / brew pr-upload 发布链路半途失败时判断正确动作、在不破坏未发布提交的前提下安全回退并重放、以及在构建异常出现时避免"以一次错误字符串为据打补丁"的误诊。

该文档定位为"维护者专用",因此文中的命令默认会在 homebrew/core 的规范检出上执行 git 历史改写(reset --hard、创建/删除备份分支等)。请先在临时 clone 或 Git worktree 中演练,任何既有本地成果务必先落盘或备份。

前置背景:从 PR 到 bottle 发布的完整链路

在深入恢复流程前,先理解这条链路为什么失败现场会以"部分提交 + 未发布 bottle"的形态出现。

homebrew/core 而言,一条 formula 改动 PR 的正常合入流程是:CI(tests.yml)在各支持平台构建成功后,产出以 bottles{,_*} 为默认模式的构建产物(artifact);随后由维护者(或 BrewTestBot)执行 brew pr-pull 把 PR 提交落到规范检出中,并为合并后的 formula 写回更新后的 bottle do 块,最后把构建出的二进制包发布到 GitHub Packages(或 GitHub Releases)。之后才会用 GitHub 的标准 Merge/Squash/Rebase 合入,具体协作约定见 docs/Homebrew-homebrew-core-Maintainer-Guide.md

两个命令是整个链条的执行器:

  • brew pr-pull PULL_REQUEST:从 PR 抓取并 cherry-pick 提交、下载 workflow 产出的 bottle 产物、执行版本一致性检查,最后委托 brew pr-upload 完成"生成 commit + 上传"。它默认以 canonical tap checkout(即 $(brew --repository homebrew/core),默认 homebrew/core)为操作对象,无论你当前在哪个目录执行。源码见 dev-cmd/pr-pull.rb:默认 tap 取 CoreTap.instance.name第 84–87 行),下载完产物后拼接 upload_args = ["pr-upload"] 并同步透传 --no-commit--dry-run--keep-old--warn-on-upload-failure 等开关(第 176–187 行)。
  • brew pr-upload:面向当前工作目录内的 *.bottle.json 运行。读取全部 JSON、按需合并生成/重写 formula 的 bottle DSL、执行 brew audit --skip-style 自检,最后把 bottle 推送到 GitHub Releases 或 GitHub Packages。

换言之:失败点可能发生在 pr-pull(下载/提交阶段)也可能发生在 pr-upload(上传阶段)。下述恢复流程正是在"上传失败、而 formula 提交本身已正确"这一精确场景下设计的。

场景一:提交已生成、仅 bottle 发布失败

如果失败发生时,formula 的提交内容是正确的(bottle do 块、sha256 摘要、版本号等都没问题),只是上传动作失败了,那么恢复流程要尽量克制,不要重做整个 pr-pull

恢复四步曲

Common-Issues-for-Maintainers.md 的指引,正确动作如下:

  1. 从失败 workflow 中下载并解压 bottle 产物(artifact)。产物就在失败的那次 CI 运行中,无需重新构建。

  2. 进入解压后的产物目录。这一点关键:brew pr-upload 的行为严重依赖当前工作目录。

  3. 带上合适的上传参数执行

    brew pr-upload --no-commit
    
  4. 发布成功后收尾(回退规范检出、删除备份分支等见下文)。

为什么是 --no-commit

阅读 pr-upload 的源码可解释每个开关的取舍。入口 dev-cmd/pr-upload.rbrun 首先扫描当前目录:

json_files = Dir["*.bottle.json"]
odie "No bottle JSON files found in the current working directory" if json_files.blank?

dev-cmd/pr-upload.rb

随后,除非传入了 --upload-only,否则它会先执行 brew bottle --merge --write第 62、93 行)——也就是说默认情况下,它会重新把 JSON 合并进 formula 并生成一个新的 commit。这正好是本次场景想避免的:既然失败提交的内容已正确,再生成/改写一次提交是多余的,甚至会污染现场。

--no-commit 开关在参数表中被定义为 "Do not generate a new commit before uploading"(第 23–24 行),它让 pr-upload 只做"上传/补传"这件事。注意它和 --upload-only 互斥(conflicts "--upload-only", "--no-commit"第 39 行),二者语义一个跳过合并、一个跳过提交,不可同用。

合并之后,只要不是 --no-commit,还会对本次涉及的 formula 执行 brew audit --skip-style第 105–111 行),确保刚写回的 bottle commit 没有破坏 lint/audit,这也是判断"提交是否正确"的一道自动化闸门。

上传目标由 JSON 中 bottle.root_url 决定:匹配 GitHub Releases 的 URL 规则则走 GitHubReleases.upload_bottles第 115–117 行),匹配 GitHub Packages 的 URL 规则则走 GitHubPackages.upload_bottles第 118–123 行),后者会把 --warn-on-upload-failure 映射为 warn_on_error 参数传入。

关键前提:pr-pull 只认规范检出

文档特别强调:

brew pr-pull always operates on the canonical tap checkout, regardless of the current directory.

这决定了后续所有 git 回退动作都必须在规范检出上进行——这也是下文所有命令都带 -C "$(brew --repository homebrew/core)" 的原因。如果你需要让 git 操作指向其他 tap,可参照 docs/How-to-Create-and-Maintain-a-Tap.mdbrew pr-pull --tap=$YOUR_GITHUB_USERNAME/tap 的用法为命令指定 --tap

场景一进阶:从更早的提交"完整重放" pr-pull

如果上传失败比"补传"更严重——例如需要从失败 pr-pull 创建提交之前的某个提交开始整体重做——那么仅靠 --no-commit 补传就不够了。此时要把规范检出重置到那个历史位置,重新跑一次 pr-pull。这属于对共享检出执行破坏性操作,因此文档给出了严格的"先备份、后回退、终恢复"流程。

第 0 步:确认无未提交工作 + 建立备份分支

在任何 reset --hard 之前,必须满足两个前提:规范检出中没有任何未提交的工作;并且远端已经被 fetch,以便后续可以回到 origin/HEAD

git -C "$(brew --repository homebrew/core)" status --short
git -C "$(brew --repository homebrew/core)" fetch origin
git -C "$(brew --repository homebrew/core)" branch pr-pull-recovery-backup

三个命令依次完成:确认工作区干净、拉取远端引用、在当前提交处创建一个备份分支 pr-pull-recovery-backup。备份分支是这条恢复路径的安全网:即使下面的 reset --hard 出现偏差,你依然可以从该分支找回被丢弃的提交。

第 1 步:把规范检出重置到失败前的提交

COMMIT_SHA失败的那次 brew pr-pull 创建提交之前的那个提交(例如用 git log 找到失败前最后一次干净的远端提交):

git -C "$(brew --repository homebrew/core)" reset --hard COMMIT_SHA

第 2 步:以原始参数重放 pr-pull

brew pr-pull PULL_REQUEST

务必"with the original options"——即把初次执行时使用的所有选项原样带上(如 --keep-old--no-upload--autosquash--head-sha 等)。如果只是想要"下载但不发布",可加 --no-upload 仅验证下载与提交阶段(见 dev-cmd/pr-pull.rb)。

为什么必须从"失败前的提交"重放而不是直接在失败现场补跑?从源码看,pr-pull 会先基于 merge-base origin/HEAD current_branch_head 计算基准(dev-cmd/pr-pull.rb),再 cherry-pick PR 提交(第 140–150 行)。如果现场已被部分提交污染,重新计算出的 merge-base 与期望不符,可能把 PR 提交重复应用或漏掉。从干净的基准状态重放,才能保证提交集合与首次执行一致。

第 3 步(按需):部分上传成功时使用 --warn-on-upload-failure

文档在此处给出了一条精确的边界:

Add --warn-on-upload-failure only when bottles were partially uploaded and their checksums are known to match the existing bottle do block.

也就是说,只有当你确认瓶子的上传是"部分成功"(一部分已传上去、一部分失败)这些产物当前的 checksum 与 formula 中既有的 bottle do完全一致时,才把该开关追加进重放命令:

brew pr-pull --warn-on-upload-failure PULL_REQUEST

该开关的语义在参数表中写明是 "Warn instead of raising an error if the bottle upload fails. Useful for repairing bottle uploads that previously failed."(dev-cmd/pr-pull.rb)。在实现上它被透传给 pr-uploaddev-cmd/pr-pull.rb),再作为 warn_on_error: 传入 github_packages.rbupload_bottles/upload_bottle/preupload_check 系列方法——上传冲突/失败时把"直接 raise 中断"降级为告警继续。它天然是面向"半程补传"设计的,不是给全量失败兜底用的:如果 checksum 对不上(说明此前上传的二进制与当前 formula 不一致),仍应报错而不是被静默放行。

第 4 步:发布成功后回到默认分支并清理备份

发布成功后,把规范检出交还给默认远端分支(origin/HEAD 在步骤 0 已 fetch):

git -C "$(brew --repository homebrew/core)" reset --hard origin/HEAD

随后确认备份分支不再需要之后再删除它。通常的判据是:重放后的 origin/HEAD 已包含所有预期提交、远端也完成推送。清理命令(由维护者本人依据实际情况执行):

git -C "$(brew --repository homebrew/core)" branch -D pr-pull-recovery-backup

此流程的禁用条件

文档给出了一条硬性红线:

Do not use this recovery procedure when the canonical checkout contains work that has not been committed or preserved elsewhere.

只要规范检出中还有未被提交、或未在其他地方保留的工作,就绝不能走 reset --hard 这套恢复流程。宁可先在临时 clone 或 Git worktree 中复现旧状态、把现场工作另行保存,也不要让共享检出的未提交成果成为 reset 的牺牲品。

场景二:面对陌生构建失败,先定位再动手

第二类"常见问题"不是操作恢复,而是一条纪律性指引,针对的是维护者在 CI 或本地构建中偶遇的、先前未见过(unfamiliar)的构建失败。

不要用"一句旧错 + 一条旧 issue"打永久补丁

文档的忠告直白且具有普遍性:

Do not add a permanent workaround based only on an old issue or an exact linker error string.

原因在于:链接错误字符串(如某符号未定义、某库缺失)在不同 formula、不同系统版本上的表象可能完全相同,但根因却分属三类不同主体(见下)。把一个基于"看起来像同一错误"的临时 workaround 固化进 formula,等于把一个未经验证的假设写进了长期维护路径——今后每次构建都可能背着这个错误补丁。

可复现的定位流程

正确路径是先复现、再归因、后决策

  1. 在受支持的 runner 上复现失败(如 GitHub Actions 上的标准镜像,或与 CI 环境一致的本地环境),确保失败可稳定重现而不是一次 flake。
  2. 检查编译器与链接器的实际调用(invocation):查看完整的编译/链接命令行、flag、环境变量、搜索路径,而不是只看报错的最后几行。Homebrew 会在构建日志中打印完整的 compiler/linker invocation,源码层面对构建环境的接管逻辑可参考 compilers.rbcompiler_selector.rb
  3. 判定问题来源,三类主体三选一:
    • formula 自身:版本更新带来的新构建要求、依赖声明缺失、patch 失效等;
    • 上游(upstream):源码在特定平台/编译器的真实缺陷或行为变更;
    • runner 镜像:构建环境本身的问题(工具链更新、镜像组件漂移),与 formula 无关。

归因直接决定了下一步动作:若是 upstream 或 runner 的问题,不应把 workaround 写进 formula;若是 formula 问题,才进入正常的修复与评审流程。

有价值的结论应沉淀到指南而不是错误清单

Document reusable findings in the relevant maintainer or formula-author guide instead of adding one-off historical errors to this page.

这条规则保护了 Common-Issues-for-Maintainers.md 的定位:它只承载可复用的流程与机制,不承担"历史错误堆砌"的功能。一次性的、仅对某个历史版本成立的错误详情,应当沉淀到 docs/Homebrew-homebrew-core-Maintainer-Guide.mddocs/BrewTestBot-For-Maintainers.md 或面向 formula 作者的 docs/Formula-Cookbook.md 等主题指南中;而本文件只收录"下次还会遇到、且动作可以标准化"的流程。

与相邻文档的分工

要正确使用本文的恢复流程,建议先明确以下文档边界:

小结:一套可执行的决策树

把上述内容压缩为维护者现场可用的决策路径:

  1. 失败发生在哪一段? 若 formula 提交已正确、仅是上传失败 → 从失败 workflow 下载 artifact、进入其目录、执行 brew pr-upload --no-commit 补传(场景一基础流程)。
  2. 需要从更早状态整体重放? → 先在规范检出上确认 git status --short 干净并 git fetch origin,创建备份分支 pr-pull-recovery-backupreset --hard 到失败 pr-pull 之前的 COMMIT_SHA,以原始选项重跑 brew pr-pull
  3. 只是部分上传成功? → 仅在 checksum 与既有 bottle do 块可确认一致的前提下加 --warn-on-upload-failure
  4. 收尾 → 成功后 reset --hard origin/HEAD;确认无用时再删备份分支。检出中任何未提交成果未保全时,一律拒绝该流程。
  5. 遇到陌生构建失败 → 先在受支持 runner 上复现、检查 compiler/linker 调用,判定根因归属 formula/upstream/runner 三者之一,再把可复用的结论写进对应指南,而不是给本页堆积一次性错误案例。

这套流程的本质,是围绕 brew pr-pullbrew pr-upload 对"规范检出"和"当前目录"的不同依赖关系(分别见 dev-cmd/pr-pull.rbdev-cmd/pr-upload.rb)建立的可逆操作纪律:任何破坏性 git 操作之前先留备份,任何恢复操作都以最小的动作集完成目标,任何结论都要能被复现所支撑。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388