♻️ refactor(core): 优化 Homebrew 智能升级管理脚本
* 优化 Shell 环境设置,采用 set -euo pipefail 提升脚本鲁棒性 * 改进路径处理逻辑,动态按需向 PATH 注入 Homebrew 常用路径 * 增强失败隔离机制,实现 Cask 批量失败后仅对过期项精准重试 * 优化临时文件管理,改用临时目录并在退出时自动清理相关资源 * 完善网络容错能力,在 bootstrap 阶段增加 curl 重试与超时限制 * 更新文档说明,同步最新的升级策略、并发控制及错误处理逻辑 * 引入终端动态宽度支持,优化非交互式环境下的输出展示效果
This commit is contained in:
+66
-28
@@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
- 执行 `brew update -v` 更新 Homebrew 仓库。
|
- 执行 `brew update -v` 更新 Homebrew 仓库。
|
||||||
- 执行 `brew doctor` 做健康检查;发现问题时给出警告,但不中断后续流程。
|
- 执行 `brew doctor` 做健康检查;发现问题时给出警告,但不中断后续流程。
|
||||||
- 使用 `brew upgrade --formula` 升级命令行工具。
|
- 批量升级 Formula 和 Cask;批量命令失败后,仅对仍然过期的项目逐个重试。
|
||||||
- 使用 `brew upgrade --cask --greedy --force` 批量升级 GUI 应用。
|
- Formula 与 Cask 阶段相互隔离,一个包失败不会阻止另一类包继续升级。
|
||||||
- Homebrew 更新、Formula 升级和 Cask 升级遇到网络、下载、HTTP 429/5xx 等瞬时错误时,会自动退避重试。
|
- 默认使用 `brew upgrade --cask --greedy`,不强制覆盖已有 App;需要时可显式开启 `--force`。
|
||||||
- 批量 Cask 升级只执行一次,以利用 Homebrew 的批量下载能力;失败后只对仍然过期的 Cask 逐个重试,并在重试时去掉 `--force`,避免重复处理已成功的应用。
|
- 网络、下载、HTTP 429/5xx 等瞬时错误会自动退避重试;权限、root、证书、checksum 和磁盘空间错误不会盲目重试。
|
||||||
- 单个 Cask 的失败不会掩盖其他 Cask 的升级结果;最终会再次核对过期列表,仅在仍有失败项时返回非零状态。
|
- 同一用户只能运行一个主脚本实例,避免并发升级争用 Homebrew 锁和 `/Applications`。
|
||||||
- 执行 `brew cleanup --prune=all` 清理旧版本和缓存。
|
- 升级全部成功后执行常规 `brew cleanup`;存在失败时默认保留缓存,方便下次恢复。
|
||||||
- 支持固定终端宽度,避免非交互环境下输出宽度异常。
|
- 支持 `--width`、环境变量宽度和终端动态宽度。
|
||||||
- 启动器支持通过 macOS Keychain 保存并读取 sudo 密码,用于 `sudo -A -v` 预刷新 sudo 凭据。
|
- 启动器支持通过 macOS Keychain 保存并读取 sudo 密码,用于 `sudo -A -v` 预刷新 sudo 凭据。
|
||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
@@ -45,7 +45,9 @@ curl --version
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
brewup() {
|
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,也可以使用:
|
如果更偏好 alias,也可以使用:
|
||||||
|
|
||||||
```bash
|
```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` 会执行以下操作:
|
`brew-upgrade-manager-bootstrap.sh` 会执行以下操作:
|
||||||
|
|
||||||
1. 创建临时文件。
|
1. 创建权限隔离的临时目录。
|
||||||
2. 生成临时 `SUDO_ASKPASS` 脚本。
|
2. 使用连接超时、低速超时和 curl 重试下载远端主脚本。
|
||||||
3. 从 macOS Keychain 读取 sudo 密码;首次使用时提示输入一次并保存到 Keychain。
|
3. 检查下载结果非空,可选校验 SHA256,并执行 `bash -n` 语法检查。
|
||||||
4. 执行 `sudo -A -v` 刷新 sudo 凭据。
|
4. 生成临时 `SUDO_ASKPASS` 脚本。
|
||||||
5. 下载远端 `brew-upgrade-manager.sh`。
|
5. 从 macOS Keychain 读取 sudo 密码;首次使用时提示输入一次并保存到 Keychain。
|
||||||
6. 可选校验 SHA256。
|
6. 执行 `sudo -A -v` 刷新 sudo 凭据。
|
||||||
7. 使用 `bash "$TEMP" "$@"` 执行主脚本并转发参数。
|
7. 执行主脚本并转发参数。
|
||||||
8. 退出时删除临时脚本文件。
|
8. 正常退出、Ctrl-C 或 TERM 时删除临时目录中的文件。
|
||||||
|
|
||||||
默认 Keychain service 名称为:
|
默认 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
|
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`
|
1. `brew update -v`
|
||||||
2. `brew doctor`
|
2. `brew doctor`
|
||||||
3. `brew upgrade --formula`
|
3. `brew upgrade --formula`;失败时仅重试仍然过期的 Formula
|
||||||
4. `brew upgrade --cask --greedy --force`;失败时对剩余 Cask 逐个执行 `brew upgrade --cask --greedy <cask>`
|
4. `brew upgrade --cask --greedy`;失败时仅重试仍然过期的 Cask
|
||||||
5. `brew cleanup --prune=all`
|
5. 全部成功时执行 `brew cleanup`,否则默认保留缓存
|
||||||
|
6. 汇总失败的 Formula/Cask 并返回最终状态码
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
@@ -220,7 +257,7 @@ HB_TERMINAL_WIDTH=130 brewup
|
|||||||
brew untap buo/cask-upgrade
|
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
|
### `brew doctor` 提示 warning
|
||||||
|
|
||||||
@@ -254,7 +291,7 @@ Warning: 'brew doctor' detected issues. Manual review and resolution are recomme
|
|||||||
|
|
||||||
### Cask 下载曾经报错但最终升级完成
|
### 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 升级命令失败后执行:
|
脚本会在批量 Cask 升级命令失败后执行:
|
||||||
|
|
||||||
@@ -262,13 +299,13 @@ Warning: 'brew doctor' detected issues. Manual review and resolution are recomme
|
|||||||
brew outdated --cask --greedy
|
brew outdated --cask --greedy
|
||||||
```
|
```
|
||||||
|
|
||||||
如果没有剩余过期 Cask,脚本会把前一次错误视为已恢复并继续执行 `brew cleanup --prune=all`。如果仍有过期 Cask,脚本会逐个重试;网络类错误按配置退避重试,其他错误执行一次。最终仍有过期 Cask 时,脚本保留失败退出状态,方便发现真实失败项。
|
如果没有剩余过期 Cask,脚本会把前一次错误视为已恢复。如果仍有过期 Cask,脚本会逐个重试;网络类错误按配置退避重试,其他错误执行一次。最终仍有过期项时,脚本返回失败状态并默认保留缓存。
|
||||||
|
|
||||||
### Cask 报 `Running Homebrew as root`
|
### Cask 报 `Running Homebrew as root`
|
||||||
|
|
||||||
某些 macOS/Homebrew 组合在覆盖现有 App、复制扩展属性时,会从 Cask 内部通过 `sudo` 调用 `brew ruby`,随后被 Homebrew 自身的 root 安全检查拒绝。这不代表整个 `brewup` 是通过 `sudo brew` 启动的。
|
某些 macOS/Homebrew 组合在覆盖现有 App、复制扩展属性时,会从 Cask 内部通过 `sudo` 调用 `brew ruby`,随后被 Homebrew 自身的 root 安全检查拒绝。这不代表整个 `brewup` 是通过 `sudo brew` 启动的。
|
||||||
|
|
||||||
新版脚本会在批量升级失败后,对仍然过期的 Cask 去掉 `--force` 单独重试。例如:
|
新版脚本默认不再使用 `--force`,并在批量升级失败后对仍然过期的 Cask 单独重试。例如:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew upgrade --cask --greedy visual-studio-code
|
brew upgrade --cask --greedy visual-studio-code
|
||||||
@@ -278,7 +315,8 @@ brew upgrade --cask --greedy visual-studio-code
|
|||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 脚本启用了 `set -e` 和 `set -o pipefail`,关键命令失败会终止流程。
|
- 脚本启用了 `set -euo pipefail`,但 Formula/Cask 升级阶段会捕获错误、继续隔离其他失败项,并在最后统一返回状态。
|
||||||
- `brew upgrade --cask --greedy --force` 可能升级或替换已安装 GUI 应用,建议先保存重要工作。
|
- `brew upgrade --cask --greedy` 可能退出正在运行的 GUI 应用,建议先保存重要工作。
|
||||||
|
- 不建议日常启用 `HB_CASK_FORCE=1`;它会允许 Homebrew 覆盖已有 Cask 文件。
|
||||||
- 远程启动器属于“下载后执行”模式,只应从可信仓库使用。
|
- 远程启动器属于“下载后执行”模式,只应从可信仓库使用。
|
||||||
- 在公司设备或受管 macOS 上运行前,先确认 Homebrew、Cask、Keychain 和 sudo 策略允许自动升级。
|
- 在公司设备或受管 macOS 上运行前,先确认 Homebrew、Cask、Keychain 和 sudo 策略允许自动升级。
|
||||||
|
|||||||
@@ -3,61 +3,56 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REMOTE="https://git.orionc.me/orion/script/raw/branch/main/homebrew/brew-upgrade-manager.sh"
|
REMOTE="https://git.orionc.me/orion/script/raw/branch/main/homebrew/brew-upgrade-manager.sh"
|
||||||
TEMP="$(mktemp "${TMPDIR:-/tmp}/brew-upgrade-manager.XXXXXX.sh")"
|
TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/brew-upgrade-manager.XXXXXX")"
|
||||||
|
TEMP="$TEMP_DIR/brew-upgrade-manager.sh"
|
||||||
|
ASKPASS_TEMP="$TEMP_DIR/brewup-askpass.sh"
|
||||||
KEYCHAIN_SERVICE="${BREWUP_KEYCHAIN_SERVICE:-brewup-sudo-password}"
|
KEYCHAIN_SERVICE="${BREWUP_KEYCHAIN_SERVICE:-brewup-sudo-password}"
|
||||||
ASKPASS_TEMP="$(mktemp "${TMPDIR:-/tmp}/brewup-askpass.XXXXXX.sh")"
|
CURRENT_USER="${USER:-$(id -un)}"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -f "$TEMP" "$ASKPASS_TEMP"
|
rm -f "$TEMP" "$ASKPASS_TEMP"
|
||||||
|
rmdir "$TEMP_DIR" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
on_interrupt() {
|
||||||
|
exit 130
|
||||||
|
}
|
||||||
|
|
||||||
|
on_terminate() {
|
||||||
|
exit 143
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap on_interrupt INT
|
||||||
|
trap on_terminate TERM
|
||||||
|
|
||||||
PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
|
PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||||
export PATH
|
export PATH
|
||||||
|
|
||||||
setup_sudo_askpass() {
|
|
||||||
cat > "$ASKPASS_TEMP" <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
exec /usr/bin/security find-generic-password -a "${USER:-$(id -un)}" -s "${BREWUP_KEYCHAIN_SERVICE:-brewup-sudo-password}" -w
|
|
||||||
EOF
|
|
||||||
chmod 700 "$ASKPASS_TEMP"
|
|
||||||
export SUDO_ASKPASS="$ASKPASS_TEMP"
|
|
||||||
export BREWUP_KEYCHAIN_SERVICE="$KEYCHAIN_SERVICE"
|
|
||||||
|
|
||||||
if ! /usr/bin/security find-generic-password -a "$USER" -s "$KEYCHAIN_SERVICE" -w >/dev/null 2>&1; then
|
|
||||||
printf "首次使用:请输入一次 sudo 密码,将保存到 macOS Keychain:"
|
|
||||||
IFS= read -r -s BREWUP_SUDO_PASSWORD
|
|
||||||
printf "\n"
|
|
||||||
/usr/bin/security add-generic-password -U -a "$USER" -s "$KEYCHAIN_SERVICE" -w "$BREWUP_SUDO_PASSWORD" >/dev/null
|
|
||||||
unset BREWUP_SUDO_PASSWORD
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "正在通过 Keychain 准备 sudo 凭据..."
|
|
||||||
if ! sudo -A -v; then
|
|
||||||
echo "Keychain 中的 sudo 密码不可用,请删除后重新保存:" >&2
|
|
||||||
echo " security delete-generic-password -a \"$USER\" -s \"$KEYCHAIN_SERVICE\"" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_sudo_askpass
|
|
||||||
|
|
||||||
echo "正在下载远程脚本..."
|
|
||||||
download_retries="${BREWUP_DOWNLOAD_RETRIES:-3}"
|
download_retries="${BREWUP_DOWNLOAD_RETRIES:-3}"
|
||||||
download_retry_delay="${BREWUP_DOWNLOAD_RETRY_DELAY_SECONDS:-2}"
|
download_retry_delay="${BREWUP_DOWNLOAD_RETRY_DELAY_SECONDS:-2}"
|
||||||
|
connect_timeout="${BREWUP_CONNECT_TIMEOUT_SECONDS:-15}"
|
||||||
|
speed_time="${BREWUP_LOW_SPEED_TIME_SECONDS:-30}"
|
||||||
[[ "$download_retries" =~ ^[1-9][0-9]*$ ]] || download_retries=3
|
[[ "$download_retries" =~ ^[1-9][0-9]*$ ]] || download_retries=3
|
||||||
[[ "$download_retry_delay" =~ ^[0-9]+$ ]] || download_retry_delay=2
|
[[ "$download_retry_delay" =~ ^[0-9]+$ ]] || download_retry_delay=2
|
||||||
|
[[ "$connect_timeout" =~ ^[1-9][0-9]*$ ]] || connect_timeout=15
|
||||||
|
[[ "$speed_time" =~ ^[1-9][0-9]*$ ]] || speed_time=30
|
||||||
|
|
||||||
curl_retry_all_errors=()
|
curl_retry_all_errors=()
|
||||||
if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then
|
if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then
|
||||||
curl_retry_all_errors+=(--retry-all-errors)
|
curl_retry_all_errors+=(--retry-all-errors)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "正在下载远程脚本..."
|
||||||
curl -f -sSL \
|
curl -f -sSL \
|
||||||
|
--connect-timeout "$connect_timeout" \
|
||||||
|
--speed-limit 1024 \
|
||||||
|
--speed-time "$speed_time" \
|
||||||
--retry "$download_retries" \
|
--retry "$download_retries" \
|
||||||
--retry-delay "$download_retry_delay" \
|
--retry-delay "$download_retry_delay" \
|
||||||
"${curl_retry_all_errors[@]}" \
|
"${curl_retry_all_errors[@]}" \
|
||||||
"$REMOTE" -o "$TEMP"
|
"$REMOTE" -o "$TEMP"
|
||||||
chmod 600 "$TEMP"
|
[[ -s "$TEMP" ]] || { echo "远程脚本为空,已停止执行。" >&2; exit 1; }
|
||||||
|
|
||||||
if [[ -n "${BREWUP_SHA256:-}" ]]; then
|
if [[ -n "${BREWUP_SHA256:-}" ]]; then
|
||||||
echo "正在校验脚本 SHA256..."
|
echo "正在校验脚本 SHA256..."
|
||||||
@@ -71,11 +66,42 @@ if [[ -n "${BREWUP_SHA256:-}" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
bash -n "$TEMP"
|
||||||
|
|
||||||
if [[ "${BREWUP_DEBUG:-}" == "1" ]]; then
|
if [[ "${BREWUP_DEBUG:-0}" == "1" ]]; then
|
||||||
echo "Downloaded script first line:"
|
echo "Downloaded script first line:"
|
||||||
head -n 1 "$TEMP"
|
head -n 1 "$TEMP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export COLUMNS="130"
|
setup_sudo_askpass() {
|
||||||
|
cat > "$ASKPASS_TEMP" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exec /usr/bin/security find-generic-password \
|
||||||
|
-a "${BREWUP_KEYCHAIN_ACCOUNT:-${USER:-$(id -un)}}" \
|
||||||
|
-s "${BREWUP_KEYCHAIN_SERVICE:-brewup-sudo-password}" -w
|
||||||
|
EOF
|
||||||
|
chmod 700 "$ASKPASS_TEMP"
|
||||||
|
export SUDO_ASKPASS="$ASKPASS_TEMP"
|
||||||
|
export BREWUP_KEYCHAIN_SERVICE="$KEYCHAIN_SERVICE"
|
||||||
|
export BREWUP_KEYCHAIN_ACCOUNT="$CURRENT_USER"
|
||||||
|
|
||||||
|
if ! /usr/bin/security find-generic-password -a "$CURRENT_USER" -s "$KEYCHAIN_SERVICE" -w >/dev/null 2>&1; then
|
||||||
|
printf "首次使用:请输入一次 sudo 密码,将保存到 macOS Keychain:"
|
||||||
|
IFS= read -r -s BREWUP_SUDO_PASSWORD
|
||||||
|
printf '\n'
|
||||||
|
/usr/bin/security add-generic-password -U -a "$CURRENT_USER" -s "$KEYCHAIN_SERVICE" -w "$BREWUP_SUDO_PASSWORD" >/dev/null
|
||||||
|
unset BREWUP_SUDO_PASSWORD
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "正在通过 Keychain 准备 sudo 凭据..."
|
||||||
|
if ! sudo -A -v; then
|
||||||
|
echo "Keychain 中的 sudo 密码不可用,请删除后重新保存:" >&2
|
||||||
|
echo " security delete-generic-password -a \"$CURRENT_USER\" -s \"$KEYCHAIN_SERVICE\"" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_sudo_askpass
|
||||||
|
|
||||||
|
export COLUMNS="${COLUMNS:-130}"
|
||||||
bash "$TEMP" "$@"
|
bash "$TEMP" "$@"
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Homebrew 智能升级脚本(Cask 失败隔离与重试版)
|
# Homebrew 智能升级脚本(失败隔离、选择性重试与性能优化版)
|
||||||
|
|
||||||
# ================== 脚本环境设置 ==================
|
set -euo pipefail
|
||||||
|
|
||||||
# set -e:当命令返回非零退出状态(表示失败)时,脚本会立即退出。
|
|
||||||
set -e
|
|
||||||
# set -o pipefail:在管道命令中,如果任何一个子命令失败,整个管道即为失败。
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
prepend_path_once() {
|
prepend_path_once() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
@@ -21,13 +16,13 @@ prepend_path_once "/usr/local/sbin"
|
|||||||
prepend_path_once "/opt/homebrew/sbin"
|
prepend_path_once "/opt/homebrew/sbin"
|
||||||
export PATH
|
export PATH
|
||||||
|
|
||||||
# --- 颜色定义 (自动检测终端是否支持) ---
|
if [[ -t 1 ]]; then
|
||||||
if [ -t 1 ]; then
|
|
||||||
GREEN='\033[1;32m'
|
GREEN='\033[1;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
BLUE='\033[1;34m'
|
BLUE='\033[1;34m'
|
||||||
CYAN='\033[1;36m'
|
CYAN='\033[1;36m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
export HOMEBREW_COLOR=1
|
||||||
else
|
else
|
||||||
GREEN=''
|
GREEN=''
|
||||||
YELLOW=''
|
YELLOW=''
|
||||||
@@ -36,35 +31,165 @@ else
|
|||||||
NC=''
|
NC=''
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 终端宽度和打印函数 ---
|
TERMINAL_WIDTH=""
|
||||||
TERMINAL_WIDTH="130"
|
ACTIVE_LOG_FILE=""
|
||||||
|
LOCK_ACQUIRED=0
|
||||||
|
LOCK_DIR="${HB_LOCK_DIR:-${TMPDIR:-/tmp}/brewup-$(id -u).lock}"
|
||||||
|
FAILED_FORMULAE=()
|
||||||
|
FAILED_CASKS=()
|
||||||
|
|
||||||
terminal_width() {
|
usage() {
|
||||||
printf "%s" "$TERMINAL_WIDTH"
|
cat <<'EOF'
|
||||||
|
Usage: brew-upgrade-manager.sh [--width N]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--width N, --width=N Set output width (40-500).
|
||||||
|
-h, --help Show this help.
|
||||||
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
separator() { local width; width=$(terminal_width); printf '=%.0s' $(seq 1 "$width"); printf "\n"; }
|
parse_args() {
|
||||||
print_header() { echo -e "${BLUE}$1${NC}"; }
|
while (($# > 0)); do
|
||||||
|
case "$1" in
|
||||||
|
--width)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Error: --width requires a value." >&2; exit 2; }
|
||||||
|
TERMINAL_WIDTH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--width=*)
|
||||||
|
TERMINAL_WIDTH="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown argument: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
retry_delay_seconds() {
|
resolve_terminal_width() {
|
||||||
local delay="${HB_RETRY_DELAY_SECONDS:-5}"
|
if [[ -z "$TERMINAL_WIDTH" ]]; then
|
||||||
[[ "$delay" =~ ^[0-9]+$ ]] || delay=5
|
TERMINAL_WIDTH="${HB_TERMINAL_WIDTH:-}"
|
||||||
printf '%s' "$delay"
|
fi
|
||||||
|
if [[ -z "$TERMINAL_WIDTH" ]] && [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
|
||||||
|
TERMINAL_WIDTH="$(tput cols 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
[[ -n "$TERMINAL_WIDTH" ]] || TERMINAL_WIDTH=130
|
||||||
|
|
||||||
|
if [[ ! "$TERMINAL_WIDTH" =~ ^[0-9]+$ ]] || ((TERMINAL_WIDTH < 40 || TERMINAL_WIDTH > 500)); then
|
||||||
|
echo "Error: terminal width must be an integer between 40 and 500." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
export COLUMNS="$TERMINAL_WIDTH"
|
||||||
|
}
|
||||||
|
|
||||||
|
separator() {
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < TERMINAL_WIDTH; i++)); do
|
||||||
|
printf '='
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
print_header() {
|
||||||
|
echo -e "${BLUE}$1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_enabled() {
|
||||||
|
case "${1:-}" in
|
||||||
|
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# shellcheck disable=SC2329 # Invoked by EXIT trap.
|
||||||
|
cleanup_runtime() {
|
||||||
|
if [[ -n "${ACTIVE_LOG_FILE:-}" && -f "$ACTIVE_LOG_FILE" ]]; then
|
||||||
|
rm -f "$ACTIVE_LOG_FILE"
|
||||||
|
fi
|
||||||
|
if ((LOCK_ACQUIRED)); then
|
||||||
|
rm -f "$LOCK_DIR/pid"
|
||||||
|
rmdir "$LOCK_DIR" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# shellcheck disable=SC2329 # Invoked by INT trap.
|
||||||
|
on_interrupt() {
|
||||||
|
exit 130
|
||||||
|
}
|
||||||
|
|
||||||
|
# shellcheck disable=SC2329 # Invoked by TERM trap.
|
||||||
|
on_terminate() {
|
||||||
|
exit 143
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup_runtime EXIT
|
||||||
|
trap on_interrupt INT
|
||||||
|
trap on_terminate TERM
|
||||||
|
|
||||||
|
acquire_lock() {
|
||||||
|
local existing_pid=""
|
||||||
|
|
||||||
|
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||||
|
LOCK_ACQUIRED=1
|
||||||
|
printf '%s\n' "$$" > "$LOCK_DIR/pid"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -r "$LOCK_DIR/pid" ]]; then
|
||||||
|
IFS= read -r existing_pid < "$LOCK_DIR/pid" || true
|
||||||
|
fi
|
||||||
|
if [[ "$existing_pid" =~ ^[0-9]+$ ]] && kill -0 "$existing_pid" 2>/dev/null; then
|
||||||
|
echo "Error: another brewup process is already running (PID ${existing_pid})." >&2
|
||||||
|
exit 75
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$LOCK_DIR/pid" 2>/dev/null || true
|
||||||
|
rmdir "$LOCK_DIR" 2>/dev/null || {
|
||||||
|
echo "Error: unable to remove stale lock directory: $LOCK_DIR" >&2
|
||||||
|
exit 75
|
||||||
|
}
|
||||||
|
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||||
|
echo "Error: another brewup process acquired the lock." >&2
|
||||||
|
exit 75
|
||||||
|
fi
|
||||||
|
LOCK_ACQUIRED=1
|
||||||
|
printf '%s\n' "$$" > "$LOCK_DIR/pid"
|
||||||
}
|
}
|
||||||
|
|
||||||
retry_attempts() {
|
retry_attempts() {
|
||||||
local attempts="${HB_RETRY_ATTEMPTS:-3}"
|
local attempts="${HB_RETRY_ATTEMPTS:-3}"
|
||||||
[[ "$attempts" =~ ^[1-9][0-9]*$ ]] || attempts=3
|
if [[ ! "$attempts" =~ ^[1-9][0-9]*$ ]] || ((attempts > 10)); then
|
||||||
|
attempts=3
|
||||||
|
fi
|
||||||
printf '%s' "$attempts"
|
printf '%s' "$attempts"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retry_delay_seconds() {
|
||||||
|
local delay="${HB_RETRY_DELAY_SECONDS:-5}"
|
||||||
|
if [[ ! "$delay" =~ ^[0-9]+$ ]] || ((delay > 300)); then
|
||||||
|
delay=5
|
||||||
|
fi
|
||||||
|
printf '%s' "$delay"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_non_retryable_brew_error() {
|
||||||
|
local log_file="$1"
|
||||||
|
grep -Eiq \
|
||||||
|
'Running Homebrew as root|Permission denied|Operation not permitted|No space left on device|checksum mismatch|SHA-?256 mismatch|certificate verify failed|SSL certificate problem|curl: \((3|23|26|60)\)|unknown (option|argument)|invalid (option|argument)' \
|
||||||
|
"$log_file"
|
||||||
|
}
|
||||||
|
|
||||||
is_retryable_brew_error() {
|
is_retryable_brew_error() {
|
||||||
local log_file="$1"
|
local log_file="$1"
|
||||||
|
|
||||||
# 网络、下载和服务端短暂错误适合重试;权限、root、签名和配置错误
|
|
||||||
# 通常是确定性失败,重试只会浪费时间并重复修改应用。
|
|
||||||
grep -Eiq \
|
grep -Eiq \
|
||||||
'curl: \([0-9]+\)|Transferred a partial file|Could not resolve host|timed out|timeout|connection (reset|refused|closed)|network|HTTP (5[0-9]{2}|429)|temporarily unavailable|failed to download|download.*failed' \
|
'curl: \((5|6|7|18|28|35|52|55|56|92)\)|Transferred a partial file|Could not resolve host|timed out|timeout|connection (reset|refused|closed)|HTTP[^0-9]*(429|5[0-9]{2})|returned error: (429|5[0-9]{2})|temporarily unavailable|failed to download|download.*failed|RPC failed|early EOF|remote end hung up|unexpected disconnect' \
|
||||||
"$log_file"
|
"$log_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,135 +197,253 @@ run_brew_with_retries() {
|
|||||||
local label="$1"
|
local label="$1"
|
||||||
shift
|
shift
|
||||||
|
|
||||||
local attempts delay attempt status log_file
|
local attempts delay attempt status next_delay
|
||||||
|
local pipeline_status=()
|
||||||
attempts="$(retry_attempts)"
|
attempts="$(retry_attempts)"
|
||||||
delay="$(retry_delay_seconds)"
|
delay="$(retry_delay_seconds)"
|
||||||
log_file="$(mktemp "${TMPDIR:-/tmp}/brewup-retry.XXXXXX.log")"
|
ACTIVE_LOG_FILE="$(mktemp "${TMPDIR:-/tmp}/brewup-retry.XXXXXX.log")"
|
||||||
|
|
||||||
for ((attempt = 1; attempt <= attempts; attempt++)); do
|
for ((attempt = 1; attempt <= attempts; attempt++)); do
|
||||||
if ((attempt > 1)); then
|
if ((attempt > 1)); then
|
||||||
echo -e "${YELLOW}Retrying ${label} (${attempt}/${attempts}) after ${delay}s...${NC}"
|
echo -e "${YELLOW}Retrying ${label} (${attempt}/${attempts}) after ${delay}s...${NC}"
|
||||||
sleep "$delay"
|
sleep "$delay"
|
||||||
delay=$((delay * 2))
|
next_delay=$((delay * 2))
|
||||||
|
((next_delay > 300)) && next_delay=300
|
||||||
|
delay="$next_delay"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# stdout 直接输出,避免正常升级时把大量进度信息落盘;只复制通常包含
|
: > "$ACTIVE_LOG_FILE"
|
||||||
# 错误详情的 stderr,用于失败分类,同时保持实时终端输出。
|
if brew "$@" 2>&1 | tee "$ACTIVE_LOG_FILE"; then
|
||||||
: > "$log_file"
|
status=0
|
||||||
if brew "$@" 2> >(tee "$log_file" >&2); then
|
|
||||||
rm -f "$log_file"
|
|
||||||
return 0
|
|
||||||
else
|
else
|
||||||
status=$?
|
pipeline_status=("${PIPESTATUS[@]}")
|
||||||
|
status="${pipeline_status[0]}"
|
||||||
|
if ((${pipeline_status[1]:-0} != 0)); then
|
||||||
|
echo "Error: failed to write retry log." >&2
|
||||||
|
status="${pipeline_status[1]}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ((attempt == attempts)) || ! is_retryable_brew_error "$log_file"; then
|
if ((status == 0)); then
|
||||||
rm -f "$log_file"
|
rm -f "$ACTIVE_LOG_FILE"
|
||||||
|
ACTIVE_LOG_FILE=""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_non_retryable_brew_error "$ACTIVE_LOG_FILE"; then
|
||||||
|
rm -f "$ACTIVE_LOG_FILE"
|
||||||
|
ACTIVE_LOG_FILE=""
|
||||||
|
return "$status"
|
||||||
|
fi
|
||||||
|
if ((attempt == attempts)) || ! is_retryable_brew_error "$ACTIVE_LOG_FILE"; then
|
||||||
|
rm -f "$ACTIVE_LOG_FILE"
|
||||||
|
ACTIVE_LOG_FILE=""
|
||||||
return "$status"
|
return "$status"
|
||||||
fi
|
fi
|
||||||
echo -e "${YELLOW}${label} failed with a retryable error.${NC}"
|
echo -e "${YELLOW}${label} failed with a retryable error.${NC}"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_outdated_formulae() {
|
||||||
|
brew outdated --formula 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
get_outdated_casks() {
|
||||||
|
brew outdated --cask --greedy 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
run_formula_upgrade() {
|
||||||
|
local bulk_status=0 outdated_formulae remaining_formulae formula
|
||||||
|
local failed_formulae=()
|
||||||
|
|
||||||
|
brew upgrade --formula || bulk_status=$?
|
||||||
|
if ((bulk_status == 0)); then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Warning: Formula upgrade returned ${bulk_status}; checking remaining Formulae...${NC}"
|
||||||
|
if ! outdated_formulae="$(get_outdated_formulae)"; then
|
||||||
|
echo -e "${YELLOW}Warning: unable to verify outdated Formulae.${NC}"
|
||||||
|
return "$bulk_status"
|
||||||
|
fi
|
||||||
|
if [[ -z "$outdated_formulae" ]]; then
|
||||||
|
echo -e "${YELLOW}No outdated Formulae remain; treating the bulk error as recovered.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$outdated_formulae"
|
||||||
|
while IFS= read -r formula; do
|
||||||
|
[[ -n "$formula" ]] || continue
|
||||||
|
echo -e "${CYAN}Retrying Formula: ${formula}${NC}"
|
||||||
|
if run_brew_with_retries "Formula ${formula}" upgrade --formula "$formula"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
failed_formulae+=("$formula")
|
||||||
|
fi
|
||||||
|
done <<< "$outdated_formulae"
|
||||||
|
|
||||||
|
if ! remaining_formulae="$(get_outdated_formulae)"; then
|
||||||
|
echo -e "${YELLOW}Warning: unable to verify Formulae after individual retries.${NC}"
|
||||||
|
FAILED_FORMULAE=("${failed_formulae[@]}")
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$remaining_formulae" ]]; then
|
||||||
|
echo -e "${GREEN}All outdated Formulae were upgraded after isolated retries.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r formula; do
|
||||||
|
[[ -n "$formula" ]] && FAILED_FORMULAE+=("$formula")
|
||||||
|
done <<< "$remaining_formulae"
|
||||||
|
echo -e "${YELLOW}Formulae still outdated after retries:${NC}"
|
||||||
|
printf '%s\n' "$remaining_formulae"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
run_cask_upgrade() {
|
run_cask_upgrade() {
|
||||||
local status=0 outdated_casks remaining_casks cask
|
local bulk_status=0 outdated_casks remaining_casks cask
|
||||||
|
local bulk_args=(upgrade --cask --greedy)
|
||||||
local failed_casks=()
|
local failed_casks=()
|
||||||
|
|
||||||
# 批量阶段只执行一次,让 Homebrew 并发获取下载;失败后只处理仍然过期
|
if is_enabled "${HB_CASK_FORCE:-0}"; then
|
||||||
# 的 Cask,避免重跑整个批次和重复检查已经升级成功的应用。
|
bulk_args+=(--force)
|
||||||
brew upgrade --cask --greedy --force || status=$?
|
echo -e "${YELLOW}HB_CASK_FORCE is enabled; existing Cask files may be overwritten.${NC}"
|
||||||
if [[ "$status" -eq 0 ]]; then
|
fi
|
||||||
|
|
||||||
|
brew "${bulk_args[@]}" || bulk_status=$?
|
||||||
|
if ((bulk_status == 0)); then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${YELLOW}Warning: Cask upgrade reported a non-zero exit code (${status}). Verifying whether outdated casks remain...${NC}"
|
echo -e "${YELLOW}Warning: Cask upgrade returned ${bulk_status}; checking remaining Casks...${NC}"
|
||||||
|
if ! outdated_casks="$(get_outdated_casks)"; then
|
||||||
if ! outdated_casks="$(brew outdated --cask --greedy 2>/dev/null)"; then
|
echo -e "${YELLOW}Warning: unable to verify outdated Casks.${NC}"
|
||||||
echo -e "${YELLOW}Warning: Unable to verify outdated casks after the failed upgrade command.${NC}"
|
return "$bulk_status"
|
||||||
return "$status"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$outdated_casks" ]]; then
|
if [[ -z "$outdated_casks" ]]; then
|
||||||
echo -e "${YELLOW}Warning: No outdated casks remain. Treating the previous Cask error as transient and continuing.${NC}"
|
echo -e "${YELLOW}No outdated Casks remain; treating the bulk error as recovered.${NC}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${YELLOW}The following casks still appear to be outdated; retrying individually:${NC}"
|
|
||||||
printf '%s\n' "$outdated_casks"
|
printf '%s\n' "$outdated_casks"
|
||||||
|
|
||||||
while IFS= read -r cask; do
|
while IFS= read -r cask; do
|
||||||
[[ -n "$cask" ]] || continue
|
[[ -n "$cask" ]] || continue
|
||||||
echo -e "${CYAN}Retrying Cask: ${cask} (without --force)${NC}"
|
echo -e "${CYAN}Retrying Cask: ${cask} (without --force)${NC}"
|
||||||
if ! run_brew_with_retries "Cask ${cask}" upgrade --cask --greedy "$cask"; then
|
if run_brew_with_retries "Cask ${cask}" upgrade --cask --greedy "$cask"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
failed_casks+=("$cask")
|
failed_casks+=("$cask")
|
||||||
fi
|
fi
|
||||||
done <<< "$outdated_casks"
|
done <<< "$outdated_casks"
|
||||||
|
|
||||||
if ! remaining_casks="$(brew outdated --cask --greedy 2>/dev/null)"; then
|
if ! remaining_casks="$(get_outdated_casks)"; then
|
||||||
echo -e "${YELLOW}Warning: Unable to verify outdated Casks after individual retries.${NC}"
|
echo -e "${YELLOW}Warning: unable to verify Casks after individual retries.${NC}"
|
||||||
return "$status"
|
FAILED_CASKS=("${failed_casks[@]}")
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$remaining_casks" ]]; then
|
if [[ -z "$remaining_casks" ]]; then
|
||||||
echo -e "${GREEN}All outdated Casks were upgraded after retrying individually.${NC}"
|
echo -e "${GREEN}All outdated Casks were upgraded after isolated retries.${NC}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${YELLOW}The following Casks remain outdated after retries:${NC}"
|
while IFS= read -r cask; do
|
||||||
|
[[ -n "$cask" ]] && FAILED_CASKS+=("$cask")
|
||||||
|
done <<< "$remaining_casks"
|
||||||
|
echo -e "${YELLOW}Casks still outdated after retries:${NC}"
|
||||||
printf '%s\n' "$remaining_casks"
|
printf '%s\n' "$remaining_casks"
|
||||||
if ((${#failed_casks[@]} > 0)); then
|
return 1
|
||||||
echo -e "${YELLOW}These Casks failed during individual retries: ${failed_casks[*]}${NC}"
|
|
||||||
fi
|
|
||||||
return "$status"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ================== 流程开始 ==================
|
run_cleanup() {
|
||||||
separator
|
local cleanup_days="${HB_CLEANUP_DAYS:-}"
|
||||||
|
|
||||||
|
if is_enabled "${HB_SKIP_CLEANUP:-0}"; then
|
||||||
|
echo -e "${YELLOW}Cleanup skipped because HB_SKIP_CLEANUP is enabled.${NC}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if is_enabled "${HB_CLEANUP_ALL:-0}"; then
|
||||||
|
brew cleanup --prune=all
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -n "$cleanup_days" ]]; then
|
||||||
|
if [[ ! "$cleanup_days" =~ ^[0-9]+$ ]] || ((cleanup_days > 3650)); then
|
||||||
|
echo "Error: HB_CLEANUP_DAYS must be an integer between 0 and 3650." >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
brew cleanup --prune="$cleanup_days"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
brew cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
local overall_status="$1"
|
||||||
|
|
||||||
|
separator
|
||||||
|
if ((overall_status == 0)); then
|
||||||
|
echo -e "${GREEN}All operations completed successfully.${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Homebrew upgrade completed with unresolved failures.${NC}"
|
||||||
|
if ((${#FAILED_FORMULAE[@]} > 0)); then
|
||||||
|
printf 'Failed Formulae: %s\n' "${FAILED_FORMULAE[*]}"
|
||||||
|
fi
|
||||||
|
if ((${#FAILED_CASKS[@]} > 0)); then
|
||||||
|
printf 'Failed Casks: %s\n' "${FAILED_CASKS[*]}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
separator
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args "$@"
|
||||||
|
resolve_terminal_width
|
||||||
|
|
||||||
|
command -v brew >/dev/null 2>&1 || { echo "Error: Homebrew is not installed or not in PATH." >&2; exit 127; }
|
||||||
|
command -v tee >/dev/null 2>&1 || { echo "Error: tee is required." >&2; exit 127; }
|
||||||
|
acquire_lock
|
||||||
|
|
||||||
|
separator
|
||||||
print_header "Step 1: Updating Homebrew repositories (brew update -v)"
|
print_header "Step 1: Updating Homebrew repositories (brew update -v)"
|
||||||
run_brew_with_retries "Homebrew repository update" update -v
|
run_brew_with_retries "Homebrew repository update" update -v
|
||||||
separator
|
separator
|
||||||
printf "\n"
|
printf '\n'
|
||||||
|
|
||||||
separator
|
separator
|
||||||
print_header "Step 2: Performing health check (brew doctor)"
|
print_header "Step 2: Performing health check (brew doctor)"
|
||||||
if ! brew doctor; then
|
if is_enabled "${HB_SKIP_DOCTOR:-0}"; then
|
||||||
echo -e "${YELLOW}Warning: 'brew doctor' detected issues. Manual review and resolution are recommended.${NC}"
|
echo -e "${YELLOW}brew doctor skipped because HB_SKIP_DOCTOR is enabled.${NC}"
|
||||||
|
elif ! brew doctor; then
|
||||||
|
echo -e "${YELLOW}Warning: 'brew doctor' detected issues. Manual review is recommended.${NC}"
|
||||||
else
|
else
|
||||||
echo "Homebrew environment is in good health."
|
echo "Homebrew environment is in good health."
|
||||||
fi
|
fi
|
||||||
separator
|
separator
|
||||||
printf "\n"
|
printf '\n'
|
||||||
|
|
||||||
separator
|
separator
|
||||||
print_header "Step 3: Executing comprehensive upgrades (Formulae & Casks)"
|
print_header "Step 3: Upgrading Formulae and Casks with failure isolation"
|
||||||
echo -e "${NC}"
|
overall_status=0
|
||||||
|
|
||||||
# 1. 升级命令行工具 (Formulae)
|
echo -e "\n${CYAN}>>> [1/2] Upgrading CLI Formulae...${NC}"
|
||||||
echo -e "\n${CYAN}>>> [1/2] Upgrading CLI Formulae (brew upgrade --formula)...${NC}"
|
if ! run_formula_upgrade; then
|
||||||
run_brew_with_retries "Formula upgrade" upgrade --formula
|
overall_status=1
|
||||||
|
fi
|
||||||
|
|
||||||
# 2. 升级图形界面软件 (Casks)
|
echo -e "\n${CYAN}>>> [2/2] Upgrading GUI Casks...${NC}"
|
||||||
echo -e "\n${CYAN}>>> [2/2] Upgrading GUI Casks (bulk --force, then isolated retries)...${NC}"
|
if ! run_cask_upgrade; then
|
||||||
|
overall_status=1
|
||||||
# 强制注入环境变量,确保终端输出依然保留 ANSI 颜色格式
|
fi
|
||||||
export HOMEBREW_COLOR=1
|
separator
|
||||||
|
printf '\n'
|
||||||
# --- 终端宽度处理 ---
|
|
||||||
# 固定宽度,避免动态识别终端列数导致 prompt 重绘异常。
|
|
||||||
export COLUMNS
|
|
||||||
COLUMNS="$(terminal_width)"
|
|
||||||
|
|
||||||
run_cask_upgrade
|
|
||||||
|
|
||||||
separator
|
separator
|
||||||
printf "\n"
|
print_header "Step 4: Cleaning up old files and caches"
|
||||||
|
if ((overall_status != 0)) && ! is_enabled "${HB_CLEANUP_ON_FAILURE:-0}"; then
|
||||||
|
echo -e "${YELLOW}Cleanup skipped because upgrades failed; cached downloads are preserved for the next retry.${NC}"
|
||||||
|
elif ! run_cleanup; then
|
||||||
|
overall_status=1
|
||||||
|
fi
|
||||||
separator
|
separator
|
||||||
print_header "Step 4: Cleaning up old files and caches (brew cleanup --prune=all)"
|
printf '\n'
|
||||||
brew cleanup --prune=all
|
|
||||||
separator
|
|
||||||
printf "\n"
|
|
||||||
|
|
||||||
echo -e "${GREEN}All operations completed!${NC}"
|
print_summary "$overall_status"
|
||||||
printf "\n"
|
exit "$overall_status"
|
||||||
|
|||||||
Reference in New Issue
Block a user