Homebrew 这东西,用得好是神器,用不好是真折磨。Mac 上装软件,离不开它;但每次 brew update 卡在 GitHub 上,进度条半天不动,最后等来一堆报错,也是不少人的日常。之前我也被折腾过几轮,后来索性写了一个「Mac OS 更新 Homebrew 镜像源脚本」,一键切换国内镜像源,把 brew update 和安装瓶装包的耗时从十几分钟压到了几十秒。这篇文章就把脚本的思路、完整实现、踩坑点都摊开来讲,适合被 Homebrew 下载速度折磨过、又不想每次手动 export 环境变量的人参考。
1. 为什么需要这样一个镜像源脚本
1.1 Homebrew 慢在哪
很多新手第一次用 Homebrew,第一感受就是“装个软件怎么这么慢”。其实慢的原因并不复杂:Homebrew 的默认数据源和安装包下载地址都指向官方服务器,官方地址在国外,国内直连的时候延迟高、丢包率高,偶尔还会直接超时。你执行 brew install wget 的时候,Homebrew 会先去拉取 formula 元数据,然后再去下载对应的 bottle 二进制包,这两个环节都有可能在国外地址上卡住。
brew update 的过程更明显。它会尝试更新 Homebrew 自己的仓库、homebrew-core 仓库,还要从 formulae.brew.sh 拉取最新的 API 数据。这些请求一旦碰到网络波动,整个命令就会长时间停在“Updating Homebrew...”那一行,看起来像死机了一样。有一次我在公司网络下跑 brew update,等了差不多二十分钟还没结束,最后 Ctrl+C 放弃,去查日志才发现是卡在了一个国外 CDN 的回源请求上。
所以问题的核心不是 Homebrew 本身不好用,而是默认下载链路在国内不够友好。要解决它,最直接的办法就是切换到国内镜像源。镜像源做的事情很简单:把 Homebrew 的元数据、仓库、二进制包都同步到国内服务器上,让你从国内地址下载,速度自然就上来了。
1.2 镜像源方案对比
目前国内用得比较多的 Homebrew 镜像源有三个:清华 TUNA、中科大 USTC、阿里云。我三个都用过,简单说一下区别。
| 镜像源 | API 域名 | Bottle 域名 | 特点 |
|---|---|---|---|
| 清华 TUNA | mirrors.tuna.tsinghua.edu.cn | mirrors.tuna.tsinghua.edu.cn | 同步及时,文档全,高校网络下很快,是我最常用的 |
| 中科大 USTC | mirrors.ustc.edu.cn | mirrors.ustc.edu.cn | 速度稳定,更新也比较勤,适合教育网和联通线路 |
| 阿里云 | mirrors.aliyun.com | mirrors.aliyun.com | 商业带宽充足,某些地区运营商网络下延迟更低 |
三个镜像源都能覆盖 Homebrew 的 API 数据、仓库和 bottle 下载,日常使用哪个都不会差太多。唯一需要留意的是一段时间内的可用性和同步延迟,万一某个源出了故障,脚本能一键切换到另一个源就很关键。
手动切换镜像源其实不复杂,网上一搜教程一大堆。但问题是每次要么手动写 export,要么改 git remote,操作一多就容易漏。尤其新版 Homebrew 4.x 对 API 域名的依赖更强,很多人只改了 git remote,没改 HOMEBREW_API_DOMAIN,结果 brew install 依然慢。我写这个脚本,就是想把这一套流程封装成一条命令,顺带解决多个源之间来回切换、恢复官方源、清理重复配置这些问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 脚本设计思路与关键细节
2.1 设计目标
动手写脚本之前,我给自己定了几个目标,避免写着写着变成一次性工具。
第一条,必须支持多个镜像源。不能写死一个清华源,万一哪天清华源炸了,脚本就废了。所以参数要能接受 tuna、ustc、aliyun,并预留 official 用来恢复官方源。
第二条,必须能重复执行。有些脚本你跑第二遍,rc 文件里就多了一堆重复的 export,看起来非常乱。我要求脚本每次执行前先清理同名的环境变量配置,再追加新配置,保证幂等。
第三条,要同时处理环境变量和 git remote。只设置环境变量,brew update 时本地仓库的 remote 可能还指向官方地址,照样会去连官方服务器。所以脚本要自动识别 brew 安装路径,把本地 brew 仓库的 remote 也切到镜像地址。
第四条,要有容错能力。脚本执行过程中,如果 brew 命令不存在、brew 仓库路径不对,不能直接报错中断,至少得把环境变量配好,然后提示用户手动处理。
2.2 环境变量怎么配
Homebrew 的镜像切换主要靠四个环境变量:HOMEBREW_API_DOMAIN、HOMEBREW_BOTTLE_DOMAIN、HOMEBREW_BREW_GIT_REMOTE、HOMEBREW_CORE_GIT_REMOTE。
HOMEBREW_API_DOMAIN 是 Homebrew 4.x 之后非常重要的一个变量。它决定了 brew update 和 brew search 时从哪里拉取 formula 和 cask 的 API 数据。如果不设置,Homebrew 会访问默认的 formulae.brew.sh,国内访问速度不稳定,所以这是必须替换的第一个变量。
HOMEBREW_BOTTLE_DOMAIN 决定了预编译二进制包的下载地址。安装软件时大部分时间都花在下载 bottle 上,这个变量不替换,前面 API 再快也没意义。镜像源一般把 bottle 目录同步到 homebrew-bottles 路径下,所以这个变量也要第一时间设置。
HOMEBREW_BREW_GIT_REMOTE 是 Homebrew 主仓库的 git 地址。对于从源码更新 Homebrew 本体有影响。新版 Homebrew 对 git 仓库的依赖比旧版轻,但设置上没坏处。HOMEBREW_CORE_GIT_REMOTE 同理,对应 homebrew-core 仓库,不过 Homebrew 4.x 默认不单独 clone homebrew-core,更多是兼容旧版使用习惯。
四个变量中,前两个是现代 Homebrew 必须设置的,后两个属于“保险栓”。脚本里我选择全部写入,这样不管用户用的是 3.x 还是 4.x,都能覆盖。
2.3 写入 shell 配置的姿势
macOS 默认 shell 是 zsh,所以环境变量要写到 ~/.zshrc 里。如果你自己改过默认 shell,比如换成了 bash,那就要写到 ~/.bash_profile 或 ~/.bashrc。我在脚本里默认用 ~/.zshrc,但保留了变量名,方便你改。
写配置最忌讳的方式是直接 echo "export HOMEBREW_API_DOMAIN=..." >> ~/.zshrc。用了几次之后,文件里会出现大量重复行,而且如果你切换过不同镜像源,旧配置会和新配置混在一起,最终生效的是哪一行你根本看不出来。正确处理方式是先删除旧的同名配置,再追加新配置。
在 macOS 的 sed 和 Linux 的 sed 语法不太一样,macOS 的 sed 做原地修改要用 sed -i '',后面必须跟一个空字符串参数,否则会报错。这一点对只在 Mac 上跑的朋友问题不大,但如果脚本拿到 Linux 上跑,就会卡住。我在脚本里专门用了 macOS 兼容写法,因为标题就是 Mac OS 场景。
2.4 容错回滚设计
镜像源脚本最怕什么?最怕切到某个源之后,镜像站本身出问题,导致 Homebrew 彻底不能用。所以脚本里我加了一个 official 参数,一键恢复官方源。
恢复官方源的时候,不但要删除镜像源的环境变量,还要把 git remote 重新指回官方地址。如果你之前用脚本切到过清华源,再切回官方源,脚本会先清理所有 HOMEBREW_ 开头的配置行,然后重新写入官方默认值。这样即使你换了多个源,也能回到一个干净的初始状态。
另外,脚本里 set -uo pipefail 而不是 set -euo pipefail。因为 sed 删除不存在的行时会返回非零状态,如果开了 set -e,脚本会在清理环境变量的时候直接退出。新手写脚本喜欢背 set -e,但遇到这种幂等操作就会把自己坑了。所以容错设计不只是功能上的,也包括脚本自身的健壮性。
3. 完整脚本实现与使用说明
3.1 脚本全文
下面是我现在还在用的脚本,你保存为 update-homebrew-mirror.sh 就能用。
bash复制#!/bin/bash
# update-homebrew-mirror.sh
# 用法: ./update-homebrew-mirror.sh {tuna|ustc|aliyun|official}
set -uo pipefail
MIRROR="${1:-tuna}"
SHELL_RC="${HOME}/.zshrc"
TUNA_API="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api"
TUNA_BOTTLE="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles"
TUNA_BREW="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"
TUNA_CORE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git"
USTC_API="https://mirrors.ustc.edu.cn/homebrew-bottles/api"
USTC_BOTTLE="https://mirrors.ustc.edu.cn/homebrew-bottles"
USTC_BREW="https://mirrors.ustc.edu.cn/brew.git"
USTC_CORE="https://mirrors.ustc.edu.cn/homebrew-core.git"
ALI_API="https://mirrors.aliyun.com/homebrew-bottles/api"
ALI_BOTTLE="https://mirrors.aliyun.com/homebrew-bottles"
ALI_BREW="https://mirrors.aliyun.com/homebrew/brew.git"
ALI_CORE="https://mirrors.aliyun.com/homebrew/homebrew-core.git"
OFFICIAL_API="https://formulae.brew.sh/api"
OFFICIAL_BOTTLE="https://ghcr.io/v2/homebrew/core"
OFFICIAL_BREW="https://github.com/Homebrew/brew.git"
OFFICIAL_CORE="https://github.com/Homebrew/homebrew-core.git"
case "$MIRROR" in
tuna|tsinghua)
API="$TUNA_API"; BOTTLE="$TUNA_BOTTLE"; BREW="$TUNA_BREW"; CORE="$TUNA_CORE"
LABEL="清华"
;;
ustc)
API="$USTC_API"; BOTTLE="$USTC_BOTTLE"; BREW="$USTC_BREW"; CORE="$USTC_CORE"
LABEL="中科大"
;;
aliyun|ali)
API="$ALI_API"; BOTTLE="$ALI_BOTTLE"; BREW="$ALI_BREW"; CORE="$ALI_CORE"
LABEL="阿里云"
;;
official|restore)
API="$OFFICIAL_API"; BOTTLE="$OFFICIAL_BOTTLE"; BREW="$OFFICIAL_BREW"; CORE="$OFFICIAL_CORE"
LABEL="官方"
;;
*)
echo "Usage: $0 {tuna|ustc|aliyun|official}"
exit 1
;;
esac
set_mirror_env() {
local name="$1"
local value="$2"
sed -i '' "/^export ${name}=/d" "$SHELL_RC" 2>/dev/null || true
echo "export ${name}=\"${value}\"" >> "$SHELL_RC"
}
set_mirror_env "HOMEBREW_API_DOMAIN" "$API"
set_mirror_env "HOMEBREW_BOTTLE_DOMAIN" "$BOTTLE"
set_mirror_env "HOMEBREW_BREW_GIT_REMOTE" "$BREW"
set_mirror_env "HOMEBREW_CORE_GIT_REMOTE" "$CORE"
if command -v brew >/dev/null 2>&1; then
BREW_REPO="$(brew --repo 2>/dev/null)" || true
if [ -n "$BREW_REPO" ] && [ -d "$BREW_REPO/.git" ]; then
git -C "$BREW_REPO" remote set-url origin "$BREW" 2>/dev/null || true
echo "已更新 brew 仓库 remote: $BREW_REPO"
fi
CORE_REPO="${BREW_REPO}/Library/Taps/homebrew/homebrew-core"
if [ -d "$CORE_REPO/.git" ]; then
git -C "$CORE_REPO" remote set-url origin "$CORE" 2>/dev/null || true
echo "已更新 homebrew-core 仓库 remote: $CORE_REPO"
fi
else
echo "警告: 未检测到 brew 命令,已经写好环境变量,但 git remote 需要你手动处理。"
fi
echo "已切换到 ${LABEL} 镜像源。"
echo "请执行: source ${SHELL_RC}"
脚本里有一个细节值得单独说一下:set_mirror_env 函数里的 sed -i '' 是 macOS 特有的写法。如果你是在 Linux 上测试,需要改成 sed -i。另外,删除旧行时我加了 2>/dev/null || true,这是为了防止第一次运行的时候文件里还没有对应配置,sed 找不到匹配行返回非零状态,导致脚本被 set -e 中断。虽然我前面没有用 set -e,但保留这个容错处理更稳。
3.2 使用步骤说明
第一步,把上面的脚本内容保存到一个文件里,比如 update-homebrew-mirror.sh。第二步,给脚本添加执行权限:
bash复制chmod +x update-homebrew-mirror.sh
第三步,执行切换命令。想切到清华源,就运行:
bash复制./update-homebrew-mirror.sh tuna
想切到中科大:
bash复制./update-homebrew-mirror.sh ustc
想切到阿里云:
bash复制./update-homebrew-mirror.sh aliyun
想恢复官方源:
bash复制./update-homebrew-mirror.sh official
脚本运行完之后,并不会立刻改变当前终端的变量,因为 export 只在当前 shell 进程里有效,而脚本是在子进程里执行的,子进程的环境变量不会回传给父进程。所以脚本会提示你执行 source ~/.zshrc,或者你也可以直接关掉终端重新开一个,效果一样。
我个人习惯是把脚本放在 ~/bin 目录下,然后配一个 alias,比如在 ~/.zshrc 里写上:
bash复制alias hm="~/bin/update-homebrew-mirror.sh"
这样以后切换镜像源只需要敲 hm tuna、hm ustc、hm official,连路径都不用记了。
3.3 验证是否生效
切换到镜像源之后,最好验证一下到底有没有生效,不然心里没底。最简单的方法是查看当前环境变量:
bash复制env | grep HOMEBREW_
正常会输出类似下面的内容:
code复制HOMEBREW_API_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api
HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles
HOMEBREW_BREW_GIT_REMOTE=https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git
HOMEBREW_CORE_GIT_REMOTE=https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git
如果只写了 ~/.zshrc,但没有执行 source ~/.zshrc,env 里是看不到这些变量的。所以验证之前先把当前 shell 刷新一下。
接下来跑一次 brew update,观察输出。切到镜像源之前,这一步经常卡在联网下载;切到镜像源之后,更新速度通常几秒钟就能完成。如果第一次跑还是慢,可以加 --verbose 参数看详细日志,确认它到底在访问哪个地址。
最后找一个体积大的包安装测试,比如:
bash复制brew install mysql
如果 bottle 下载速度明显提升,就说明 HOMEBREW_BOTTLE_DOMAIN 生效了。这套流程跑完,基本可以确认镜像源切换成功。
4. 常见问题与排查技巧实录
4.1 切换镜像后 brew update 还是慢
如果你执行完脚本、source 完配置,brew update 还是慢,首先检查环境变量是否真的生效了。有些终端工具会启动多个 shell 层,比如 iTerm2 里嵌入了 zsh,但前面可能还有一层 login shell 配置,变量会被覆盖。这个时候直接在终端里执行一次 export HOMEBREW_API_DOMAIN=...,再跑 brew update,如果能变快,说明是 shell 配置加载顺序的问题。
另一个常见原因是本地 brew 仓库的 git remote 没有改成功。脚本里我已经自动改了 brew --repo 的 remote,但你如果之前手动改过其他 remote,或者 brew 命令本身不在标准路径,脚本里的检测可能失败。手动检查一下:
bash复制git -C "$(brew --repo)" remote -v
如果输出还是 https://github.com/Homebrew/brew.git,说明 remote 没换成镜像,手动执行一下脚本里的 git remote set-url 命令即可。
还有一个小概率是镜像源本身波动。清华、中科大、阿里云都有服务状态页,遇到大面积故障时,别死磕一个源,切换到另一个源往往马上就好。
4.2 环境变量重复、脚本重复执行导致 rc 文件混乱
我见过有人把网上教程里的 export 命令复制了三遍到 ~/.zshrc,最终 Homebrew 到底用的是哪个变量,根本无从判断。用我这个脚本一般不会出现这种问题,因为每次执行都会先删除同名配置再追加。但如果你之前手动加过,最好先清理一下。
清理方法可以手动打开 ~/.zshrc,把里面所有 HOMEBREW_ 开头的 export 删除,然后重新执行脚本。如果你喜欢命令行操作,也可以用 grep 过滤出无关内容:
bash复制grep -v '^export HOMEBREW_' ~/.zshrc > ~/.zshrc.tmp && mv ~/.zshrc.tmp ~/.zshrc
这条命令会把所有 Homebrew 环境变量配置行删掉,然后你重新执行 ./update-homebrew-mirror.sh tuna,配置就是干净的了。注意先备份一份 ~/.zshrc,防止误删。
4.3 brew --repo 找不到或路径不对
正常 Homebrew 在 Apple Silicon 上安装在 /opt/homebrew,在 Intel Mac 上安装在 /usr/local。brew --repo 能自动返回正确的路径。但如果你是用第三方脚本安装的 Homebrew,或者曾经移动过 Homebrew 目录,brew --repo 可能返回一个奇怪的非标准路径,甚至直接报错。
比如一些人遇到 /storage/users/currentuser/.harmonybrew/homebrew does not exist 这种提示,就是用了非标准的 Homebrew 分支或者管理工具,它的仓库路径和官方 Homebrew 完全不一样。这种情况下,我的脚本会检测不到标准路径,会提示你手动处理 remote。解决办法是找到实际仓库路径,手动执行 git remote set-url。不要强行把脚本改成适配某个非标准路径,因为它的目录结构可能和官方版差异很大,改了反而出问题。
如果你只是想通过换镜像源解决下载慢,但 brew 命令本身都还不稳定,我建议先备份 brew list 的输出,卸载残留,重新安装官方 Homebrew,再用脚本切镜像源。基础环境不干净,换哪个源都白搭。
4.4 提示 this version of mac os is not supported on this platform
这条报错和镜像源没有直接关系,但很多人会在换源之后遇到,容易混淆。它的意思是当前 Homebrew 版本要求的最低 macOS 版本比你机器上的系统版本高,所以拒绝运行。
这种问题通常出现在老 Mac 升级 Homebrew 之后。Homebrew 官方会逐渐提高最低系统版本要求,老旧系统上的 Homebrew 一旦更新到新版本,就可能直接罢工。解决方案不是换镜像源,而是安装一个与你系统兼容的旧版本 Homebrew。建议先卸载当前 Homebrew,再根据你的 macOS 版本手动安装对应版本的 Homebrew,同时避开它自动更新的逻辑。我平时会在 ~/.zshrc 里加一个 HOMEBREW_NO_AUTO_UPDATE=1,减少自动更新带来的风险,需要更新时再手动执行。
4.5 还需要设置 HOMEBREW_NO_AUTO_UPDATE 加速吗
HOMEBREW_NO_AUTO_UPDATE=1 这个变量我用了一段时间,确实能省掉不少时间。因为它让 brew install 不再自动先跑一遍 brew update,安装小包的时候几乎秒开。但副作用是你本地的 formula 数据可能不是最新的,安装某些新软件时可能找不到。
所以我的建议是:日常用可以设置这个变量,但每隔一段时间手动执行一次 brew update,保证数据不过期。镜像源脚本切好之后,手动更新本身已经很便宜,所以配合起来很舒服。你可以在 ~/.zshrc 里写:
bash复制export HOMEBREW_NO_AUTO_UPDATE=1
如果不想永久生效,只在某次安装时临时用,直接在命令前面加:
bash复制HOMEBREW_NO_AUTO_UPDATE=1 brew install wget
4.6 卸载残留问题
有人会被 Homebrew 的卸载残留问题搞到崩溃,尤其是反复安装、卸载、再安装的机器上,/opt/homebrew 下可能残留旧的 Cellar、Caskroom 目录,~/Library/Caches/Homebrew 里还有一堆旧版缓存。这些残留文件不影响镜像源脚本运行,但会影响新装 Homebrew 的初始状态,偶尔还会导致权限错乱。
如果你决定完全清除 Homebrew,官方卸载脚本只能删掉大部分文件,漏网之鱼需要手动检查。我之前整理过一份清理清单,主要包括这些目录:
/opt/homebrew或/usr/local/Homebrew/opt/homebrew/Caskroom和/opt/homebrew/Cellar~/Library/Caches/Homebrew~/Library/Logs/Homebrew/private/var/cache/Homebrew或/private/var/log/Homebrew
清理完再重装 Homebrew,然后直接跑我的镜像源脚本,整个过程会干净很多。不要只想着换源,如果基础环境本身就带着一堆旧包袱,做什么都会慢半拍。
我用这套脚本快半年了,最有价值的不是省下那几分钟,而是它让我对 Homebrew 的配置逻辑有了更清楚的认识。镜像源本质上是把官方资源搬到离你更近的地方,但前提是你得知道 Homebrew 会去哪些地方拉数据、拉包、拉仓库。搞懂这四个环境变量,你就不需要依赖任何人的一键脚本,自己也能随手写一个。最后再分享一个小技巧:换源之后别急着重启终端,先 source ~/.zshrc,然后跑 brew update --verbose 看它到底卡在哪个环节,这样才能对症下药。脚本本身也可以继续扩展,比如把 Homebrew 的安装过程也做成镜像安装,后续我会再整理一份。
