首页
/ Homebrew 公式多版本管理(Formulae Versions)实战指南:版本化公式规范与锁定安装工具解析

Homebrew 公式多版本管理(Formulae Versions)实战指南:版本化公式规范与锁定安装工具解析

2026-09-08 14:23:28作者:齐冠琰

本文基于 Homebrew/brew 仓库中的 docs/Versions.md,系统讲解 Homebrew 多版本公式(versioned formulae)的命名约定、纳入 homebrew/core 的标准门槛,以及日常开发中把本地包"锁定/冻结"在特定版本的七种官方工具与配套环境变量。读完你可以正确区分 foo@1.2foo-full 的本质差异,理解 keg_only :versioned_formula 的底层机制,并在 brew pin$HOMEBREW_NO_AUTO_UPDATEbrew version-installbrew extract 等方案中做出合理取舍。

一、什么是版本化公式(Versioned Formulae)

1.1 命名与类名的映射关系

Homebrew 通过一种特殊命名格式在 homebrew/core 中同时支持同一个公式的多个版本:文件名形如 foo@1.2.rb,对应的 Ruby 类名则写成 FooAT12。这里的转换规则是:

  • 公式名中的 @ 被去掉;
  • 版本号中的 .(小数点)也被去掉;
  • 类名整体保持驼峰(CamelCase)风格。

例如 node@18.rb 对应类 NodeAT18。这一映射关系在仓库的代码中得到了印证,例如 dev-cmd/extract.rb 提取历史版本时会执行:

# 版本号只能包含数字并以小数点分隔
version_string = version.to_s
                    .sub(/\D*(.+?)\D*$/, "\\1")
                    .gsub(/\D+/, ".")
versioned_name = Formulary.class_s("#{name}@#{version_string}")
result.sub!("class #{class_name} < Formula", "class #{versioned_name} < Formula")

也就是说,从 Git 历史中取出旧版公式后,公式文件会被写入目标 tap 的 Formula/<name>@<version_string>.rb,类名被同步改写为 <Name>AT<digits>,从而匹配文件名。

1.2 版本化公式 ≠ 变体公式

本文讨论的对象是 foo@1.2 这类"版本化公式"。而 foo-full 这类名称并不属于版本化公式——它们是独立的变体公式,用于提供不同的依赖选择或功能取舍,遵循的是 homebrew/core 依赖与变体策略,而不是本文所述的多版本机制。区分这两类概念,是阅读后续标准与工具的前提。

二、可以纳入 homebrew/core 的版本化公式标准

文档明确列出了 homebrew/core 收录版本化公式必须满足的一系列标准。这是向 Homebrew 上游提交版本化公式(或理解为什么你的 PR 被要求重写为变体公式)的核心门槛:

  1. 构建兼容性:版本化软件应在 Homebrew 支持的所有 macOS 版本上都能构建。
  2. 版本差异级别:版本化公式应与当前稳定版在 major/minor(主/次)版本上不同,而不是仅 patch(补丁)版本不同——patch 版本通常代表 bug 或安全修复,Homebrew 希望确保用户应用这些安全更新。
  3. 不接受不稳定版本:alpha、beta、开发版等不稳定版本,对版本化公式(以及非版本化公式)一律不可接受。
  4. 上游必须有发布分支与安全更新策略:上游应为每个公式版本维护 release branch,并明确承诺在必要时为每个版本发布安全更新(文档以 2020 年 1 月的 PHP 为例:PHP 7.0 不在支持列表,而 PHP 7.2 在)。与之相对,大多数软件项目只对最新版本发布安全更新,因此它们的旧版本不具备被版本化收录的资格。
  5. 共享代码库:版本化公式应与主公式共享同一代码库。如果项目已拆分为不同仓库,推荐新建一个独立公式(formula2 而非 formula@2formula@1)。
  6. 递归依赖不得重复:依赖版本化公式的公式,其递归依赖树中不得同时出现同一个公式的两个不同版本。例如,若你依赖 openssl@1.0foo,而 foo 又依赖 openssl,那么你应该改用 openssl 而不是 openssl@1.0
  7. 可链接性约束:仅当上游通过"带后缀的二进制名"等机制明确支持时,版本化公式才允许与其非版本化版本同时被 link。否则必须使用 keg_only :versioned_formula,让用户得以同时安装多个版本。
  8. 不得污染 HOMEBREW_PREFIXkeg_only :versioned_formula 不应在 HOMEBREW_PREFIXpost_install 任何与主公式(或其他版本化公式)冲突或重复的内容。例如 node@6 不应像 node 公式那样把自己的 npm 装进 HOMEBREW_PREFIX
  9. 有真实用户需求:被提交的版本化公式应有大量用户预期使用;一旦不再满足,将被移除。Homebrew 尽量不移除位于安装请求分析(install-on-request)前 3,000 名的公式。
  10. 不携带需要安全更新的 resource:版本化公式不应携带需要安全更新的 resource。例如 node@6 不应内带 npm resource,而应依赖上游 tarball 自带的那份 npm
  11. 与主公式尽可能相似:新建或更新版本化公式应是审视主公式的机会(例如:某些无用选项能否移除或改为默认值)。版本化公式与主公式应尽量合理一致。
  12. 数量上限:任何时刻一个公式(含主版本)最多支持 5 个版本,除非它们足够热门(如 90 天分析安装量超过 1000)。移除超额版本时按使用量与支持状态优先,而非按"年龄"。
  13. ABI 稳定性:版本化公式必须在其版本分支存续期内保持 ABI 稳定。对其的更新不得引入 ABI 不兼容、不得导致依赖者需要 revision 重建。Homebrew 会拒绝违反此要求的版本升级,并从该点起弃用该公式。

2.1 keg_only :versioned_formula 的源码含义

"只能同时安装多个版本、但不同时 link"这一要求,在仓库里有直接对应的实现。查看 keg_only_reason.rb,可以看到 :versioned_formula 被建模为一种专门的 keg_only 原因:

def versioned_formula?
  @reason == :versioned_formula
end

当公式只写 keg_only :versioned_formula、没有额外说明文字时,默认解释文案就是:

this is an alternate version of another formula

也就是说,声明 keg_only :versioned_formula 后,该公式的 keg 只进入 Cellar 而不链接到 HOMEBREW_PREFIX,从机制上保证了主公式与旧版本可以共存的场景下互不抢占二进制路径。若上游确实用后缀二进制(如 python3.7)提供了并行支持,才可考虑放开 link 限制。

三、将已安装公式锁定在特定版本:先问自己是否真的需要

文档给出了一条明确的指引:Homebrew 的"多版本"机制不应被用来把公式强行"钉"在你的个人需求上。如果 homebrew/core 中已存在对应的版本化公式,应优先直接使用它——因为它仍由 Homebrew 维护与更新。

若你想要的是别的方案,请遵循"选择能满足需求的最小工具"原则。以下是文档列出的全部官方选项,按自动化程度从高到低排列。

3.1 brew pin:阻止已安装包被升级

当你希望 brew upgrade 不再升级已经安装的包时,使用 brew pin <formula_or_cask>

优点

  • 是最简单的内置选项。

缺点

  • 被 pin 期间该包将收不到任何更新,包括安全更新;
  • 当其他公式要求更新版本时,被 pin 的公式可能阻塞其安装或升级;
  • 被 pin 的 cask 若开启了自动更新(auto_updates),仍可能在 Homebrew 之外自行更新。

cmd/pin.rb 的实现可以看到命令的完整语义:

  • 通过 --formula / --cask 可强制把命名参数解释为某一类包;
  • 对每个包:已 pin 则提示 already pinned;未安装则报 not installed;否则调用 package.pin
  • 针对 cask 还有专门检查——见 cmd/pin.rb:若被 pin 的 cask 声明了 auto_updates true,命令会明确提示"尽管被 pin,它仍可能在 Homebrew 之外自行更新"。

pin 的物理实现位于 formula_pin.rb:它会在 HOMEBREW_PINNED_KEGS 目录下为公式名创建指向当前安装 keg 版本目录的符号链接:

def path
  HOMEBREW_PINNED_KEGS/@formula.name
end

def pin_at(version)
  HOMEBREW_PINNED_KEGS.mkpath
  version_path = @formula.rack/version.to_s
  path.make_relative_symlink(version_path) if !pinned? && version_path.exist?
end

即:pinned? 的本质就是判断该符号链接是否存在(path.symlink?),pinnable? 则要求公式至少已安装过一个版本。解除用 brew unpin <formula_or_cask> 即可。

3.2 $HOMEBREW_NO_AUTO_UPDATE:暂停自动刷新元数据

export HOMEBREW_NO_AUTO_UPDATE=1

当你希望 Homebrew 在你显式运行 brew update 之前不再自动刷新 formula 与 cask 元数据时使用它。该环境变量在 env_config.rb 中注册,并在 env_config.rb 等处作为"彻底关闭自动更新"的选项被引用。

优点

  • 只有当你显式执行 brew update 时,Homebrew 才会获知新版本信息。

缺点

  • 它本身不会阻止 brew upgrade 改变已安装公式;
  • 在运行 brew update 之前,你可能错过 bug 修复与安全更新。

如果你同时使用 brew bundle,可再叠加 brew bundle --no-upgradeexport HOMEBREW_BUNDLE_NO_UPGRADE=1,让 brew bundle 也停止升级已安装依赖。

3.3 brew bundle --no-upgrade$HOMEBREW_BUNDLE_NO_UPGRADE

当你希望 brew bundle 不再对过时的依赖执行 brew upgrade 时:

brew bundle --no-upgrade
# 或
export HOMEBREW_BUNDLE_NO_UPGRADE=1

优点

  • 是降低 brew bundle 版本"颠簸(churn)"的最简单方式。

缺点

  • 不提供 pin 版本或锁文件支持
  • brew install 在必要时仍可能升级某个依赖;
  • 使用期间可能错过修复与安全更新。

3.4 $HOMEBREW_NO_INSTALL_UPGRADE:阻止 brew install 的"意外升级"

export HOMEBREW_NO_INSTALL_UPGRADE=1

当你希望 brew install <formula> 不再把已安装但已过时的公式顺手升级到新版时使用它。该变量同样在 env_config.rb 中定义。

优点

  • 避免 brew install 带来令人意外的版本跳变。

缺点

  • 它不 pin 版本;
  • 它不能阻止 brew upgrade
  • 使用期间可能错过修复与安全更新。

3.5 $HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK:跳过已安装依赖者检查

export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1

当你希望 Homebrew 在 install / upgrade / reinstall 之后跳过对已安装依赖者是否过时或损坏的检查时使用它(定义见 env_config.rb)。

优点

  • 可以减少连锁升级与连锁重装。

缺点

  • 它不是版本冻结工具;
  • 可能把损坏的链接或过时的依赖者留在原地;
  • 可能加大后续 brew install / brew upgrade 出问题的概率。

3.6 brew version-install:把旧版本提取进自己的 tap 并安装

brew version-install automake@1.12

当你想要一个"更简单的流程来提取某个旧版本到自己 tap 并安装"时,使用 brew version-install。该命令是 version-install.rbHomebrew::Cmd::VersionInstall 的实现,其核心执行流程非常清晰:

  1. 解析输入:既支持 <formula>@<version>(如 automake@1.12),也支持 brew version-install <formula> <version> 两个位置参数的形式;若两种写法同时给出但版本不一致会直接报错(见 version-install.rb)。
  2. 构造版本化名:公式基名去除 @... 后缀并小写,版本号做归一化(去除首尾非数字、把非数字替换为 .),最终形成 <base>@<normalized_version>,例如 automake@1.12(见 version-install.rb)。
  3. 已安装即退出:若该版本化名称或当前主公式的匹配版本已安装,直接提示后返回。
  4. 找不到现成版本化公式时提取:默认 tap 名为 <user>/homebrew-versionsDEFAULT_TAP_REPOSITORY = "versions"),<user> 优先取 GitHub 用户名(在启用 GitHub API 的前提下通过 cmd/version-install.rb 查询),否则取本地用户名。若 tap 不存在会用 brew tap-new --no-git 创建,然后调用 brew extract <formula> <tap> --version=<version> 完成历史提取。
  5. 给出维护警示:提取完成后会打印明确警告——You are responsible for maintaining this <tap>/<formula>@<version>!,它不会收到任何 bugfix/安全更新(见 version-install.rb)。
  6. 安装:最终执行 brew install <install_target>

优点

  • 是从你自己的 tap 使用旧公式版本的最简单入口。

缺点

  • 从那一刻起,更新、维护、修复弃用与安全更新全部由你负责

3.7 brew extract:手工管理历史公式的低层工作流

当你想以更低层的方式操作、并完全自行管理提取出的公式文件时,使用 brew extract。语法为(见 dev-cmd/extract.rb):

brew extract [--version=<version>] [--git-revision=<revision>] [--force] <formula> <tap>

关键行为,均可从 dev-cmd/extract.rb 的源码得到印证:

  • 不传 --version= 时,沿仓库 Git 历史从 HEAD 向前搜索该公式最近一次出现的版本;传了 --version= 则逐 revision 回退比对,直到找到版本匹配(还支持按语义化版本段做前缀匹配),见 dev-cmd/extract.rb
  • 目标 tap 不允许是 homebrew/corehomebrew/cask 或与源 tap 相同(除非以 HOMEBREW_DEVELOPER=1 开发者模式运行),见 dev-cmd/extract.rb
  • 提取时会自动改写类名FooFooAT123)以匹配新文件名,并删除 bottle block(历史 bottle 不再可用),见 dev-cmd/extract.rb
  • 若目标公式文件或补丁文件已存在,需追加 --force 才会覆盖;补丁文件也会一并从历史 revision 提取到 tap 中(见 dev-cmd/extract.rb)。
  • 写出的文件位于 <tap>/Formula/<name>@<version_string>.rb

优点

  • 对自家 tap 中的公式文件拥有最大掌控力。

缺点

  • 这是最手动的方案;
  • 从那一刻起,更新、维护、修复弃用与安全更新同样由你负责。

3.8 完整的 brew version-install 示例

若你的项目依赖 automake 1.12(而不是最新版),可以这样拿到版本为 1.12 的 automake 公式并安装:

brew version-install automake@1.12

需要提醒的是:以此方式取得的公式可能包含已弃用、已禁用甚至已移除的旧 Homebrew 语法(例如校验和可能是 sha1 而不是 sha256)。brew version-install 不会去修改或升级公式以符合当前的规范与风格要求——这正是前面列出其缺点项的原因。

四、维护边界与责任划分

文档在结尾部分划清了 Homebrew 的维护边界,本节逐条转述如下:

  • Homebrew 支持上述命令与本地工作流,但不承诺为每一个被冻结或提取的公式版本长期维护。
  • 在提交 Homebrew issue 之前,请先运行 brew update,并确保问题在当前元数据下可复现。若你使用了 brew pin$HOMEBREW_NO_AUTO_UPDATE$HOMEBREW_BUNDLE_NO_UPGRADE$HOMEBREW_NO_INSTALL_UPGRADE$HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECKbrew version-installbrew extract,则只有当问题能用 core 公式复现时才允许提交 issue
  • 如果你在自己的 tap 中维护公式,那么这些公式本身及其弃用处理、安全更新都是你的责任。若你或你的组织需要长期掌控公式版本,请参考 How to Create and Maintain a Tap(如何创建与维护 tap)
  • "一次 brew upgrade 破坏了你本地冻结或提取的公式",并不构成让 Homebrew 在 homebrew/core 中新增并维护另一个历史版本的理由。
  • 另外说明:Homebrew 可能出于自身需求,在 homebrew/core临时加入一些不完全符合上述标准的版本化公式。某个版本化公式存在于 homebrew/core,并不代表它会无限期维护,也不代表 Homebrew 愿意接受任何更多不满足上述要求的新版本。

五、方案选型速查

你的诉求 推荐方案 是否冻结版本 是否继续收到更新
阻止 brew upgrade 升级已装包 brew pin <formula_or_cask> 是(仅该包) 否(pin 期间)
暂停元数据自动刷新 export HOMEBREW_NO_AUTO_UPDATE=1 否(仅延迟获知新版本) 运行 brew update 前否
brew bundle 不再升级依赖 brew bundle --no-upgradeexport HOMEBREW_BUNDLE_NO_UPGRADE=1 视情况
阻止 brew install 意外升级已装包 export HOMEBREW_NO_INSTALL_UPGRADE=1 视情况
跳过安装/升级后的依赖者检查 export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 视情况
提取旧版本到自己 tap 并安装 brew version-install <formula>@<version> 可复用历史版本 否(自行维护)
手工从 Git 历史提取公式文件 brew extract [--version=…] <formula> <tap> 可复用历史版本 否(自行维护)

选型时的通用建议是:先查 homebrew/core 中是否已有对应的官方版本化公式(受支持且持续更新);只有当找不到现成方案时,才逐级下探到上述"冻结"或"提取"手段,并清醒地接受随之而来的维护责任。

热门项目推荐
相关项目推荐

项目优选

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