笔记 by 车东

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)

🧭 迁移流程(每批)

  1. 建清单:brew list --formula、brew leaves
  2. 逐项探测 archive
  3. sudo port install <port> → 看到 ---> Fetching archive 即走预编译
  4. 验证:port installed active + 跑二进制 --version 冒烟
  5. 卸载 brew 副本:brew uninstall <...> + brew autoremove
  6. 复查 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.tbz2 200 即 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 直接就是更新。