Homebrew 公式多版本管理(Formulae Versions)实战指南:版本化公式规范与锁定安装工具解析
本文基于 Homebrew/brew 仓库中的 docs/Versions.md,系统讲解 Homebrew 多版本公式(versioned formulae)的命名约定、纳入
homebrew/core的标准门槛,以及日常开发中把本地包"锁定/冻结"在特定版本的七种官方工具与配套环境变量。读完你可以正确区分foo@1.2与foo-full的本质差异,理解keg_only :versioned_formula的底层机制,并在brew pin、$HOMEBREW_NO_AUTO_UPDATE、brew version-install、brew 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 被要求重写为变体公式)的核心门槛:
- 构建兼容性:版本化软件应在 Homebrew 支持的所有 macOS 版本上都能构建。
- 版本差异级别:版本化公式应与当前稳定版在 major/minor(主/次)版本上不同,而不是仅 patch(补丁)版本不同——patch 版本通常代表 bug 或安全修复,Homebrew 希望确保用户应用这些安全更新。
- 不接受不稳定版本:alpha、beta、开发版等不稳定版本,对版本化公式(以及非版本化公式)一律不可接受。
- 上游必须有发布分支与安全更新策略:上游应为每个公式版本维护 release branch,并明确承诺在必要时为每个版本发布安全更新(文档以 2020 年 1 月的 PHP 为例:PHP 7.0 不在支持列表,而 PHP 7.2 在)。与之相对,大多数软件项目只对最新版本发布安全更新,因此它们的旧版本不具备被版本化收录的资格。
- 共享代码库:版本化公式应与主公式共享同一代码库。如果项目已拆分为不同仓库,推荐新建一个独立公式(
formula2而非formula@2或formula@1)。 - 递归依赖不得重复:依赖版本化公式的公式,其递归依赖树中不得同时出现同一个公式的两个不同版本。例如,若你依赖
openssl@1.0和foo,而foo又依赖openssl,那么你应该改用openssl而不是openssl@1.0。 - 可链接性约束:仅当上游通过"带后缀的二进制名"等机制明确支持时,版本化公式才允许与其非版本化版本同时被 link。否则必须使用
keg_only :versioned_formula,让用户得以同时安装多个版本。 - 不得污染 HOMEBREW_PREFIX:
keg_only :versioned_formula不应在HOMEBREW_PREFIX中post_install任何与主公式(或其他版本化公式)冲突或重复的内容。例如node@6不应像node公式那样把自己的npm装进HOMEBREW_PREFIX。 - 有真实用户需求:被提交的版本化公式应有大量用户预期使用;一旦不再满足,将被移除。Homebrew 尽量不移除位于安装请求分析(install-on-request)前 3,000 名的公式。
- 不携带需要安全更新的 resource:版本化公式不应携带需要安全更新的
resource。例如node@6不应内带npmresource,而应依赖上游 tarball 自带的那份npm。 - 与主公式尽可能相似:新建或更新版本化公式应是审视主公式的机会(例如:某些无用选项能否移除或改为默认值)。版本化公式与主公式应尽量合理一致。
- 数量上限:任何时刻一个公式(含主版本)最多支持 5 个版本,除非它们足够热门(如 90 天分析安装量超过 1000)。移除超额版本时按使用量与支持状态优先,而非按"年龄"。
- 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-upgrade 或 export 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.rb 中 Homebrew::Cmd::VersionInstall 的实现,其核心执行流程非常清晰:
- 解析输入:既支持
<formula>@<version>(如automake@1.12),也支持brew version-install <formula> <version>两个位置参数的形式;若两种写法同时给出但版本不一致会直接报错(见 version-install.rb)。 - 构造版本化名:公式基名去除
@...后缀并小写,版本号做归一化(去除首尾非数字、把非数字替换为.),最终形成<base>@<normalized_version>,例如automake@1.12(见 version-install.rb)。 - 已安装即退出:若该版本化名称或当前主公式的匹配版本已安装,直接提示后返回。
- 找不到现成版本化公式时提取:默认 tap 名为
<user>/homebrew-versions(DEFAULT_TAP_REPOSITORY = "versions"),<user>优先取 GitHub 用户名(在启用 GitHub API 的前提下通过 cmd/version-install.rb 查询),否则取本地用户名。若 tap 不存在会用brew tap-new --no-git创建,然后调用brew extract <formula> <tap> --version=<version>完成历史提取。 - 给出维护警示:提取完成后会打印明确警告——
You are responsible for maintaining this <tap>/<formula>@<version>!,它不会收到任何 bugfix/安全更新(见 version-install.rb)。 - 安装:最终执行
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/core、homebrew/cask或与源 tap 相同(除非以HOMEBREW_DEVELOPER=1开发者模式运行),见 dev-cmd/extract.rb。 - 提取时会自动改写类名(
Foo→FooAT123)以匹配新文件名,并删除 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_CHECK、brew version-install或brew 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-upgrade 或 export 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 中是否已有对应的官方版本化公式(受支持且持续更新);只有当找不到现成方案时,才逐级下探到上述"冻结"或"提取"手段,并清醒地接受随之而来的维护责任。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java60
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280