#!/usr/bin/env bash # Homebrew 智能升级脚本(Cask 失败隔离与重试版) # ================== 脚本环境设置 ================== # set -e:当命令返回非零退出状态(表示失败)时,脚本会立即退出。 set -e # set -o pipefail:在管道命令中,如果任何一个子命令失败,整个管道即为失败。 set -o pipefail prepend_path_once() { local dir="$1" [[ -d "$dir" ]] || return 0 case ":$PATH:" in *":$dir:"*) ;; *) PATH="$dir:$PATH" ;; esac } prepend_path_once "/usr/local/sbin" prepend_path_once "/opt/homebrew/sbin" export PATH # --- 颜色定义 (自动检测终端是否支持) --- if [ -t 1 ]; then GREEN='\033[1;32m' YELLOW='\033[1;33m' BLUE='\033[1;34m' CYAN='\033[1;36m' NC='\033[0m' else GREEN='' YELLOW='' BLUE='' CYAN='' NC='' fi # --- 终端宽度和打印函数 --- TERMINAL_WIDTH="130" terminal_width() { printf "%s" "$TERMINAL_WIDTH" } separator() { local width; width=$(terminal_width); printf '=%.0s' $(seq 1 "$width"); printf "\n"; } print_header() { echo -e "${BLUE}$1${NC}"; } 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 fi echo -e "${YELLOW}Warning: Cask upgrade reported a non-zero exit code (${status}). Verifying whether outdated casks remain...${NC}" if ! outdated_casks="$(brew outdated --cask --greedy 2>/dev/null)"; then echo -e "${YELLOW}Warning: Unable to verify outdated casks after the failed upgrade command.${NC}" return "$status" fi if [[ -z "$outdated_casks" ]]; then echo -e "${YELLOW}Warning: No outdated casks remain. Treating the previous Cask error as transient and continuing.${NC}" return 0 fi 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" } # ================== 流程开始 ================== separator print_header "Step 1: Updating Homebrew repositories (brew update -v)" run_brew_with_retries "Homebrew repository update" update -v separator printf "\n" separator print_header "Step 2: Performing health check (brew doctor)" if ! brew doctor; then echo -e "${YELLOW}Warning: 'brew doctor' detected issues. Manual review and resolution are recommended.${NC}" else echo "Homebrew environment is in good health." fi separator printf "\n" separator print_header "Step 3: Executing comprehensive upgrades (Formulae & Casks)" echo -e "${NC}" # 1. 升级命令行工具 (Formulae) echo -e "\n${CYAN}>>> [1/2] Upgrading CLI Formulae (brew upgrade --formula)...${NC}" run_brew_with_retries "Formula upgrade" upgrade --formula # 2. 升级图形界面软件 (Casks) echo -e "\n${CYAN}>>> [2/2] Upgrading GUI Casks (bulk --force, then isolated retries)...${NC}" # 强制注入环境变量,确保终端输出依然保留 ANSI 颜色格式 export HOMEBREW_COLOR=1 # --- 终端宽度处理 --- # 固定宽度,避免动态识别终端列数导致 prompt 重绘异常。 export COLUMNS COLUMNS="$(terminal_width)" run_cask_upgrade separator printf "\n" separator print_header "Step 4: Cleaning up old files and caches (brew cleanup --prune=all)" brew cleanup --prune=all separator printf "\n" echo -e "${GREEN}All operations completed!${NC}" printf "\n"