笔记 by 车东

Homebrew 停止 Intel 支持:迁移到 MacPorts 的完整笔记

· 系统

2026 年 9 月起,Homebrew 停止提供 Intel (x86_64) 的预编译 bottle。对 Intel Mac 用户意味着:任何没有预编译包的 formula,每次升级都要在本机源码编译。实测 rust 类程序在 8GiB 内存的机器上编译常报 SIGSEGV 栈溢出,llvm 等大依赖一次要编译很久。

替代方案 MacPorts 2.12.6 仍持续发布 x86_64 预编译二进制包。将重编译型软件迁到 MacPorts,brew 只留小工具,是一条可行的路。本文整理多台 Intel Mac 迁移的完整经验。

迁移决策:只迁有预编译 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.tbz2wget-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-1.27(无裸名端口)
httpd apache2
node@22 nodejs22
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 --formulabrew 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 = norevupgrade_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 ]

系统包管理之外的:改用语言官方工具链

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 nvm / fnm 官方安装脚本 按项目切版本,绕过 nodejs port
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 uvwhich node 验一下),别让系统包管理版本抢跑。
  • 与上文的分类:brew/port 管系统级工具,语言官方工具链管语言项目工具,两者职责分开。迁移时少了很多"要不要迁"的纠结——能走官方工具链的直接走官方工具链。

保留在 brew 的类别

类别 例子
MP 无 x86_64 archive 的 rust 程序 eza、glab(rustc 源码编译易栈溢出)
等 MP 官方二进制 MySQL(mysql9 archive 发布后迁)
MP 无对应端口 sdl2-compat、tldr
brew 版本领先 MP cmake(brew 4.4.3 vs MP 3.31.12)

终态检查

  • brew leaves 只剩故意保留的项
  • port installed active 数量显著上升
  • 服务方式变化:brew servicessudo 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
  • 各台保留的 eza/glab/lynx/pkgconf:archive 发布后补迁

日常维护:

# 手动一键更新
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)
npm update -g                                         # Node 全局包(若 node 走 brew)

要全自动,挂 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 outdatedport outdated;uv 是单文件工具,没有独立 outdated 概念,uv self update 直接就是更新。