refactor(homebrew): 重构 Homebrew 升级管理脚本的配色和清理策略

- 实现终端主题检测功能,支持深色/浅色模式自动切换配色方案
- 添加 256 色终端支持,优化不同背景下的颜色对比度
- 引入版本变更高亮显示,突出包名、旧版本、新版本的变化
- 集成 OSC 11 协议查询终端背景色,提升跨终端兼容性
- 将清理策略从按日期清理改为统一执行 brew cleanup --prune=all
- 移除自定义缓存清理逻辑,使用 Homebrew 内置清理机制
- 增强输出着色功能,按语义对 Homebrew 输出进行彩色标记
- 更新文档说明新的清理流程和终端配色特性
This commit is contained in:
2026-07-19 01:18:46 +08:00
parent 1f221fd2fc
commit aa85eb32c7
2 changed files with 283 additions and 178 deletions
+19 -14
View File
@@ -20,7 +20,7 @@
- 默认使用 `brew upgrade --cask --greedy`,不强制覆盖已有 App;需要时可显式开启 `--force` - 默认使用 `brew upgrade --cask --greedy`,不强制覆盖已有 App;需要时可显式开启 `--force`
- 网络、下载、HTTP 429/5xx 等瞬时错误会自动退避重试;权限、root、证书、checksum 和磁盘空间错误不会盲目重试。 - 网络、下载、HTTP 429/5xx 等瞬时错误会自动退避重试;权限、root、证书、checksum 和磁盘空间错误不会盲目重试。
- 同一用户只能运行一个主脚本实例,避免并发升级争用 Homebrew 锁和 `/Applications` - 同一用户只能运行一个主脚本实例,避免并发升级争用 Homebrew 锁和 `/Applications`
- 升级结束后清理 Homebrew prefix 中的失效链接,再按本地自然日清理日期早于今天的缓存,同时保留今天生成的文件 - 升级与失败重试全部结束后,执行 `brew cleanup --prune=all` 清理全部缓存和旧版本
- 支持 `--width`、环境变量宽度和终端动态宽度。 - 支持 `--width`、环境变量宽度和终端动态宽度。
- 启动器支持通过 macOS Keychain 保存并读取 sudo 密码,用于 `sudo -A -v` 预刷新 sudo 凭据。 - 启动器支持通过 macOS Keychain 保存并读取 sudo 密码,用于 `sudo -A -v` 预刷新 sudo 凭据。
@@ -102,6 +102,19 @@ HB_TERMINAL_WIDTH=130 ./brew-upgrade-manager.sh
优先级为:命令行 `--width` 高于 `HB_TERMINAL_WIDTH`。两者都不设置时,脚本会读取当前终端宽度;无法读取时默认使用 `130` 优先级为:命令行 `--width` 高于 `HB_TERMINAL_WIDTH`。两者都不设置时,脚本会读取当前终端宽度;无法读取时默认使用 `130`
## 终端主题与配色
主脚本默认使用 `HB_COLOR_THEME=auto`。它会优先读取终端提供的背景信息,并通过标准 OSC 11 查询兼容 Apple Terminal、iTerm2、Termius、Kitty、WezTerm、Alacritty 等常见终端;在 SSH 中,终端客户端允许响应透传时也能识别本地背景。深色背景使用偏亮色阶,浅色背景使用更深的色阶,避免黄色、青色在白底上难以辨认。
如果终端屏蔽了背景色查询,或者需要固定配色,可以手动覆盖:
```bash
HB_COLOR_THEME=dark brewup
HB_COLOR_THEME=light brewup
```
终端支持 256 色时会使用精细色阶;否则自动退回标准 ANSI 深色/浅色方案。输出含义保持一致:包名为青色,旧版本为黄色,变化箭头为蓝色,新版本和成功状态为绿色,警告为黄色,错误为红色。
## 启动器行为 ## 启动器行为
`brew-upgrade-manager-bootstrap.sh` 会执行以下操作: `brew-upgrade-manager-bootstrap.sh` 会执行以下操作:
@@ -189,21 +202,13 @@ BREWUP_CONNECT_TIMEOUT_SECONDS=20 BREWUP_LOW_SPEED_TIME_SECONDS=45 brewup
HB_CASK_FORCE=1 brewup HB_CASK_FORCE=1 brewup
``` ```
默认在升级流程的最后先执行 升级、失败隔离和重试全部在前一步完成。最后的清理阶段只执行一次
```bash ```bash
brew cleanup --prune-prefix brew cleanup --prune=all
``` ```
该命令只处理 prefix 中的失效链接,不删除下载缓存。随后脚本以本地时区的“昨天 23:59:59”为截止点,只删除 Homebrew 缓存目录中修改日期早于今天的文件。它不是滚动 24 小时:无论今天几点运行,今天 00:00 之后生成的缓存都会保留,昨天及更早的缓存会清理。主脚本同时设置 `HOMEBREW_NO_INSTALL_CLEANUP=1`,避免 Homebrew 在每个安装步骤后提前清理。 不再按日期区分新旧缓存,也不保留当天下载;清理由 Homebrew 自己统一完成。主脚本设置 `HOMEBREW_NO_INSTALL_CLEANUP=1`,避免每个安装后重复清理。
如需调整缓存保留天数、彻底清理,或完全跳过清理:
```bash
HB_CLEANUP_DAYS=30 brewup # 改用 Homebrew 的滚动天数策略
HB_CLEANUP_ALL=1 brewup
HB_SKIP_CLEANUP=1 brewup
```
如果 `brew doctor` 明显影响执行时间,也可以跳过: 如果 `brew doctor` 明显影响执行时间,也可以跳过:
@@ -219,7 +224,7 @@ HB_SKIP_DOCTOR=1 brewup
2. `brew doctor` 2. `brew doctor`
3. `brew upgrade --formula`;失败时仅重试仍然过期的 Formula 3. `brew upgrade --formula`;失败时仅重试仍然过期的 Formula
4. `brew upgrade --cask --greedy`;失败时仅重试仍然过期的 Cask 4. `brew upgrade --cask --greedy`;失败时仅重试仍然过期的 Cask
5. `brew cleanup --prune-prefix`,再删除日期早于今天的缓存并保留今天的下载 5. `brew cleanup --prune=all`,一次性清理全部缓存和旧版本
6. 汇总失败项并返回最终状态码 6. 汇总失败项并返回最终状态码
## 常见问题 ## 常见问题
@@ -301,7 +306,7 @@ Warning: 'brew doctor' detected issues. Manual review and resolution are recomme
brew outdated --cask --greedy brew outdated --cask --greedy
``` ```
如果没有剩余过期 Cask,脚本会把前一次错误视为已恢复。如果仍有过期 Cask,脚本会逐个重试;网络类错误按配置退避重试,其他错误执行一次。最终仍有过期项时,脚本返回失败状态并默认保留缓存 如果没有剩余过期 Cask,脚本会把前一次错误视为已恢复。如果仍有过期 Cask,脚本会逐个重试;网络类错误按配置退避重试,其他错误执行一次。最终仍有过期项时,脚本记录失败状态,然后进入统一的 `brew cleanup --prune=all` 清理阶段
### Cask 报 `Running Homebrew as root` ### Cask 报 `Running Homebrew as root`
+264 -164
View File
@@ -16,34 +16,172 @@ prepend_path_once "/usr/local/sbin"
prepend_path_once "/opt/homebrew/sbin" prepend_path_once "/opt/homebrew/sbin"
export PATH export PATH
# 禁止 Homebrew 在每个安装步骤后自动清理;统一在最后一步按缓存年龄清理。 # 禁止 Homebrew 在每个安装步骤后自动清理;统一在最后一步彻底清理。
export HOMEBREW_NO_INSTALL_CLEANUP=1 export HOMEBREW_NO_INSTALL_CLEANUP=1
hex_channel_to_16bit() {
local hex="$1" value maximum bits
[[ "$hex" =~ ^[[:xdigit:]]{1,4}$ ]] || return 1
value=$((16#$hex))
bits=$((${#hex} * 4))
maximum=$(((1 << bits) - 1))
printf '%s' "$((value * 65535 / maximum))"
}
query_terminal_background_rgb() {
local response="" red_hex green_hex blue_hex red green blue
[[ -r /dev/tty && -w /dev/tty ]] || return 1
command -v stty >/dev/null 2>&1 || return 1
command -v dd >/dev/null 2>&1 || return 1
# OSC 11 是跨终端的背景色查询协议。使用子 shell 确保任何退出路径都会
# 恢复 TTY 设置;短暂无输入超时也避免不支持该协议的终端阻塞流程。
response="$({
saved_tty="$(stty -g < /dev/tty 2>/dev/null)" || exit 1
trap 'stty "$saved_tty" < /dev/tty 2>/dev/null || true' EXIT HUP INT TERM
stty -echo -icanon min 0 time 3 < /dev/tty 2>/dev/null || exit 1
printf '\033]11;?\007' > /dev/tty
LC_ALL=C dd bs=1 count=128 < /dev/tty 2>/dev/null || true
} 2>/dev/null)"
if [[ "$response" =~ rgb:([[:xdigit:]]{1,4})/([[:xdigit:]]{1,4})/([[:xdigit:]]{1,4}) ]]; then
red_hex="${BASH_REMATCH[1]}"
green_hex="${BASH_REMATCH[2]}"
blue_hex="${BASH_REMATCH[3]}"
red="$(hex_channel_to_16bit "$red_hex")" || return 1
green="$(hex_channel_to_16bit "$green_hex")" || return 1
blue="$(hex_channel_to_16bit "$blue_hex")" || return 1
printf '%s %s %s' "$red" "$green" "$blue"
return 0
fi
return 1
}
detect_terminal_theme() {
local requested="${HB_COLOR_THEME:-auto}" background_index="" rgb=""
local red green blue brightness
case "$requested" in
dark|light)
printf '%s' "$requested"
return
;;
auto|'') ;;
*) requested=auto ;;
esac
# COLORFGBG 的最后一项是背景色索引,常见终端会主动提供。
if [[ -n "${COLORFGBG:-}" ]]; then
background_index="${COLORFGBG##*[;:]}"
if [[ "$background_index" =~ ^[0-9]+$ ]]; then
if ((background_index == 0 || background_index == 8 || background_index < 7)); then
printf 'dark'
else
printf 'light'
fi
return
fi
fi
# Apple Terminal 不设置 COLORFGBG,但能返回当前标签页的实际背景色。
if [[ "${TERM_PROGRAM:-}" == "Apple_Terminal" ]] && command -v osascript >/dev/null 2>&1; then
rgb="$(osascript -e 'tell application "Terminal" to get background color of current settings of selected tab of front window' 2>/dev/null || true)"
rgb="${rgb//, / }"
read -r red green blue <<< "$rgb"
if [[ "$red" =~ ^[0-9]+$ && "$green" =~ ^[0-9]+$ && "$blue" =~ ^[0-9]+$ ]]; then
brightness=$(((red * 299 + green * 587 + blue * 114) / 1000))
if ((brightness >= 32768)); then
printf 'light'
else
printf 'dark'
fi
return
fi
fi
# iTerm2、Termius、Kitty、WezTerm、Alacritty 等终端通常支持 OSC 11
# 它也可经由 SSH 返回客户端终端的真实背景色。
rgb="$(query_terminal_background_rgb || true)"
read -r red green blue <<< "$rgb"
if [[ "$red" =~ ^[0-9]+$ && "$green" =~ ^[0-9]+$ && "$blue" =~ ^[0-9]+$ ]]; then
brightness=$(((red * 299 + green * 587 + blue * 114) / 1000))
if ((brightness >= 32768)); then
printf 'light'
else
printf 'dark'
fi
return
fi
# 终端未报告背景时,用 macOS 外观作合理回退;其他平台默认深色。
if [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]] && \
[[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null || true)" != "Dark" ]]; then
printf 'light'
else
printf 'dark'
fi
}
COLOR_THEME="none"
if [[ -t 1 ]]; then if [[ -t 1 ]]; then
RED='\033[31m' COLOR_COUNT="$(tput colors 2>/dev/null || true)"
GREEN='\033[32m' [[ "$COLOR_COUNT" =~ ^[0-9]+$ ]] || COLOR_COUNT=8
YELLOW='\033[33m' COLOR_THEME="$(detect_terminal_theme)"
BLUE='\033[34m' if ((COLOR_COUNT >= 256)) && [[ "$COLOR_THEME" == "light" ]]; then
CYAN='\033[36m' # 浅色背景使用更深的色阶,尤其避免亮黄、亮青在白底上失去对比。
RED='\033[38;5;124m'
GREEN='\033[38;5;28m'
YELLOW='\033[38;5;130m'
BLUE='\033[38;5;25m'
MAGENTA='\033[38;5;90m'
CYAN='\033[38;5;30m'
elif ((COLOR_COUNT >= 256)); then
# 深色背景使用偏亮的色阶,保持正文和状态信息清晰。
RED='\033[38;5;203m'
GREEN='\033[38;5;114m'
YELLOW='\033[38;5;221m'
BLUE='\033[38;5;75m'
MAGENTA='\033[38;5;177m'
CYAN='\033[38;5;80m'
elif [[ "$COLOR_THEME" == "light" ]]; then
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
MAGENTA='\033[35m'
CYAN='\033[36m'
else
RED='\033[91m'
GREEN='\033[92m'
YELLOW='\033[93m'
BLUE='\033[94m'
MAGENTA='\033[95m'
CYAN='\033[96m'
fi
BOLD='\033[1m'
NC='\033[0m' NC='\033[0m'
export HOMEBREW_COLOR=1 export HOMEBREW_COLOR=1
else else
COLOR_COUNT=0
RED='' RED=''
GREEN='' GREEN=''
YELLOW='' YELLOW=''
BLUE='' BLUE=''
MAGENTA=''
CYAN='' CYAN=''
BOLD=''
NC='' NC=''
fi fi
TERMINAL_WIDTH="" TERMINAL_WIDTH=""
ACTIVE_LOG_FILE="" ACTIVE_LOG_FILE=""
ACTIVE_CLEANUP_MARKER=""
ACTIVE_CLEANUP_MANIFEST=""
LOCK_ACQUIRED=0 LOCK_ACQUIRED=0
LOCK_DIR="${HB_LOCK_DIR:-${TMPDIR:-/tmp}/brewup-$(id -u).lock}" LOCK_DIR="${HB_LOCK_DIR:-${TMPDIR:-/tmp}/brewup-$(id -u).lock}"
FAILED_FORMULAE=() FAILED_FORMULAE=()
FAILED_CASKS=() FAILED_CASKS=()
CLEANUP_FAILED=0
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@@ -139,6 +277,111 @@ print_colored_list() {
done <<< "$items" done <<< "$items"
} }
print_version_transition() {
local line="$1" indent left arrow new_version suffix old_version package
# Homebrew 常见版本行:
# package 1.0.0 -> 1.1.0
# 1.0.0 -> 1.1.0
# 包名、旧版本、方向和新版本分别着色,让变化能被快速识别。
if [[ "$line" =~ ^([[:space:]]*)(.*[^[:space:]])[[:space:]]+(\-\>|=\>)[[:space:]]+([^[:space:]]+)(.*)$ ]]; then
indent="${BASH_REMATCH[1]}"
left="${BASH_REMATCH[2]}"
arrow="${BASH_REMATCH[3]}"
new_version="${BASH_REMATCH[4]}"
suffix="${BASH_REMATCH[5]}"
old_version="${left##*[[:space:]]}"
package="${left%"$old_version"}"
printf '%s' "$indent"
if [[ -n "$package" ]]; then
printf '%b%s%b' "$BOLD$CYAN" "$package" "$NC"
fi
printf '%b%s%b %b%s%b %b%s%b%s\n' \
"$YELLOW" "$old_version" "$NC" \
"$BLUE" "$arrow" "$NC" \
"$BOLD$GREEN" "$new_version" "$NC" "$suffix"
return 0
fi
return 1
}
colorize_brew_heading() {
local text="${1#==> }" action subject
case "$text" in
Upgrading\ *|Installing\ *|Reinstalling\ *|Fetching\ *|Downloading\ *|Pouring\ *|Would\ upgrade\ *)
action="${text%% *}"
subject="${text#* }"
printf '%b==>%b %b%s%b %b%s%b\n' \
"$BLUE" "$NC" "$CYAN" "$action" "$NC" "$BOLD$GREEN" "$subject" "$NC"
;;
Upgraded\ *|Installed\ *|Successfully\ *)
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$BOLD$GREEN" "$text" "$NC"
;;
Outdated\ *|Disabled\ *|Warning*)
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$YELLOW" "$text" "$NC"
;;
Formulae|Casks|New\ Formulae|New\ Casks|Caveats|Summary)
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$BOLD$MAGENTA" "$text" "$NC"
;;
*)
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$CYAN" "$text" "$NC"
;;
esac
}
colorize_brew_output() {
local line
while IFS= read -r line || [[ -n "$line" ]]; do
if print_version_transition "$line"; then
continue
fi
case "$line" in
'==> '*)
colorize_brew_heading "$line"
;;
Warning:*|warning:*)
printf '%b%s%b\n' "$YELLOW" "$line" "$NC"
;;
Error:*|error:*|Failed:*|failed:*)
printf '%b%s%b\n' "$RED" "$line" "$NC"
;;
Removing:*)
printf '%b%s%b\n' "$YELLOW" "$line" "$NC"
;;
'✔︎ '*|'✔ '*|'🍺 '*|*'was successfully upgraded!'|*'successfully installed!'|All\ dependencies\ satisfied.)
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
;;
Already\ up-to-date.*|Updated\ Homebrew\ *|Updated\ *tap*|Successfully\ rebased\ *|Pruned\ *|This\ operation\ has\ freed\ *)
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
;;
' Downloaded '*|' Verified '*)
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
;;
*)
printf '%s\n' "$line"
;;
esac
done
}
run_brew_colored() {
local pipeline_status=()
# 管道输出时禁用 Homebrew 自带的不完整配色,再统一按语义着色。
if env -u HOMEBREW_COLOR brew "$@" 2>&1 | colorize_brew_output; then
return 0
fi
pipeline_status=("${PIPESTATUS[@]}")
if ((${pipeline_status[1]:-0} != 0)); then
print_error "failed to colorize Homebrew output."
return "${pipeline_status[1]}"
fi
return "${pipeline_status[0]}"
}
is_enabled() { is_enabled() {
case "${1:-}" in case "${1:-}" in
1|true|TRUE|yes|YES|on|ON) return 0 ;; 1|true|TRUE|yes|YES|on|ON) return 0 ;;
@@ -151,12 +394,6 @@ cleanup_runtime() {
if [[ -n "${ACTIVE_LOG_FILE:-}" && -f "$ACTIVE_LOG_FILE" ]]; then if [[ -n "${ACTIVE_LOG_FILE:-}" && -f "$ACTIVE_LOG_FILE" ]]; then
rm -f "$ACTIVE_LOG_FILE" rm -f "$ACTIVE_LOG_FILE"
fi fi
if [[ -n "${ACTIVE_CLEANUP_MARKER:-}" && -f "$ACTIVE_CLEANUP_MARKER" ]]; then
rm -f "$ACTIVE_CLEANUP_MARKER"
fi
if [[ -n "${ACTIVE_CLEANUP_MANIFEST:-}" && -f "$ACTIVE_CLEANUP_MANIFEST" ]]; then
rm -f "$ACTIVE_CLEANUP_MANIFEST"
fi
if ((LOCK_ACQUIRED)); then if ((LOCK_ACQUIRED)); then
rm -f "$LOCK_DIR/pid" rm -f "$LOCK_DIR/pid"
rmdir "$LOCK_DIR" 2>/dev/null || true rmdir "$LOCK_DIR" 2>/dev/null || true
@@ -257,7 +494,7 @@ run_brew_with_retries() {
fi fi
: > "$ACTIVE_LOG_FILE" : > "$ACTIVE_LOG_FILE"
if brew "$@" 2>&1 | tee "$ACTIVE_LOG_FILE"; then if env -u HOMEBREW_COLOR brew "$@" 2>&1 | tee "$ACTIVE_LOG_FILE" | colorize_brew_output; then
status=0 status=0
else else
pipeline_status=("${PIPESTATUS[@]}") pipeline_status=("${PIPESTATUS[@]}")
@@ -265,6 +502,9 @@ run_brew_with_retries() {
if ((${pipeline_status[1]:-0} != 0)); then if ((${pipeline_status[1]:-0} != 0)); then
print_error "failed to write retry log." print_error "failed to write retry log."
status="${pipeline_status[1]}" status="${pipeline_status[1]}"
elif ((${pipeline_status[2]:-0} != 0)); then
print_error "failed to colorize Homebrew output."
status="${pipeline_status[2]}"
fi fi
fi fi
@@ -334,7 +574,7 @@ run_formula_upgrade() {
print_success "No Formulae require updates." print_success "No Formulae require updates."
fi fi
brew upgrade --formula || bulk_status=$? run_brew_colored upgrade --formula || bulk_status=$?
if ((bulk_status == 0)); then if ((bulk_status == 0)); then
if ((initial_formulae_known)) && [[ -n "$initial_formulae" ]]; then if ((initial_formulae_known)) && [[ -n "$initial_formulae" ]]; then
print_colored_list "Updated Formulae:" "$GREEN" "$initial_formulae" print_colored_list "Updated Formulae:" "$GREEN" "$initial_formulae"
@@ -409,7 +649,7 @@ run_cask_upgrade() {
print_success "No Casks require updates." print_success "No Casks require updates."
fi fi
brew "${bulk_args[@]}" || bulk_status=$? run_brew_colored "${bulk_args[@]}" || bulk_status=$?
if ((bulk_status == 0)); then if ((bulk_status == 0)); then
if ((initial_casks_known)) && [[ -n "$initial_casks" ]]; then if ((initial_casks_known)) && [[ -n "$initial_casks" ]]; then
print_colored_list "Updated Casks:" "$GREEN" "$initial_casks" print_colored_list "Updated Casks:" "$GREEN" "$initial_casks"
@@ -464,152 +704,8 @@ run_cask_upgrade() {
return 1 return 1
} }
format_bytes() {
local bytes="$1" unit=0 divisor=1 tenths
local units=(B KiB MiB GiB TiB)
while ((unit < 4 && bytes >= divisor * 1024)); do
((unit += 1))
((divisor *= 1024))
done
if ((unit == 0)); then
printf '%d B' "$bytes"
return
fi
tenths=$(((bytes * 10 + divisor / 2) / divisor))
printf '%d.%d %s' "$((tenths / 10))" "$((tenths % 10))" "${units[unit]}"
}
is_cache_download_artifact() {
[[ "$1" == downloads/* ]]
}
cache_artifact_display_name() {
local name="${1#downloads/}"
# Homebrew 下载文件通常以 SHA-256-- 开头;终端只显示有意义的原文件名。
[[ "$name" == *--* ]] && name="${name#*--}"
printf '%s' "$name"
}
prune_cache_before_today() {
local cache_dir cutoff_stamp cutoff_date file file_size relative_path display_name hidden_count
local removed_count=0 removed_bytes=0 kept_count=0 kept_bytes=0 cleanup_failed=0
local removed_artifact_count=0 removed_artifact_bytes=0
local kept_artifact_count=0 kept_artifact_bytes=0
cache_dir="$(brew --cache)"
if [[ ! -d "$cache_dir" || "$cache_dir" == "/" || "$cache_dir" == "$HOME" ]]; then
print_error "Homebrew returned an unsafe cache path: $cache_dir"
return 1
fi
# macOS date -v 使用本地时区。以昨天 23:59:59 为边界,确保今天零点
# 之后生成的缓存全部保留,而不是使用滚动 24 小时窗口。
cutoff_stamp="$(date -v-1d '+%Y%m%d2359.59')"
cutoff_date="$(date '+%Y-%m-%d')"
ACTIVE_CLEANUP_MARKER="$(mktemp "${TMPDIR:-/tmp}/brewup-cleanup-cutoff.XXXXXX")"
ACTIVE_CLEANUP_MANIFEST="$(mktemp "${TMPDIR:-/tmp}/brewup-cleanup-files.XXXXXX")"
touch -t "$cutoff_stamp" "$ACTIVE_CLEANUP_MARKER"
if ! find "$cache_dir" -mindepth 1 \( -type f -o -type l \) \
! -newer "$ACTIVE_CLEANUP_MARKER" -print0 > "$ACTIVE_CLEANUP_MANIFEST"; then
print_error "failed to enumerate Homebrew cache files to remove."
return 1
fi
printf '%bRemoved download artifacts:%b\n' "$YELLOW" "$NC"
while IFS= read -r -d '' file; do
if ! file_size="$(stat -f '%z' -- "$file" 2>/dev/null)"; then
file_size=0
fi
relative_path="${file#"$cache_dir"/}"
if rm -f -- "$file"; then
removed_count=$((removed_count + 1))
removed_bytes=$((removed_bytes + file_size))
if is_cache_download_artifact "$relative_path"; then
removed_artifact_count=$((removed_artifact_count + 1))
removed_artifact_bytes=$((removed_artifact_bytes + file_size))
display_name="$(cache_artifact_display_name "$relative_path")"
printf ' %b%10s%b %q\n' "$CYAN" "$(format_bytes "$file_size")" "$NC" "$display_name"
fi
else
print_warning "failed to remove cache file: $file" >&2
cleanup_failed=1
fi
done < "$ACTIVE_CLEANUP_MANIFEST"
((removed_artifact_count > 0)) || printf ' (none)\n'
hidden_count=$((removed_count - removed_artifact_count))
printf '%bRemoved:%b %b%d files / %s%b before %b%s%b.\n' \
"$YELLOW" "$NC" "$CYAN" "$removed_count" "$(format_bytes "$removed_bytes")" "$NC" \
"$BLUE" "$cutoff_date" "$NC"
printf ' Downloads shown: %b%d / %s%b; internal files hidden: %b%d%b.\n' \
"$CYAN" "$removed_artifact_count" "$(format_bytes "$removed_artifact_bytes")" "$NC" \
"$CYAN" "$hidden_count" "$NC"
if ! find "$cache_dir" -mindepth 1 \( -type f -o -type l \) \
-newer "$ACTIVE_CLEANUP_MARKER" -print0 > "$ACTIVE_CLEANUP_MANIFEST"; then
print_error "failed to enumerate today's retained Homebrew cache files."
return 1
fi
printf '%bRetained download artifacts from today:%b\n' "$GREEN" "$NC"
while IFS= read -r -d '' file; do
if ! file_size="$(stat -f '%z' -- "$file" 2>/dev/null)"; then
file_size=0
fi
relative_path="${file#"$cache_dir"/}"
kept_count=$((kept_count + 1))
kept_bytes=$((kept_bytes + file_size))
if is_cache_download_artifact "$relative_path"; then
kept_artifact_count=$((kept_artifact_count + 1))
kept_artifact_bytes=$((kept_artifact_bytes + file_size))
display_name="$(cache_artifact_display_name "$relative_path")"
printf ' %b%10s%b %q\n' "$CYAN" "$(format_bytes "$file_size")" "$NC" "$display_name"
fi
done < "$ACTIVE_CLEANUP_MANIFEST"
((kept_artifact_count > 0)) || printf ' (none)\n'
hidden_count=$((kept_count - kept_artifact_count))
printf '%bRetained:%b %b%d files / %s%b dated %b%s%b or later.\n' \
"$GREEN" "$NC" "$CYAN" "$kept_count" "$(format_bytes "$kept_bytes")" "$NC" \
"$BLUE" "$cutoff_date" "$NC"
printf ' Downloads shown: %b%d / %s%b; internal files hidden: %b%d%b.\n' \
"$CYAN" "$kept_artifact_count" "$(format_bytes "$kept_artifact_bytes")" "$NC" \
"$CYAN" "$hidden_count" "$NC"
rm -f "$ACTIVE_CLEANUP_MARKER"
ACTIVE_CLEANUP_MARKER=""
rm -f "$ACTIVE_CLEANUP_MANIFEST"
ACTIVE_CLEANUP_MANIFEST=""
return "$cleanup_failed"
}
run_cleanup() { run_cleanup() {
local cleanup_days="${HB_CLEANUP_DAYS:-}" run_brew_colored cleanup --prune=all
if is_enabled "${HB_SKIP_CLEANUP:-0}"; then
print_notice "Cleanup skipped because HB_SKIP_CLEANUP is enabled."
return 0
fi
if is_enabled "${HB_CLEANUP_ALL:-0}" || [[ "$cleanup_days" == "all" ]]; then
brew cleanup --prune=all
return
fi
if [[ -n "$cleanup_days" ]]; then
if [[ ! "$cleanup_days" =~ ^[0-9]+$ ]] || ((cleanup_days > 3650)); then
print_error "HB_CLEANUP_DAYS must be an integer between 0 and 3650."
return 2
fi
brew cleanup --prune="$cleanup_days"
return
fi
# 普通 brew cleanup 可能按版本状态删除今天生成的下载,无法严格保留
# 今日缓存。这里只让 Homebrew 清理 prefix 中的失效链接,再按自然日
# 精确清理缓存文件。
brew cleanup --prune-prefix
prune_cache_before_today
} }
print_summary() { print_summary() {
@@ -619,13 +715,16 @@ print_summary() {
if ((overall_status == 0)); then if ((overall_status == 0)); then
print_success "All operations completed successfully." print_success "All operations completed successfully."
else else
printf '%bHomebrew upgrade completed with unresolved failures.%b\n' "$RED" "$NC" printf '%bHomebrew maintenance completed with unresolved failures.%b\n' "$RED" "$NC"
if ((${#FAILED_FORMULAE[@]} > 0)); then if ((${#FAILED_FORMULAE[@]} > 0)); then
printf '%bFailed Formulae:%b %s\n' "$RED" "$NC" "${FAILED_FORMULAE[*]}" printf '%bFailed Formulae:%b %s\n' "$RED" "$NC" "${FAILED_FORMULAE[*]}"
fi fi
if ((${#FAILED_CASKS[@]} > 0)); then if ((${#FAILED_CASKS[@]} > 0)); then
printf '%bFailed Casks:%b %s\n' "$RED" "$NC" "${FAILED_CASKS[*]}" printf '%bFailed Casks:%b %s\n' "$RED" "$NC" "${FAILED_CASKS[*]}"
fi fi
if ((CLEANUP_FAILED)); then
printf '%bHomebrew cleanup failed.%b\n' "$RED" "$NC"
fi
fi fi
separator separator
} }
@@ -674,9 +773,10 @@ separator
printf '\n' printf '\n'
separator separator
print_header "Step 4: Cleaning old cache (showing download artifacts only)" print_header "Step 4: Cleaning all Homebrew cache and old versions (brew cleanup --prune=all)"
if ! run_cleanup; then if ! run_cleanup; then
overall_status=1 overall_status=1
CLEANUP_FAILED=1
fi fi
separator separator
printf '\n' printf '\n'