From 3e7063b30a2e1ab1158a79862eb493a81bb2dfc3 Mon Sep 17 00:00:00 2001 From: Orion Date: Fri, 17 Jul 2026 00:10:37 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(core):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20Homebrew=20=E5=8D=87=E7=BA=A7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8=E7=9A=84=E9=94=99=E8=AF=AF=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E4=B8=8E=E9=9A=94=E7=A6=BB=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 重构 Cask 升级逻辑,引入失败隔离与针对性重试机制 * 新增通过环境变量自定义重试次数与延迟间隔的功能 * 优化启动器下载流程,集成 curl 自动退避重试策略 * 改进 Cask 失败处理,仅对未成功项进行二次重试并移除强制标志 * 更新文档,详细说明网络瞬时错误处理与新版本升级逻辑 * 增强脚本健壮性,完善针对数值参数的合法性校验机制 --- homebrew/README.md | 42 ++++++++-- homebrew/brew-upgrade-manager-bootstrap.sh | 16 +++- homebrew/brew-upgrade-manager.sh | 97 ++++++++++++++++++++-- 3 files changed, 142 insertions(+), 13 deletions(-) diff --git a/homebrew/README.md b/homebrew/README.md index bdac9a0..2946666 100644 --- a/homebrew/README.md +++ b/homebrew/README.md @@ -16,8 +16,10 @@ - 执行 `brew update -v` 更新 Homebrew 仓库。 - 执行 `brew doctor` 做健康检查;发现问题时给出警告,但不中断后续流程。 - 使用 `brew upgrade --formula` 升级命令行工具。 -- 使用 `brew upgrade --cask --greedy --force` 强制升级 GUI 应用。 -- 当 Cask 升级命令因瞬时下载错误返回失败,但复核确认已无过期 Cask 时,继续执行后续清理。 +- 使用 `brew upgrade --cask --greedy --force` 批量升级 GUI 应用。 +- Homebrew 更新、Formula 升级和 Cask 升级遇到网络、下载、HTTP 429/5xx 等瞬时错误时,会自动退避重试。 +- 批量 Cask 升级只执行一次,以利用 Homebrew 的批量下载能力;失败后只对仍然过期的 Cask 逐个重试,并在重试时去掉 `--force`,避免重复处理已成功的应用。 +- 单个 Cask 的失败不会掩盖其他 Cask 的升级结果;最终会再次核对过期列表,仅在仍有失败项时返回非零状态。 - 执行 `brew cleanup --prune=all` 清理旧版本和缓存。 - 支持固定终端宽度,避免非交互环境下输出宽度异常。 - 启动器支持通过 macOS Keychain 保存并读取 sudo 密码,用于 `sudo -A -v` 预刷新 sudo 凭据。 @@ -153,6 +155,24 @@ BREWUP_SHA256= brewup BREWUP_DEBUG=1 brewup ``` +## 重试设置 + +主脚本默认对可识别的瞬时错误最多尝试 3 次,等待时间从 5 秒开始并按倍数增加: + +```bash +HB_RETRY_ATTEMPTS=4 HB_RETRY_DELAY_SECONDS=3 brewup +``` + +启动器下载远程主脚本时默认最多重试 3 次,每次间隔 2 秒: + +```bash +BREWUP_DOWNLOAD_RETRIES=5 BREWUP_DOWNLOAD_RETRY_DELAY_SECONDS=3 brewup +``` + +权限不足、以 root 运行 Homebrew、代码签名失败等确定性错误不会盲目重复重试。批量 Cask 升级遇到这类错误后,脚本仍会尝试对剩余 Cask 逐个执行一次,以隔离失败项并避开 `--force`。 + +为了兼顾性能,正常命令的 stdout 会直接输出,不写入重试日志;脚本只临时记录通常包含错误信息的 stderr。Cask 批量阶段不会整体重试,只有仍然过期的失败项会进入单项重试。单项升级保持串行,因为并行运行多个 Homebrew 写操作会争用 Homebrew 锁和 `/Applications`,通常不会更快,也更容易产生安装冲突。 + ## 执行流程 主脚本执行顺序: @@ -160,7 +180,7 @@ BREWUP_DEBUG=1 brewup 1. `brew update -v` 2. `brew doctor` 3. `brew upgrade --formula` -4. `brew upgrade --cask --greedy --force` +4. `brew upgrade --cask --greedy --force`;失败时对剩余 Cask 逐个执行 `brew upgrade --cask --greedy ` 5. `brew cleanup --prune=all` ## 常见问题 @@ -236,13 +256,25 @@ Warning: 'brew doctor' detected issues. Manual review and resolution are recomme `brew upgrade --cask --greedy --force` 有时会在下载阶段出现 `curl: (18) Transferred a partial file` 之类的瞬时错误,随后 Homebrew 又重试并完成安装。此时命令仍可能返回非零退出码。 -脚本会在 Cask 升级命令失败后执行一次: +脚本会在批量 Cask 升级命令失败后执行: ```bash brew outdated --cask --greedy ``` -如果没有剩余过期 Cask,脚本会把前一次错误视为瞬时失败并继续执行 `brew cleanup --prune=all`。如果仍有过期 Cask,脚本会保留失败退出状态,方便发现真实失败项。 +如果没有剩余过期 Cask,脚本会把前一次错误视为已恢复并继续执行 `brew cleanup --prune=all`。如果仍有过期 Cask,脚本会逐个重试;网络类错误按配置退避重试,其他错误执行一次。最终仍有过期 Cask 时,脚本保留失败退出状态,方便发现真实失败项。 + +### Cask 报 `Running Homebrew as root` + +某些 macOS/Homebrew 组合在覆盖现有 App、复制扩展属性时,会从 Cask 内部通过 `sudo` 调用 `brew ruby`,随后被 Homebrew 自身的 root 安全检查拒绝。这不代表整个 `brewup` 是通过 `sudo brew` 启动的。 + +新版脚本会在批量升级失败后,对仍然过期的 Cask 去掉 `--force` 单独重试。例如: + +```bash +brew upgrade --cask --greedy visual-studio-code +``` + +如果单独重试仍失败,请到“系统设置 → 隐私与安全性 → App 管理”中允许当前终端管理应用,然后再次运行。不要使用 `sudo brew upgrade`。 ## 注意事项 diff --git a/homebrew/brew-upgrade-manager-bootstrap.sh b/homebrew/brew-upgrade-manager-bootstrap.sh index 6c8173d..0a41dd7 100755 --- a/homebrew/brew-upgrade-manager-bootstrap.sh +++ b/homebrew/brew-upgrade-manager-bootstrap.sh @@ -42,7 +42,21 @@ EOF setup_sudo_askpass echo "正在下载远程脚本..." -curl -f -sSL "$REMOTE" -o "$TEMP" +download_retries="${BREWUP_DOWNLOAD_RETRIES:-3}" +download_retry_delay="${BREWUP_DOWNLOAD_RETRY_DELAY_SECONDS:-2}" +[[ "$download_retries" =~ ^[1-9][0-9]*$ ]] || download_retries=3 +[[ "$download_retry_delay" =~ ^[0-9]+$ ]] || download_retry_delay=2 + +curl_retry_all_errors=() +if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then + curl_retry_all_errors+=(--retry-all-errors) +fi + +curl -f -sSL \ + --retry "$download_retries" \ + --retry-delay "$download_retry_delay" \ + "${curl_retry_all_errors[@]}" \ + "$REMOTE" -o "$TEMP" chmod 600 "$TEMP" if [[ -n "${BREWUP_SHA256:-}" ]]; then diff --git a/homebrew/brew-upgrade-manager.sh b/homebrew/brew-upgrade-manager.sh index cd36204..a0639b6 100644 --- a/homebrew/brew-upgrade-manager.sh +++ b/homebrew/brew-upgrade-manager.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Homebrew 智能升级脚本(强制 Cask 更新增强版 v5.4 - 容忍 Cask 瞬时下载失败) +# Homebrew 智能升级脚本(Cask 失败隔离与重试版) # ================== 脚本环境设置 ================== @@ -46,9 +46,68 @@ terminal_width() { separator() { local width; width=$(terminal_width); printf '=%.0s' $(seq 1 "$width"); printf "\n"; } print_header() { echo -e "${BLUE}$1${NC}"; } -run_cask_upgrade() { - local status=0 outdated_casks +retry_delay_seconds() { + local delay="${HB_RETRY_DELAY_SECONDS:-5}" + [[ "$delay" =~ ^[0-9]+$ ]] || delay=5 + printf '%s' "$delay" +} +retry_attempts() { + local attempts="${HB_RETRY_ATTEMPTS:-3}" + [[ "$attempts" =~ ^[1-9][0-9]*$ ]] || attempts=3 + printf '%s' "$attempts" +} + +is_retryable_brew_error() { + local log_file="$1" + + # 网络、下载和服务端短暂错误适合重试;权限、root、签名和配置错误 + # 通常是确定性失败,重试只会浪费时间并重复修改应用。 + 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' \ + "$log_file" +} + +run_brew_with_retries() { + local label="$1" + shift + + local attempts delay attempt status log_file + attempts="$(retry_attempts)" + delay="$(retry_delay_seconds)" + log_file="$(mktemp "${TMPDIR:-/tmp}/brewup-retry.XXXXXX.log")" + + for ((attempt = 1; attempt <= attempts; attempt++)); do + if ((attempt > 1)); then + echo -e "${YELLOW}Retrying ${label} (${attempt}/${attempts}) after ${delay}s...${NC}" + sleep "$delay" + delay=$((delay * 2)) + fi + + # stdout 直接输出,避免正常升级时把大量进度信息落盘;只复制通常包含 + # 错误详情的 stderr,用于失败分类,同时保持实时终端输出。 + : > "$log_file" + if brew "$@" 2> >(tee "$log_file" >&2); then + rm -f "$log_file" + return 0 + else + status=$? + fi + + if ((attempt == attempts)) || ! is_retryable_brew_error "$log_file"; then + rm -f "$log_file" + return "$status" + fi + echo -e "${YELLOW}${label} failed with a retryable error.${NC}" + done +} + +run_cask_upgrade() { + local status=0 outdated_casks remaining_casks cask + local failed_casks=() + + # 批量阶段只执行一次,让 Homebrew 并发获取下载;失败后只处理仍然过期 + # 的 Cask,避免重跑整个批次和重复检查已经升级成功的应用。 brew upgrade --cask --greedy --force || status=$? if [[ "$status" -eq 0 ]]; then return 0 @@ -66,8 +125,32 @@ run_cask_upgrade() { return 0 fi - echo -e "${YELLOW}The following casks still appear to be outdated:${NC}" + echo -e "${YELLOW}The following casks still appear to be outdated; retrying individually:${NC}" printf '%s\n' "$outdated_casks" + + while IFS= read -r cask; do + [[ -n "$cask" ]] || continue + echo -e "${CYAN}Retrying Cask: ${cask} (without --force)${NC}" + if ! run_brew_with_retries "Cask ${cask}" upgrade --cask --greedy "$cask"; then + failed_casks+=("$cask") + fi + done <<< "$outdated_casks" + + if ! remaining_casks="$(brew outdated --cask --greedy 2>/dev/null)"; then + echo -e "${YELLOW}Warning: Unable to verify outdated Casks after individual retries.${NC}" + return "$status" + fi + + if [[ -z "$remaining_casks" ]]; then + echo -e "${GREEN}All outdated Casks were upgraded after retrying individually.${NC}" + return 0 + fi + + echo -e "${YELLOW}The following Casks remain outdated after retries:${NC}" + printf '%s\n' "$remaining_casks" + if ((${#failed_casks[@]} > 0)); then + echo -e "${YELLOW}These Casks failed during individual retries: ${failed_casks[*]}${NC}" + fi return "$status" } @@ -75,7 +158,7 @@ run_cask_upgrade() { separator print_header "Step 1: Updating Homebrew repositories (brew update -v)" -brew update -v +run_brew_with_retries "Homebrew repository update" update -v separator printf "\n" @@ -95,10 +178,10 @@ echo -e "${NC}" # 1. 升级命令行工具 (Formulae) echo -e "\n${CYAN}>>> [1/2] Upgrading CLI Formulae (brew upgrade --formula)...${NC}" -brew upgrade --formula +run_brew_with_retries "Formula upgrade" upgrade --formula # 2. 升级图形界面软件 (Casks) -echo -e "\n${CYAN}>>> [2/2] Upgrading GUI Casks (brew upgrade --cask --greedy --force)...${NC}" +echo -e "\n${CYAN}>>> [2/2] Upgrading GUI Casks (bulk --force, then isolated retries)...${NC}" # 强制注入环境变量,确保终端输出依然保留 ANSI 颜色格式 export HOMEBREW_COLOR=1