为什么要重装

本地的 Homebrew 从 6.x 一路升级上来,混杂了多年前的 tuna 镜像源配置、残留的 Cask,以及一些不再使用的 formula。这次决定推倒重来:彻底卸载(含所有 Cask 与配置),装最新版,再把需要的东西装回来,最后加一个每周自动更新的定时任务,一劳永逸。

整个过程在 Apple Silicon 的 Mac(brew 前缀为 /opt/homebrew)上完成,旧版本 6.0.21,新版本 7.0.6。

第一步:卸载前先摸底

动手前先记录现场,免得重装时想不起来原来装过什么:

brew list --formula | wc -l   # 48
brew list --cask              # datagrip google-chrome intellij-idea lens orbstack typora
brew tap
brew services list

另外记下 shell 配置里和 brew 相关的行(~/.zprofile 里的 brew shellenv 和镜像源 export),后面清理要用。

第二步:卸载 Cask(带 —zap)

Cask 的卸载分两步走:先用 --zap 卸载,这样连应用的用户配置数据一起清掉,才算”彻底”:

brew services stop --all
for c in datagrip google-chrome intellij-idea lens orbstack typora; do
  brew uninstall --cask --zap --force "$c"
done

这里踩了两个坑:

  1. --zap 会删用户数据,务必想清楚。比如 DataGrip / IntelliJ 的设置和插件目录会被清空。Chrome 的用户数据(书签、历史)同样在 zap 范围内,这次因为缺少”完全磁盘访问权限”而跳过了,反而保住了数据——算是因祸得福。
  2. OrbStack 有系统级残留。它的 ~/Library/Group Containers/...dev.orbstack 目录连 sudo 都删不掉(TCC 拦截),需要先给终端开启”完全磁盘访问权限”再手动清理。

收尾时还有两个 Cask(google-chrome、typora)因为 zap 阶段报错而停留在”已安装”状态,用不带 --zap 的强制卸载补刀即可,App 本体当时其实已经被删掉了:

brew uninstall --cask --force google-chrome typora
brew list --cask   # 空,干净了

第三步:运行官方卸载脚本

curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh -o /tmp/brew_uninstall.sh
NONINTERACTIVE=1 bash /tmp/brew_uninstall.sh --force --path=/opt/homebrew

官方脚本默认会连 ~/Library/Caches/Homebrew 和 ~/Library/Logs/Homebrew 一起删(除非加 --skip-cache-and-logs),这一步不需要额外操作。

但脚本在删除 /opt/homebrew 本体和 /etc/paths.d/homebrew 时需要 sudo,在非交互终端里会直接报 Need sudo access 而不删除任何东西。我的解法是用 macOS 的 GUI 提权弹窗代替终端输密码:

osascript -e 'do shell script "rm -rf /opt/homebrew /etc/paths.d/homebrew" with administrator privileges'

会弹出系统密码框,输一次开机密码即可,适合各类自动化场景。

第四步:手动清理残留配置

官方脚本不管 shell 配置文件,手动清理 ~/.zprofile 里这几行:

eval "$(/opt/homebrew/bin/brew shellenv zsh)"
export HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"
export HOMEBREW_CORE_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git"

删掉旧的 tuna 镜像源,让新装的 brew 回到官方源。至此旧环境清空约 1.4GB,which brew 已经找不到了。

最大的一课:我的 pi 命令行工具装在旧 brew 前缀里(/opt/homebrew/bin/pi),rm -rf /opt/homebrew 把它一起带走了。重装 brew 之后记得把装在 prefix 里的全局工具也重装一遍。

第五步:安装最新版 Homebrew

官方安装命令配合非交互模式:

curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o /tmp/brew_install.sh
NONINTERACTIVE=1 bash /tmp/brew_install.sh

同样有 sudo 密码问题。install.sh 明确拒绝以 root 身份整体运行(Don't run this as root!),但它支持 SUDO_ASKPASS——于是写一个用 AppleScript 弹窗要密码的 askpass helper:

#!/bin/bash
# /tmp/brew_askpass.sh
osascript -e 'display dialog "Homebrew 安装需要管理员密码:" default answer "" with hidden answer buttons {"确定"} default button "确定" with title "Homebrew"' -e 'text returned of result'
chmod +x /tmp/brew_askpass.sh
SUDO_ASKPASS=/tmp/brew_askpass.sh NONINTERACTIVE=1 bash /tmp/brew_install.sh

安装过程会弹出一次图形密码框,输完密码后全程自动,一次成功。装完按提示把 eval "$(/opt/homebrew/bin/brew shellenv zsh)" 加回 ~/.zprofile,新开终端验证:

brew --version   # Homebrew 7.0.6
brew list        # 空的,干干净净

第六步:把软件装回来

按摸底时记下的清单原样恢复。48 个 formula 一条命令(依赖会自动解决):

brew install awscli brotli c-ares ca-certificates cairo fontconfig freetype gettext \
  giflib glib graphite2 harfbuzz hdrhistogram_c icu4c@78 jpeg-turbo kubernetes-cli \
  libnghttp2 libnghttp3 libngtcp2 libpng libtiff libunistring libuv libx11 libxau \
  libxcb libxdmcp libxext libxrender little-cms2 lz4 lzo maven mpdecimal node@24 \
  openjdk openssl@3 pcre2 pixman python@3.13 readline simdjson sqlite uv uvwasi \
  xorgproto xz zstd

6 个 Cask:

brew install --cask datagrip google-chrome intellij-idea lens orbstack typora

其中 intellij-idea 第一次下载遇到 curl: (92) HTTP/2 stream ... PROTOCOL_ERROR,单独重试一次就成功了——JetBrains 的大包下载偶发失败很常见,重试即可。当时恢复到:63 个 formula(含自动拉入的依赖)+ 6 个 Cask,全部最新版。OrbStack 装完后记得手动打开一次 App 完成初始化。

后续又把系统里其他能 brew 管理的应用也迁了进来,并装了 Ghostty,见下文。

第七步:每周自动更新

手动更新坚持不了几周,写个脚本交给 macOS 原生的 launchd。脚本 ~/scripts/brew-weekly-upgrade.sh 核心就是:

brew update
brew upgrade
brew cleanup
brew autoremove

外加几处工程化细节:

  • 用 mkdir 原子操作做防重入锁,避免上次没跑完又触发;
  • 日志写到 ~/Library/Logs/Homebrew/weekly-upgrade.log,超 5MB 自动轮转;
  • 代理自适应:本地代理端口通就用它,不通就直连(提前验证过直连 GitHub 没问题)。

定时任务 ~/Library/LaunchAgents/com.lucas.brew-weekly-upgrade.plist,每周一 09:00 执行:

<key>StartCalendarInterval</key>
<dict>
    <key>Weekday</key>
    <integer>1</integer>
    <key>Hour</key>
    <integer>9</integer>
    <key>Minute</key>
    <integer>0</integer>
</dict>

注册并验证:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.lucas.brew-weekly-upgrade.plist
launchctl print gui/$(id -u)/com.lucas.brew-weekly-upgrade | grep -E "state|calendar"
# 手动试跑一次
bash ~/scripts/brew-weekly-upgrade.sh

把系统里其他应用也迁入 brew

重装完成后顺手盘点了 /Applications,把能用 brew 管理的应用全部迁入 brew,这样每周自动更新能一并覆盖。方法:先 brew search --cask 确认 token,再用 brew info --cask --json=v2 核对装出来的 App 名和本地一致(大小写无所谓,macOS 文件系统不敏感),然后删旧版、brew install --cask 装回。只删 App 本体,~/Library 里的账号数据和设置都在,打开即原样。

这次迁入的 13 个:百度网盘、ChatGPT、Claude、GitHub Desktop、Sourcetree、腾讯会议、迅雷、VirtualBox、WebStorm、微信、企业微信(token 是 wechatwork)、WPS 国际版(本机是 12.1.26046,对应 wpsoffice 而非 wpsoffice-cn)、语雀。

几个有意没动的:

  • Antify:搜过,没有对应 Cask,保持不动。
  • 爱思助手(i4Tools):没有 Cask,直接卸载。顺手把残留一起清了:~/Library/Preferences/cn.i4Tools.mac.plist 和 ~/Library/Application Support/爱思助手,复查 /Library 启动项无残留。
  • 豆包浏览器:那个 948 字节的跳转小文件,一直都在,从没动过(别和豆包主 App 搞混)。

两个”钉子户”:豆包与网易云音乐

豆包和网易云音乐有 Cask,但装不上。下载没问题(hdiutil verify 显示 checksum VALID),卡在 brew 内部的 hdiutil attach:系统弹”此磁盘映像可能有问题”的确认框,在非交互环境下被自动取消。试过清隔离属性、Finder 里手动点确认、-noverify,一律不行;但同文件用 diskutil image attach 一次挂载成功——这是新系统上 hdiutil 的策略问题,不是文件损坏。

解法:diskutil 挂载后手动把 Doubao.app / NeteaseMusic.app 拷回 /Applications,然后卸载镜像。这两个就只能手动管理了——好在它们自带应用内自动更新。每周更新脚本里加了一段提醒,每次跑完顺带报告它俩的版本号,看到有新版就去应用内点更新。

Ghostty:安装与配置

顺手把终端换成了 Ghostty:

brew install --cask ghostty
brew install --cask font-jetbrains-mono

字体版权提醒:JetBrains Mono 采用 SIL Open Font License 1.1,个人与商业使用均免费;且该 Cask 从 JetBrains 官方 GitHub Releases 直接下载,无版权风险。

配置在 ~/Library/Application Support/com.mitchellh.ghostty/config:

# 无限滚动:1 亿行,实际只受内存限制
scrollback-limit = 100000000

font-family = "JetBrains Mono"
font-size = 16

# 行高 1.3 倍(Ghostty 没有单独的 line-height,用单元格高度百分比)
adjust-cell-height = 30%

改完新开窗口即生效。

踩坑总结

  1. --zap 是双刃剑:清得彻底,但连 IDE 配置、插件一起删,执行前确认备份。
  2. Cask 卸载可能因”完全磁盘访问权限”部分失败:App 本体一般能删掉,残留的用户数据手动补删即可,Chrome 书签这次就是这样保住的。
  3. 非交互环境下的 sudo:osascript ... with administrator privileges 和 SUDO_ASKPASS 组合,可以在不暴露密码的前提下完成提权。
  4. 别删了自己脚下的船:通过 brew prefix 安装的全局工具(如 pi、npm 全局包)会随 rm -rf /opt/homebrew 一起消失,提前列出来,重装后补回。
  5. 大包 Cask 下载失败先重试:JetBrains 系的 dmg 偶发 PROTOCOL_ERROR,基本都是网络抖动,重试一次就好。
  6. 迁移前先核对 App 名:用 brew info --cask --json=v2 看 artifact 里的 App 名和本地是否一致,删错了很麻烦;企业微信的 token 是 wechatwork,WPS 有国际版和 CN 版之分,先看清本机版本再选。
  7. 不是所有 Cask 都装得上:某些国产厂商的 dmg 在新系统上过不了 hdiutil 挂载确认,认栽手动装,让它们用应用内更新即可。