Homebrew 停止 Intel 支持:迁移到 MacPorts 的完整笔记
📌 TLDR
Homebrew 从 2026-09 起停发 Intel (x86_64) 的预编译 bottle:没有 bottle 的 formula 每次升级都要本机编译,实测 8GiB 机器上 rust 类常报 SIGSEGV、llvm 一编就是很久。而 MacPorts 2.12.6 仍持续发 x86_64 二进制。迁移原则很干脆:有预编译 archive 的才迁到 MacPorts,需要源码编译的留在 brew。探测办法是 curl -I 看 packages.macports.org 上对应 tbz2 是否 200;注意文件名可能带 +pcre、+gnutls 这类 variant 后缀,裸名 404 不代表没有,可列目录确认。uv、rustup 这类语言自带工具链两头都不进包管理器。
2026 年 9 月起,Homebrew 停止提供 Intel (x86_64) 的预编译 bottle。对 Intel Mac 用户意味着:任何没有预编译包的 formula,每次升级都要在本机源码编译。实测 rust 类程序在 8GiB 内存的机器上编译常报 SIGSEGV 栈溢出,llvm 等大依赖一次要编译很久。
替代方案 MacPorts 2.12.6 仍持续发布 x86_64 预编译二进制包。将重编译型软件迁到 MacPorts,brew 只留小工具,是一条可行的路。本文整理多台 Intel Mac 迁移的完整经验。
适用范围:本文针对 Intel (x86_64) Mac。Apple Silicon / 新版 macOS 仍有完整 bottle 供给,无需迁移——fleet 里一台 arm64 + macOS 27 的机器继续用 brew(135 个 formula、MySQL 服务健康),零改动。
迁移决策:只迁有预编译 archive 的包
核心原则:MacPorts 有预编译 archive 才迁移;需要源码编译的保留 brew 版。另有一类根本不进系统包管理器——语言生态自带的官方工具链(Python 的 uv、Rust 的 rustup 等),见下文。
archive 探测(200 即迁,404 则保留):
curl -sI -o /dev/null -w "%{http_code}\n" \
https://packages.macports.org/eza/eza-0.23.5_0.darwin_24.x86_64.tbz2
_0 是 port revision,可能为 _1/_2。404 不一定是没有:
- variant 后缀:有的 archive 文件名带 variant,如
ripgrep-15.2.0_0+pcre.darwin_24.x86_64.tbz2、wget-1.25.0_1+gnutls.darwin_24.x86_64.tbz2。裸文件名探 404,加上 variant 就是 200。port install会自动选择默认 variant,无需手动加。 - 判断是否真的没有:看目录列表
https://packages.macports.org/<port>/,能列出全部可用 archive 文件名。
端口命名与 brew 的差异
| brew | MacPorts 端口 |
|---|---|
| python@3.14 | python314 |
| go | go(元端口,go_toolchain wrapper:拉 go-1.27 + 建 /opt/local/bin/go select 链接) |
| httpd | apache2 |
| node@22 | 用户级官方安装(node 不走端口,见「语言官方工具链」) |
| mysql-client@8.4 | mysql9(等官方二进制) |
| imagemagick | ImageMagick7 |
| ffmpeg | ffmpeg(+gpl2 variant 才有完整编解码) |
🧰 前置准备
免密执行 port
批量迁移要全自动,给 port 开 NOPASSWD(只限这一条命令):
echo "user ALL=(ALL) NOPASSWD: /opt/local/bin/port" | sudo tee /etc/sudoers.d/port-nopasswd
PATH 优先级
~/.zprofile 里把 /opt/local 前置,新终端自动优先用 MacPorts 版本:
export PATH="/opt/local/bin:/opt/local/sbin:$PATH"
关闭 rev-upgrade(关键,见坑 1)
🧭 迁移流程(每批)
- 建清单:
brew list --formula、brew leaves - 逐项探测 archive
sudo port install <port>→ 看到---> Fetching archive即走预编译- 验证:
port installed active+ 跑二进制--version冒烟 - 卸载 brew 副本:
brew uninstall <...>+brew autoremove - 复查 leaves 与依赖方
⚠️ 坑与经验
1. rev-upgrade 假阳性死循环(最值钱的教训)
症状:每次 port install/upgrade 后 ffmpeg 被反复重建,一次约 40 分钟,连续失败。
根因:MacPorts 默认每次装完自动跑 rev-upgrade,扫描已装二进制的动态库链接一致性。ffmpeg 链接 CoreImage.framework:SDK stub 声明 compat 1.0.1、运行时框架报 1.0.0 → 被永久误判 broken → 自动 rebuild。
修复:
# /opt/local/etc/macports/macports.conf
revupgrade_autorun no
revupgrade_mode none
revupgrade_autorun no:install/upgrade 后不再自动跑 rev-upgrade(断掉死循环)revupgrade_mode none:即使手动sudo port rev-upgrade,也只报告不 rebuild(双保险)
路径陷阱:MacPorts 只读 /opt/local/etc/macports/macports.conf;根目录 /opt/local/etc/macports.conf 是废文件,改了没用。无 sudo 时可用用户级 ~/.macports/macports.conf 覆盖——解析顺序为 系统 → 用户级,后读覆盖先读。
验证:用 perl/tclsh 按同样顺序解析两个文件,确认最终 revupgrade_autorun = no、revupgrade_mode = none。
2. brew autoremove 清不干净孤儿
症状:卸载主包后,它的旧依赖会逐个浮出为 brew leaves(如 libheif→aom/libde265,curl→pcre2,gpgme→gnupg/libgcrypt,pipx→python@3.14),而 autoremove 因旧回执缺 installed_as_dependency 标记经常漏掉它们。
处置:brew uses --installed <pkg> 确认无任何已装 formula 依赖 → brew uninstall。要清理好几波,每波卸载后可能又冒出新孤儿;gpgme/gnupg 那条加密链(gmp/gnutls/nettle/p11-kit 等)尤其长。
3. ssh 远程批量运维
- 复合命令(ssh + 多行管道)容易超时/被拒,拆成最小单命令逐个跑
- 引号:
ssh host 'perl ~/...'用单引号让~在远程展开,否则会在本地展开报错
4. ls 被 eza 替代
装了 eza 并 alias 成 ls 的机器,脚本里 ls 参数不兼容会静默误报"文件不存在"。探测一律用 /bin/ls 或 [ -f path ]。
5. 脚本硬编码 Homebrew 路径 → 迁移后 iperf3「找不到」
症状:迁到 MacPorts 后,tailscale-mesh-bench.sh(chedong.com 仓库 cli/tools/tailscale/)在 mbprodong 报 iperf3 not found — skipping as server,该节点整行 N/A,其他节点测它时 REFUSED。
根因:脚本定位 iperf3 用的是 find /usr/local /opt/homebrew -name iperf3,只认 Homebrew 两种前缀(Intel /usr/local、ARM /opt/homebrew),漏了 MacPorts 的 /opt/local。更隐蔽的是:SSH 非交互会话 PATH 只有 /usr/bin:/bin:…,command -v/which 也找不到 /opt/local/bin/iperf3——所以脚本只能靠 find 显式前缀,而前缀清单没跟着迁移更新。
修复:find 里补 /opt/local:
find /usr/local /opt/homebrew /opt/local -name iperf3 -type f 2>/dev/null | head -1
验证:重跑后 mbprodong 正常 server started (/opt/local/bin/iperf3, ...),全网状测试补全、REFUSED/N/A 消失。
通用教训:凡 hardcode 了 /usr/local、/opt/homebrew 的脚本/alias/Makefile,迁移后都要补 /opt/local。排查路径一次扫全三处:find /usr/local /opt/homebrew /opt/local -name <bin> \( -type f -o -type l \),别靠 which(非交互 SSH 下 PATH 不完整,which 会误报 NONE)。
6. npm 全局包 EACCES:MacPorts 端口 prefix 是 root 的 /opt/local
症状:nodejs22/npm10 端口装好后,npm install -g <pkg> 报 EACCES: permission denied ... /opt/local/lib/node_modules。因为 npm 默认 prefix 跟着 node 安装目录走,即 root 所有的 /opt/local。
处置:Node 干脆不占端口,改走语言官方工具链(见下节),官方 tarball 解压到用户目录:
mkdir -p ~/.local/node ~/.local/bin
curl -fsSL -o /tmp/node.tgz https://nodejs.org/dist/v22.23.1/node-v22.23.1-darwin-x64.tar.gz
tar -xzf /tmp/node.tgz --strip-components=1 -C ~/.local/node && rm /tmp/node.tgz
ln -sf ~/.local/node/bin/{node,npm,npx} ~/.local/bin/
npm config set prefix ~/.local # 全局包装进 ~/.local,bin 落 ~/.local/bin,免 sudo
~/.zprofile 里 ~/.local/bin 排在 /opt/local/bin 之前。此后 npm i -g 不再报 EACCES,全局二进制(如 taotoken)落 ~/.local/bin。旧的 /opt/local/lib/node_modules(端口时代 root 装的全局包)与 /usr/local 残留(brew 时代的 node/npm/npx 二进制)一并清理即可。
7. go 端口激活冲突:Image error … does not belong to a registered port
症状:sudo port upgrade outdated 时,fzf/glab 等需源码编译的 Go 程序会自动拉入 go 端口做构建依赖;若 /opt/local/bin/go、/opt/local/bin/gofmt 已被未注册软链占用(迁移期手动创建、指向 ../lib/go-1.27/bin/go),激活阶段报:
Error: Image error: /opt/local/bin/go already exists and does not belong to a registered port
整批升级中断,同一批次里多台机器先后中招。
修复:
sudo port -f activate go # -f 强制覆盖未注册文件(原文件改存 go.mp_<时间戳>)
sudo port upgrade outdated # 重跑整批
8. 单次 port 进程的 archive 缓存缺陷:无 archive 端口致命 404、不回退源码
症状:同批 port upgrade outdated,个别机器对无 archive 的端口(如 fzf)报:
Error: Failed to archivefetch fzf: Failed to fetch archive for fzf: The requested URL returned error: 404
而不是预期的 Archive not available for fzf, building locally 源码回退。同批其他机器能正常回退,唯独这台整批卡死。
根因:portutil::archive_available_result(“该端口有没有可下载 archive"的探测结果)在单次 port 进程内缓存、跨端口共享且不重置。批次里任一端口确认有 archive,缓存即置 1;之后处理到无 archive 的端口时误判"archive 应该存在”,把所有镜像试完后直接 404 报错中断。
判定:正常回退路径的日志里有 Checking if ... exists at <site> 探测行 + Archive not available for X, building locally;被缓存误判的日志没有探测行,直接试完全部镜像后致命 404。
修复:对无 archive 的端口显式强制源码、绕过 archivefetch:
sudo port -s upgrade fzf # -s 强制源码编译;有 archive 的端口继续正常走预编译
9. NOPASSWD sudo 只在写全路径时生效
/etc/sudoers.d 里 NOPASSWD: /opt/local/bin/port 只匹配全路径。非登录 shell(pdsh / ssh 远程命令)的 PATH 不含 /opt/local/bin,sudo port ... 解析不到全路径 → 仍要密码。用 source ~/.zprofile ; sudo port ...(把 /opt/local/bin 加进 PATH),或直接写 sudo /opt/local/bin/port ...。
系统包管理之外的:改用语言官方工具链
brew/port 不是万能的,也不该是所有软件的归宿。凡是语言生态自带包管理器/官方安装器的,优先用官方工具——它们自带隔离环境、独立升级、不污染系统路径,也不被 brew/port 的依赖链和 rev-upgrade 拖累。
| 生态 | 官方工具 | 安装方式 | 说明 |
|---|---|---|---|
| Python | uv | curl -LsSf https://astral.sh/uv/install.sh | sh |
独立管理 venv 与 Python 版本,替代 brew python + pipx |
| Rust | rustup | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
toolchain 隔离,rustc 慢编译不进系统 |
| Node | 官方 tarball | 解压到 ~/.local/node,npm config set prefix ~/.local |
用户级 node/npm,全局包免 sudo(见坑 6) |
| Go | go install | go install <pkg>@latest |
二进制直接进 $(go env GOBIN) |
| 前端 CLI | corepack / npm i -g | 随 Node | pnpm/yarn 等自带 |
两个注意点:
- PATH 顺序:官方安装器一般装到
~/.local/bin、~/.cargo/bin、~/.nvm/…,确认它们排在/opt/local之前(which uv、which node验一下),别让系统包管理版本抢跑。 - 与上文的分类:brew/port 管系统级工具,语言官方工具链管语言项目工具,两者职责分开。迁移时少了很多"要不要迁"的纠结——能走官方工具链的直接走官方工具链。
保留在 brew 的类别
| 类别 | 例子 |
|---|---|
| 内存不足以源码编译的 rust 程序 | eza(本机 4GiB rustc SIGSEGV → 定案保留 brew pin;8GiB 及以上实测编得动,fleet 4 台已 sudo port -s install eza 成功,约 20-40 分钟) |
| 等 MP 官方二进制 | MySQL(mysql-innovation/-server/mysql9 x86_64 archive 发布后迁,2026-10 探测仍 404) |
| MP 无对应端口 | sdl2-compat、tldr |
| brew 版本领先 MP | cmake(brew 4.4.3 vs MP 3.31.12) |
rust 程序源码编译的内存分界(eza 0.23.5 实测):8GiB 通过、4GiB SIGSEGV。若 rust 程序的 x86_64 archive 一直不出,内存够的机器可以不等包直接 port -s install;4GiB 机器只能等 archive 或保留 brew 版(brew 0.23.5 与 MP 0.23.5 功能一致,不影响使用)。
终态检查
brew leaves只剩故意保留的项port installed active数量显著上升- 服务方式变化:
brew services→sudo port load <port>(如 apache2)
复查清单
- mysql9:
curl -sI https://packages.macports.org/mysql9/mysql9-9.7.2_1.darwin_24.x86_64.tbz2200 即sudo port install mysql9→ 卸 brew mysql-client(2026-10 探测仍 404,继续等) - glab:✅ 已有 x86_64 archive(1.118.0_0,2026-10 探测 200)→
sudo port install glab补迁 - pkgconf:有 archive(3.0.7_0)但
pkgconf与pkgconfig端口互斥,且机器已装pkgconfig @0.29.2(ImageMagick/gobject-introspection 硬依赖)→ 无需替换,pkg-config 功能已由 MP 提供,port install pkgconf会报 conflict - eza:4 台(8/8/8/16GiB)已
sudo port -s install eza源码编译装齐(+doc+git 变体;port install会因无 archive 404 中止整批,必须显式-s);本机 4GiB 编不了(见上文内存分界)→ brew pin。注意-doc变体会拉 pandoc→GHC→texlive 全家桶(150+ 依赖),4GiB 机器别碰,eza -doc(无 doc)只拉 30 个左右 - lynx:仍无 archive,继续保留在 brew
- go:
sudo port install go(元端口)即建/opt/local/bin/go链接;只装了 go-1.27 的机器缺 select 链接、command -v go为空,补元端口即修(见坑 7)
日常维护:
# 手动一键更新
sudo /opt/local/bin/port selfupdate && sudo /opt/local/bin/port upgrade outdated # MacPorts
/usr/local/bin/brew update && /usr/local/bin/brew upgrade && /usr/local/bin/brew cleanup # Homebrew
uv self update # uv(语言工具链,无需 sudo)
~/.local/bin/npm update -g # Node 全局包(用户级,免 sudo)
要全自动,挂 crontab 定时(周一三五早 5 点错峰;sudo port 依赖前文 NOPASSWD 配置;crontab 环境 PATH 不含 /opt/local/bin 与 ~/.local/bin,必须写全路径):
9 5 * * 1,3,5 /usr/local/bin/brew upgrade >> /tmp/brew_upgrade.log 2>&1
11 5 * * 1,3,5 sudo /opt/local/bin/port selfupdate && sudo /opt/local/bin/port upgrade outdated >> /tmp/port_upgrade.log 2>&1
13 5 * * * $HOME/.local/bin/uv self update >> /tmp/uv_update.log 2>&1
只想"检查不升级":brew outdated、port outdated;uv 是单文件工具,没有独立 outdated 概念,uv self update 直接就是更新。