♻️ refactor(core): 优化 Homebrew 智能升级管理脚本

* 优化 Shell 环境设置,采用 set -euo pipefail 提升脚本鲁棒性
* 改进路径处理逻辑,动态按需向 PATH 注入 Homebrew 常用路径
* 增强失败隔离机制,实现 Cask 批量失败后仅对过期项精准重试
* 优化临时文件管理,改用临时目录并在退出时自动清理相关资源
* 完善网络容错能力,在 bootstrap 阶段增加 curl 重试与超时限制
* 更新文档说明,同步最新的升级策略、并发控制及错误处理逻辑
* 引入终端动态宽度支持,优化非交互式环境下的输出展示效果
This commit is contained in:
2026-07-17 00:30:07 +08:00
parent 3e7063b30a
commit ff54bffc53
3 changed files with 461 additions and 154 deletions
+66 -28
View File
@@ -15,13 +15,13 @@
- 执行 `brew update -v` 更新 Homebrew 仓库。
- 执行 `brew doctor` 做健康检查;发现问题时给出警告,但不中断后续流程。
- 使用 `brew upgrade --formula` 升级命令行工具
- 使用 `brew upgrade --cask --greedy --force` 批量升级 GUI 应用
- Homebrew 更新、Formula 升级和 Cask 升级遇到网络、下载、HTTP 429/5xx 等瞬时错误时,会自动退避重试
- 批量 Cask 升级只执行一次,以利用 Homebrew 的批量下载能力;失败后只对仍然过期的 Cask 逐个重试,并在重试时去掉 `--force`,避免重复处理已成功的应用
- 单个 Cask 的失败不会掩盖其他 Cask 的升级结果;最终会再次核对过期列表,仅在仍有失败项时返回非零状态
- 执行 `brew cleanup --prune=all` 清理旧版本和缓存
- 支持固定终端宽度,避免非交互环境下输出宽度异常
- 批量升级 Formula 和 Cask;批量命令失败后,仅对仍然过期的项目逐个重试
- Formula 与 Cask 阶段相互隔离,一个包失败不会阻止另一类包继续升级
- 默认使用 `brew upgrade --cask --greedy`,不强制覆盖已有 App;需要时可显式开启 `--force`
- 网络、下载、HTTP 429/5xx 等瞬时错误会自动退避重试;权限、root、证书、checksum 和磁盘空间错误不会盲目重试
- 同一用户只能运行一个主脚本实例,避免并发升级争用 Homebrew 锁和 `/Applications`
- 升级全部成功后执行常规 `brew cleanup`;存在失败时默认保留缓存,方便下次恢复
- 支持 `--width`、环境变量宽度和终端动态宽度
- 启动器支持通过 macOS Keychain 保存并读取 sudo 密码,用于 `sudo -A -v` 预刷新 sudo 凭据。
## 依赖
@@ -45,7 +45,9 @@ curl --version
```bash
brewup() {
curl -fsSL https://git.orionc.me/orion/script/raw/branch/main/homebrew/brew-upgrade-manager-bootstrap.sh | bash -s -- "$@"
curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 \
https://git.orionc.me/orion/script/raw/branch/main/homebrew/brew-upgrade-manager-bootstrap.sh \
| bash -s -- "$@"
}
```
@@ -70,7 +72,7 @@ brewup --width 160
如果更偏好 alias,也可以使用:
```bash
alias brewup='curl -fsSL https://git.orionc.me/orion/script/raw/branch/main/homebrew/brew-upgrade-manager-bootstrap.sh | bash -s --'
alias brewup='curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 https://git.orionc.me/orion/script/raw/branch/main/homebrew/brew-upgrade-manager-bootstrap.sh | bash -s --'
```
函数版对参数转发更直观,推荐优先使用函数。
@@ -104,14 +106,14 @@ HB_TERMINAL_WIDTH=130 ./brew-upgrade-manager.sh
`brew-upgrade-manager-bootstrap.sh` 会执行以下操作:
1. 创建临时文件
2. 生成临时 `SUDO_ASKPASS` 脚本。
3. 从 macOS Keychain 读取 sudo 密码;首次使用时提示输入一次并保存到 Keychain
4. 执行 `sudo -A -v` 刷新 sudo 凭据
5. 下载远端 `brew-upgrade-manager.sh`
6. 可选校验 SHA256
7. 使用 `bash "$TEMP" "$@"` 执行主脚本并转发参数。
8. 退出时删除临时脚本文件。
1. 创建权限隔离的临时目录
2. 使用连接超时、低速超时和 curl 重试下载远端主脚本。
3. 检查下载结果非空,可选校验 SHA256,并执行 `bash -n` 语法检查
4. 生成临时 `SUDO_ASKPASS` 脚本
5. 从 macOS Keychain 读取 sudo 密码;首次使用时提示输入一次并保存到 Keychain
6. 执行 `sudo -A -v` 刷新 sudo 凭据
7. 执行主脚本并转发参数。
8. 正常退出、Ctrl-C 或 TERM 时删除临时目录中的文件。
默认 Keychain service 名称为:
@@ -169,9 +171,43 @@ HB_RETRY_ATTEMPTS=4 HB_RETRY_DELAY_SECONDS=3 brewup
BREWUP_DOWNLOAD_RETRIES=5 BREWUP_DOWNLOAD_RETRY_DELAY_SECONDS=3 brewup
```
权限不足、以 root 运行 Homebrew、代码签名失败等确定性错误不会盲目重复重试。批量 Cask 升级遇到这类错误后,脚本仍会尝试对剩余 Cask 逐个执行一次,以隔离失败项并避开 `--force`
下载连接和低速超时也可以调整:
为了兼顾性能,正常命令的 stdout 会直接输出,不写入重试日志;脚本只临时记录通常包含错误信息的 stderr。Cask 批量阶段不会整体重试,只有仍然过期的失败项会进入单项重试。单项升级保持串行,因为并行运行多个 Homebrew 写操作会争用 Homebrew 锁和 `/Applications`,通常不会更快,也更容易产生安装冲突。
```bash
BREWUP_CONNECT_TIMEOUT_SECONDS=20 BREWUP_LOW_SPEED_TIME_SECONDS=45 brewup
```
权限不足、以 root 运行 Homebrew、证书校验、checksum、磁盘空间不足等确定性错误不会盲目重复重试。批量升级失败后,脚本仍会对剩余项目逐个执行一次,以隔离真实失败项。
为了兼顾性能,Formula 和 Cask 的批量阶段都只执行一次,只有仍然过期的失败项进入单项重试。单项升级保持串行,因为并行运行多个 Homebrew 写操作会争用 Homebrew 锁和 `/Applications`。用于错误分类的临时日志会在命令结束或收到信号后删除。
## Cask 与清理策略
默认不使用 `--force`。确实需要覆盖已有 Cask 文件时:
```bash
HB_CASK_FORCE=1 brewup
```
升级成功后默认执行常规 `brew cleanup`。可指定缓存保留天数,或明确清空全部缓存:
```bash
HB_CLEANUP_DAYS=30 brewup
HB_CLEANUP_ALL=1 brewup
HB_SKIP_CLEANUP=1 brewup
```
存在升级失败时默认跳过 cleanup,以保留下载缓存。仍希望清理时:
```bash
HB_CLEANUP_ON_FAILURE=1 brewup
```
如果 `brew doctor` 明显影响执行时间,也可以跳过:
```bash
HB_SKIP_DOCTOR=1 brewup
```
## 执行流程
@@ -179,9 +215,10 @@ BREWUP_DOWNLOAD_RETRIES=5 BREWUP_DOWNLOAD_RETRY_DELAY_SECONDS=3 brewup
1. `brew update -v`
2. `brew doctor`
3. `brew upgrade --formula`
4. `brew upgrade --cask --greedy --force`;失败时对剩余 Cask 逐个执行 `brew upgrade --cask --greedy <cask>`
5. `brew cleanup --prune=all`
3. `brew upgrade --formula`;失败时仅重试仍然过期的 Formula
4. `brew upgrade --cask --greedy`;失败时仅重试仍然过期的 Cask
5. 全部成功时执行 `brew cleanup`,否则默认保留缓存
6. 汇总失败的 Formula/Cask 并返回最终状态码
## 常见问题
@@ -220,7 +257,7 @@ HB_TERMINAL_WIDTH=130 brewup
brew untap buo/cask-upgrade
```
GUI 应用升级现在使用 Homebrew 官方的 `brew upgrade --cask --greedy --force`,脚本不再需要 `buo/cask-upgrade`
GUI 应用升级现在使用 Homebrew 官方的 `brew upgrade --cask --greedy`,脚本不再需要 `buo/cask-upgrade`
### `brew doctor` 提示 warning
@@ -254,7 +291,7 @@ Warning: 'brew doctor' detected issues. Manual review and resolution are recomme
### Cask 下载曾经报错但最终升级完成
`brew upgrade --cask --greedy --force` 有时会在下载阶段出现 `curl: (18) Transferred a partial file` 之类的瞬时错误,随后 Homebrew 又重试并完成安装。此时命令仍可能返回非零退出码。
`brew upgrade --cask --greedy` 有时会在下载阶段出现 `curl: (18) Transferred a partial file` 之类的瞬时错误,随后 Homebrew 又完成安装。此时批量命令仍可能返回非零退出码。
脚本会在批量 Cask 升级命令失败后执行:
@@ -262,13 +299,13 @@ Warning: 'brew doctor' detected issues. Manual review and resolution are recomme
brew outdated --cask --greedy
```
如果没有剩余过期 Cask,脚本会把前一次错误视为已恢复并继续执行 `brew cleanup --prune=all`。如果仍有过期 Cask,脚本会逐个重试;网络类错误按配置退避重试,其他错误执行一次。最终仍有过期 Cask 时,脚本保留失败退出状态,方便发现真实失败项
如果没有剩余过期 Cask,脚本会把前一次错误视为已恢复。如果仍有过期 Cask,脚本会逐个重试;网络类错误按配置退避重试,其他错误执行一次。最终仍有过期项时,脚本返回失败状态并默认保留缓存
### Cask 报 `Running Homebrew as root`
某些 macOS/Homebrew 组合在覆盖现有 App、复制扩展属性时,会从 Cask 内部通过 `sudo` 调用 `brew ruby`,随后被 Homebrew 自身的 root 安全检查拒绝。这不代表整个 `brewup` 是通过 `sudo brew` 启动的。
新版脚本在批量升级失败后对仍然过期的 Cask 去掉 `--force` 单独重试。例如:
新版脚本默认不再使用 `--force`,并在批量升级失败后对仍然过期的 Cask 单独重试。例如:
```bash
brew upgrade --cask --greedy visual-studio-code
@@ -278,7 +315,8 @@ brew upgrade --cask --greedy visual-studio-code
## 注意事项
- 脚本启用了 `set -e` 和 `set -o pipefail`关键命令失败会终止流程
- `brew upgrade --cask --greedy --force` 可能升级或替换已安装 GUI 应用,建议先保存重要工作。
- 脚本启用了 `set -euo pipefail`但 Formula/Cask 升级阶段会捕获错误、继续隔离其他失败项,并在最后统一返回状态
- `brew upgrade --cask --greedy` 可能退出正在运行的 GUI 应用,建议先保存重要工作。
- 不建议日常启用 `HB_CASK_FORCE=1`;它会允许 Homebrew 覆盖已有 Cask 文件。
- 远程启动器属于“下载后执行”模式,只应从可信仓库使用。
- 在公司设备或受管 macOS 上运行前,先确认 Homebrew、Cask、Keychain 和 sudo 策略允许自动升级。