讲个真事:上月给朋友的新 Mac 配环境,brew install wget敲下去,进度条直接卡在Updating Homebrew...环节快十分钟没动。我第一反应不是网不好,而是这家伙的 Homebrew 还顶着默认的 GitHub 源在跑。在国内网络环境下,Homebrew 从 brew、homebrew-core 到 bottle 二进制包,全程走 GitHub 那几条链路,卡、慢、超时几乎成了常态。这篇里的脚本就是我当时顺手整理的,解决一件事:在苹果电脑上一条命令把 Homebrew 默认源切成国内源,没装过的直接带源装,想还原、想卸载也有一键脚本兜底。适合刚入手 Mac 的开发者、被 brew install 折磨过的人,也适合负责团队新机装机脚本的同学抄作业。
1. 换源解决的不是"慢",而是 Homebrew 的下载链路问题
很多人以为换源就是改个网址的事,真正理解之后你会发现,Homebrew 的下载链路比想象中要复杂,这也是为什么网上经常出现"我明明换了源怎么还是慢"的疑问。
1.1 一次 brew install 背后到底访问了哪些地址
以我用 Intel Mac 和老版本 Homebrew 的体验来说,一条brew install触发的东西有:先检查 brew 本体所在的 Git 仓库是否落后(git pull),然后去拉取 formula 索引,再根据平台架构去下载对应的 bottle 预编译包。这三个动作分别对应不同的下载地址:
- brew 本体仓库和 homebrew-core、homebrew-cask 配方仓库:默认都在
github.com/Homebrew/下 - 公式索引 API:新版 Homebrew 默认从
formulae.brew.sh/api/formula.jws.json拉取 - bottle 预编译包:默认从
ghcr.io(GitHub Container Registry)下载
这四条链路任何一条慢,体现出来的结果都一样:brew install卡在某个进度条,或者干脆报curl超时。换源的本质,就是把这几条链路的默认地址全部替换成国内可访问的镜像仓库,而不是只换其中某一个。这也是我写脚本时坚持把HOMEBREW_API_DOMAIN、HOMEBREW_BOTTLE_DOMAIN、git remote 一起处理的原因。
1.2 为什么网上很多换源教程没效果
我见过不少人照着帖子改了HOMEBREW_BOTTLE_DOMAIN,结果brew update依然卡死。查下来通常是漏了两处:一是HOMEBREW_API_DOMAIN没设,公式索引还在走官方接口;二是 brew 本体仓库的 git remote 没切,brew update那一下还是要跟 GitHub 打交道。
补充一个细节:如果你用的是较老的 Homebrew 版本,它安装 formula 时还依赖 homebrew-core 这个本地 tap 仓库,这意味着换源必须包含 homebrew-core 的 git remote 切换。而 Homebrew 4.x 以后默认走 JSON API,反而没那么依赖本地 tap。所以脚本里我做了兼容处理:本地存在 homebrew-core 目录就切,不存在就直接走 API,两边都不会出问题。
2. 镜像源选型:清华、中科大、阿里,我用一份对比表说话
国内能换的 Homebrew 镜像源不少,但每个源同步策略、覆盖范围、抗压能力都不一样。选错了不是不能用,是高峰期可能比官方源还慢。这里我把自己实际测过的几个源拉出来对比。
| 镜像站 | brew/公式仓库 | bottle 下载 | API 接口 | 个人评级 |
|---|---|---|---|---|
| 清华 TUNA | 支持 | 支持 | 支持 | 首选,教育网线路快,同步及时 |
| 中科大 USTC | 支持 | 支持 | 支持 | 老牌稳定,适合中科大附近网络 |
| 阿里云 | 支持 | 支持 | 支持 | 家宽线路好,高峰期偶尔限速 |
| 腾讯云 | 支持 | 支持 | 支持 | 覆盖面不错,但文档相对少 |
2.1 清华源为什么被推荐得最多
清华 TUNA 镜像站在国内运维时间长,homebrew-bottles这个目录下不光有 bottle 包,还有对应的 API JSON 数据,两者是同一条 URL 前缀下的不同路径。这样的好处是配置简单:我只要把HOMEBREW_BOTTLE_DOMAIN和HOMEBREW_API_DOMAIN都指到同一个根域名,Homebrew 就能自动拼出正确的下载路径。
实际体验上,清华源的同步延迟通常在可接受范围内,不会出现官方发了新版本、镜像却要等好几天的情况。对大部分用户来说,换源后速度提升最明显的场景就是brew install下载 bottle 包,几百 MB 的东西,官方源可能跑到一半丢连接,清华源基本满速跑完。
2.2 中科大源的特殊技巧
中科大源有个特点:它把brew.git、homebrew-core.git、homebrew-cask.git分别放在平级路径下,而且对老版本 Homebrew 的兼容做得不错。如果你还在用某些特殊渠道装的旧版本 brew,USTC 会是更省心的选择。
据说中科大建议对HOME_DIR下的 Homebrew 目录做remote set-url切换,这个动作恰恰是我脚本里核心的操作。各家源地址差异主要就是 URL 前缀,所以我给脚本预留了三个源参数,实际使用中想换哪个,改一个参数就行。
3. install.sh 完整实现:从零安装与已装切换两条路径
这个脚本我写了两个功能分支。第一分支是检测到 Mac 上压根没装 Homebrew,直接利用官方安装脚本支持的环境变量,实现"带着国内源完成安装";第二分支是已装过 Homebrew,用git remote set-url把仓库地址切到镜像,并写入持久化环境变量。
3.1 脚本的整体骨架
脚本主要有四个动作:选源、检测、切换、写入配置。我特意把选源放在最前面,方便你改成自己常用的镜像:
#!/bin/bash set -euo pipefail # ===================================================== # install-mirror.sh # 用法: bash install-mirror.sh [tuna|ustc|aliyun] # 功能: Homebrew 一键换国内源,未安装时自动带源安装 # ===================================================== MIRROR_FLAG="${1:-tuna}" case "$MIRROR_FLAG" in tuna) BREW_GIT="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git" CORE_GIT="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git" CASK_GIT="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git" API_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api" BOTTLE_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles" ;; ustc) BREW_GIT="https://mirrors.ustc.edu.cn/brew.git" CORE_GIT="https://mirrors.ustc.edu.cn/homebrew-core.git" CASK_GIT="https://mirrors.ustc.edu.cn/homebrew-cask.git" API_DOMAIN="https://mirrors.ustc.edu.cn/homebrew-bottles/api" BOTTLE_DOMAIN="https://mirrors.ustc.edu.cn/homebrew-bottles" ;; aliyun) BREW_GIT="https://mirrors.aliyun.com/homebrew/brew.git" CORE_GIT="https://mirrors.aliyun.com/homebrew/homebrew-core.git" CASK_GIT="https://mirrors.aliyun.com/homebrew/homebrew-cask.git" API_DOMAIN="https://mirrors.aliyun.com/homebrew/homebrew-bottles/api" BOTTLE_DOMAIN="https://mirrors.aliyun.com/homebrew/homebrew-bottles" ;; *) echo "未知镜像源: $MIRROR_FLAG,可用值: tuna / ustc / aliyun" exit 1 ;; esac提示:不确定选哪个源的时候,默认
tuna就好。教育网、家宽、移动、联通我都试过,综合表现最稳。
3.2 分支一:未装 Homebrew,直接用国内源完成安装
官方安装脚本本身就支持通过环境变量指定 git 仓库地址。很多人不知道这一点,还在傻等官方安装脚本慢慢拉 GitHub,这个做法太不推荐了。完整的安装分支如下:
if ! command -v brew >/dev/null 2>&1; then echo "未检测到 Homebrew,使用带国内源的方式安装..." export HOMEBREW_BREW_GIT_REMOTE="$BREW_GIT" export HOMEBREW_CORE_GIT_REMOTE="$CORE_GIT" export HOMEBREW_CASK_GIT_REMOTE="$CASK_GIT" export HOMEBREW_API_DOMAIN="$API_DOMAIN" export HOMEBREW_BOTTLE_DOMAIN="$BOTTLE_DOMAIN" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" else echo "已检测到 Homebrew,执行仓库源切换..." brew_prefix="$(brew --prefix)" git -C "$brew_prefix" remote set-url origin "$BREW_GIT" core_tap="$brew_prefix/Library/Taps/homebrew/homebrew-core" cask_tap="$brew_prefix/Library/Taps/homebrew/homebrew-cask" if [[ -d "$core_tap" ]]; then git -C "$core_tap" remote set-url origin "$CORE_GIT" fi if [[ -d "$cask_tap" ]]; then git -C "$cask_tap" remote set-url origin "$CASK_GIT" fi fi这里需要注意,brew --prefix在 Apple Silicon 机器上通常是/opt/homebrew,Intel 机型是/usr/local,用命令取出来的值比写死路径可靠得多。另外在新版 Homebrew 里,homebrew-core 可能不会以本地 tap 的形式存在,所以我都加了-d目录判断,目录不存在就跳过,不影响整体。
3.3 分支二:把环境变量写成持久化配置
光切 git remote 还不够,bottle 下载和 API 接口索引还要靠环境变量。所以脚本最后的动作是写入 shell 配置文件。这里注意几个细节:
- macOS 默认 shell 是 zsh,但不少人装了 bash,所以脚本要自动检测
- 写入前先把旧的
export HOMEBREW_开头行清掉,避免换源后残留旧配置导致脚本反复追加 - 不要用
echo "$var" >> file这种粗暴追加法,要先清理再写入
detect_profile() { if [[ -f "$HOME/.zprofile" ]]; then echo "$HOME/.zprofile" elif [[ -f "$HOME/.zshrc" ]]; then echo "$HOME/.zshrc" elif [[ -f "$HOME/.bash_profile" ]]; then echo "$HOME/.bash_profile" else echo "$HOME/.zshrc" fi } PROFILE="$(detect_profile)" sed -i '' '/^export HOMEBREW_/d' "$PROFILE" 2>/dev/null || true cat >> "$PROFILE" <<EOF export HOMEBREW_API_DOMAIN="$API_DOMAIN" export HOMEBREW_BOTTLE_DOMAIN="$BOTTLE_DOMAIN" export HOMEBREW_BREW_GIT_REMOTE="$BREW_GIT" export HOMEBREW_CORE_GIT_REMOTE="$CORE_GIT" export HOMEBREW_CASK_GIT_REMOTE="$CASK_GIT" EOF echo "环境变量已写入 $PROFILE" echo "执行 source $PROFILE 后建议先运行一次 brew update"这里还隐藏了一个细节:为什么连HOMEBREW_CORE_GIT_REMOTE这种新版本已经不太用的变量也要写?因为老版本和不走 API 的配置模式下,缺了它,brew update依然会跑回官方地址。写全了兼容性最好,代价无非是多几行 export。
4. uninstall.sh 与残留清理:卸载不完全比不卸载更难受
Homebrew 卸载比安装更容易翻车。官方提供了 uninstall 脚本,但它同样需要从 GitHub 拉取;而且即使卸载完成,shell 环境变量、缓存目录、日志目录都会残留。所以我做卸载脚本时,把它分成三步:还原源、卸载本体、清理残留。
4.1 先用官方卸载脚本,网络兜底方案再手动删除
官方脚本能自动识别 brew 装在哪、要不要保留某些目录,比手工rm -rf安全得多。所以在卸载脚本里我优先调用官方脚本,只有命令失败才提示手动删除:
echo "第一步:执行官方卸载脚本" if ! /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)" -- --skip-cache-and-logs; then echo "官方卸载脚本执行失败,尝试手动删除" brew_prefix="$(brew --prefix)" if [[ -d "$brew_prefix" ]]; then rm -rf "$brew_prefix" echo "已删除 $brew_prefix" fi fi--skip-cache-and-logs参数的意思是卸载时不用动缓存和日志目录,这两个目录后面由我自己接管。如果你装了 MySQL、PostgreSQL 这类把数据放在$(brew --prefix)/var下的软件,官方卸载脚本会提示你数据残留,记得提前备份。
4.2 清理环境变量与缓存目录
卸载完成后,~/.zprofile或~/.zshrc里的 export 行不会自己消失。下次新装 Homebrew 时,这些残留变量会继续把新 brew 指向旧镜像,不算致命,但会带来"为什么我重装还是这么慢"的困惑。所以清理脚本要把它们一并处理:
echo "第二步:清理 shell 环境变量残留" PROFILE="$(detect_profile)" sed -i '' '/^export HOMEBREW_/d' "$PROFILE" 2>/dev/null || true echo "已从 $PROFILE 中移除 HOMEBREW_ 相关环境变量" echo "第三步:清理缓存与日志目录" rm -rf "$HOME/Library/Caches/Homebrew" rm -rf "$HOME/Library/Logs/Homebrew" echo "缓存与日志已清理"写完这个脚本我特意测试过:先手动往 zshrc 里塞了几行export HOMEBREW_BOTTLE_DOMAIN=...,跑完脚本后grep -n HOMEBREW ~/.zshrc输出为空,清理得很干净。另外临时文件目录、旧版安装器留下的/Library/Developer/CommandLineTools是否要删,看个人需求,一般不建议动系统工具链。
5. 换源后的验证与常见坑:环境变量、权限和缓存这三道坎
脚本写完了不代表万事大吉。我在测试时踩过几个坑,每一步都有对应的排查方法,这里按"先验证、后处理坑"的顺序说。
5.1 换源是否生效,看这三处输出
第一处,看brew config。终端里执行brew config,正常情况下能看到类似下面的输出:
HOMEBREW_BOTTLE_DOMAIN: https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottlesHOMEBREW_API_DOMAIN: https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api
如果这里还是空,或者写着ghcr.io,那就是环境变量没 load 进来,执行source ~/.zprofile再重开一个终端会话试试。
第二处,看 brew 本体的 git remote。执行:
git -C "$(brew --prefix)" remote -v输出里的origin应该指向mirrors.tuna.tsinghua.edu.cn开头的地址,如果还是github.com/Homebrew/brew.git,说明切换分支没有真正生效。
第三处,重新装一个包,观察下载 URL。挑一个包,比如brew install htop,在下载 bottle 时注意终端输出的地址。如果是指向清华或者中科大源的homebrew-bottles,说明 bottle 路径没问题。
5.2 权限问题:Cannot write to /opt/homebrew的根源
Homebrew 对安装目录的属主非常敏感。有些时候我们为了装软件,会对/opt/homebrew或者/usr/local执行sudo chown -R,但某些文件属主还是 root,导致brew update报出权限类的错误。
这种问题用脚本没法根治,正确的检查姿势是:
ls -ld "$(brew --prefix)"输出应该显示当前用户有读写权限。如果没有,执行:
sudo chown -R "$(whoami):admin" "$(brew --prefix)"如果权限修复后brew update依然报错,可以用brew doctor看看还有什么历史遗留问题。说实话,很多 Mac 上 Homebrew 的怪毛病,brew doctor都能直接给出修复建议,这是最被低估的排查命令。
5.3 缓存导致的"假换源"
换源最隐蔽的坑是缓存。你换源之前,Homebrew 已经把某些软件的下载地址缓存到了~/Library/Caches/Homebrew。换源后再装同一个包时,如果缓存里有对应版本的下载记录,它可能还在用旧的下载地址做校验,甚至直接复用断点缓存的数据。
遇到这种情况,我的经验是换源后先做一次彻底清理:
rm -rf "$HOME/Library/Caches/Homebrew/downloads"然后重新安装目标软件。别嫌这一步多余,它能消灭九成"换源没效果"的误判。
6. 周边工具的联动:pip、npm、Jenkins 换源思路与 Homebrew 一致
Homebrew 这套换源逻辑其实能复用到很多其他包管理器。近期常看到有人搜 pip 国内源、npm 国内源、Jenkins 插件国内源,底层套路惊人地相似:定位默认下载地址,改成镜像站对应路径,然后写入配置文件持久化。
6.1 pip 和 npm 的换源操作
pip 如果觉得安装第三方库慢,最简单的是执行:
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple这条命令和 Homebrew 写入HOMEBREW_API_DOMAIN的思路一模一样,都是把"从哪里下载包"这个配置写进用户级配置文件。npm 则是:
npm config set registry https://registry.npmmirror.com反观 Jenkins 插件中心,本质上就是把update-center.json的下载地址替换成 Jenkins 官方中文镜像提供的 URL,配置完重启 Jenkins 后,插件安装速度会有肉眼可见的提升。
6.2 换源这几个通用坑,跨工具都成立
我踩过这么多换源方案后发现,不管换什么源,坑基本都是那几类:一觉醒来镜像站同步滞后导致装到了旧版本;缓存没有清理导致下载的还是旧地址;配置文件格式写错导致工具直接罢工。所以无论换哪个源,建议遵循同一个流程:先备份原配置、再修改、然后验证。Homebrew 这套脚本只是把这个流程自动化了,核心方法论是可以迁移到所有包管理器的。
顺便说一句,脚本里的源地址如果哪天发现失效,先去对应镜像站的首页确认路径是否变动,Homebrew 镜像的 URL 组织方式偶尔会调整,改一下case分支里的地址就能继续用。