feat(homebrew): 添加 Homebrew 输出流式着色功能
- 实现 can_stream_colorize_tty_output 函数检测流式着色支持 - 添加 colorize_brew_tty_stream 函数处理实时进度条和语义配色 - 使用 Perl 脚本实现流式过滤器,即时处理 CRLF 和 CR 换行 - 优化 run_brew_colored 函数集成伪终端和流式着色 - 改进日志记录功能,保留错误分类和重试决策所需信息 - 修复进度条缓冲问题,确保实时显示升级状态
This commit is contained in:
+2
-1
@@ -31,6 +31,7 @@
|
|||||||
- Homebrew
|
- Homebrew
|
||||||
- Bash
|
- Bash
|
||||||
- `curl`
|
- `curl`
|
||||||
|
- macOS 自带的 `/usr/bin/script` 和 `/usr/bin/perl`(用于兼顾实时进度与自定义配色)
|
||||||
- macOS Keychain 工具 `/usr/bin/security`,仅启动器需要
|
- macOS Keychain 工具 `/usr/bin/security`,仅启动器需要
|
||||||
|
|
||||||
可先检查:
|
可先检查:
|
||||||
@@ -197,7 +198,7 @@ BREWUP_CONNECT_TIMEOUT_SECONDS=20 BREWUP_LOW_SPEED_TIME_SECONDS=45 brewup
|
|||||||
|
|
||||||
为了兼顾性能,Formula 和 Cask 的批量阶段都只执行一次,只有仍然过期的失败项进入单项重试。单项升级保持串行,因为并行运行多个 Homebrew 写操作会争用 Homebrew 锁和 `/Applications`。用于错误分类的临时日志会在命令结束或收到信号后删除。
|
为了兼顾性能,Formula 和 Cask 的批量阶段都只执行一次,只有仍然过期的失败项进入单项重试。单项升级保持串行,因为并行运行多个 Homebrew 写操作会争用 Homebrew 锁和 `/Applications`。用于错误分类的临时日志会在命令结束或收到信号后删除。
|
||||||
|
|
||||||
在交互式终端中,主脚本会让 Homebrew 直接连接 TTY,以便 Homebrew 6 实时重绘并行下载进度。需要记录错误以供重试判断时,macOS 自带的 `script(1)` 会创建伪终端并同步保存日志。如果将 `brewup` 输出重定向到文件或 CI,则自动切换为稳定的逐行输出,不绘制动态进度条。
|
在交互式终端中,主脚本会通过 macOS 自带的 `script(1)` 为 Homebrew 创建伪终端,以便 Homebrew 6 实时重绘并行下载进度。后续的流式着色器会立即透传进度条的回车刷新,并只对完整文本行应用包名、版本和状态配色。需要记录错误以供重试判断时,`script(1)` 也会同步保存日志。如果将 `brewup` 输出重定向到文件或 CI,则自动切换为稳定的逐行输出,不绘制动态进度条。
|
||||||
|
|
||||||
## Cask 与清理策略
|
## Cask 与清理策略
|
||||||
|
|
||||||
|
|||||||
@@ -570,13 +570,158 @@ colorize_brew_output() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
can_stream_colorize_tty_output() {
|
||||||
|
[[ -t 1 && "$(uname -s 2>/dev/null || true)" == "Darwin" && \
|
||||||
|
-x /usr/bin/script && -x /usr/bin/perl ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
colorize_brew_tty_stream() {
|
||||||
|
# `script` 将 brew 的输出传递给本函数时,普通行以 CRLF 结尾,进度条则以
|
||||||
|
# CR 重绘。按行的 Bash read 无法同时处理两者,会等到下一个换行才刷新进度。
|
||||||
|
# 这个小型 Perl 流式过滤器会立即透传 CR 更新,仅对完整的普通行重复使用
|
||||||
|
# 现有的语义配色规则。
|
||||||
|
HB_STREAM_RED="$RED" \
|
||||||
|
HB_STREAM_GREEN="$GREEN" \
|
||||||
|
HB_STREAM_YELLOW="$YELLOW" \
|
||||||
|
HB_STREAM_BLUE="$BLUE" \
|
||||||
|
HB_STREAM_MAGENTA="$MAGENTA" \
|
||||||
|
HB_STREAM_CYAN="$CYAN" \
|
||||||
|
HB_STREAM_BOLD="$BOLD" \
|
||||||
|
HB_STREAM_NC="$NC" \
|
||||||
|
/usr/bin/perl -e '
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use utf8;
|
||||||
|
|
||||||
|
$| = 1;
|
||||||
|
|
||||||
|
my %color = map { $_ => ($ENV{"HB_STREAM_" . uc($_)} // "") }
|
||||||
|
qw(red green yellow blue magenta cyan bold nc);
|
||||||
|
for my $value (values %color) {
|
||||||
|
$value =~ s/\\033/\e/g;
|
||||||
|
$value =~ s/\\e/\e/g;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub print_version_transition {
|
||||||
|
my ($line) = @_;
|
||||||
|
return 0 unless $line =~ /^(\s*)(.*\S)\s+(-\>|=\>)\s+(\S+)(.*)$/;
|
||||||
|
|
||||||
|
my ($indent, $left, $arrow, $new_version, $suffix) = ($1, $2, $3, $4, $5);
|
||||||
|
my ($old_version) = $left =~ /(\S+)$/;
|
||||||
|
my $package = substr($left, 0, length($left) - length($old_version));
|
||||||
|
|
||||||
|
print $indent;
|
||||||
|
print $color{bold}, $color{cyan}, $package, $color{nc} if length $package;
|
||||||
|
print $color{yellow}, $old_version, $color{nc}, " ";
|
||||||
|
print $color{blue}, $arrow, $color{nc}, " ";
|
||||||
|
print $color{bold}, $color{green}, $new_version, $color{nc}, $suffix, "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub print_heading {
|
||||||
|
my ($line) = @_;
|
||||||
|
my $text = substr($line, 4);
|
||||||
|
|
||||||
|
if ($text =~ /^(Upgrading|Installing|Reinstalling|Fetching|Downloading|Pouring|Would)\s+(.+)$/) {
|
||||||
|
print $color{blue}, "==>", $color{nc}, " ";
|
||||||
|
print $color{cyan}, $1, $color{nc}, " ";
|
||||||
|
print $color{bold}, $color{green}, $2, $color{nc}, "\n";
|
||||||
|
} elsif ($text =~ /^(?:Upgraded\s|Installed\s|Successfully\s)/) {
|
||||||
|
print $color{blue}, "==>", $color{nc}, " ", $color{bold}, $color{green}, $text, $color{nc}, "\n";
|
||||||
|
} elsif ($text =~ /^(?:Outdated\s|Disabled\s|Warning)/) {
|
||||||
|
print $color{blue}, "==>", $color{nc}, " ", $color{yellow}, $text, $color{nc}, "\n";
|
||||||
|
} elsif ($text =~ /^(?:Formulae|Casks|New Formulae|New Casks|Caveats|Summary)$/) {
|
||||||
|
print $color{blue}, "==>", $color{nc}, " ", $color{bold}, $color{magenta}, $text, $color{nc}, "\n";
|
||||||
|
} else {
|
||||||
|
print $color{blue}, "==>", $color{nc}, " ", $color{cyan}, $text, $color{nc}, "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub colorize_line {
|
||||||
|
my ($line) = @_;
|
||||||
|
return if print_version_transition($line);
|
||||||
|
|
||||||
|
if ($line =~ /^==> /) {
|
||||||
|
print_heading($line);
|
||||||
|
} elsif ($line =~ /^(?:Warning:|warning:)/) {
|
||||||
|
print $color{yellow}, $line, $color{nc}, "\n";
|
||||||
|
} elsif ($line =~ /^(?:Error:|error:|Failed:|failed:)/) {
|
||||||
|
print $color{red}, $line, $color{nc}, "\n";
|
||||||
|
} elsif ($line =~ /^Removing:/) {
|
||||||
|
print $color{yellow}, $line, $color{nc}, "\n";
|
||||||
|
} elsif ($line =~ /^(?:✔︎ |✔ |🍺 )/ ||
|
||||||
|
$line =~ /was successfully upgraded!|successfully installed!|All dependencies satisfied\./) {
|
||||||
|
print $color{green}, $line, $color{nc}, "\n";
|
||||||
|
} elsif ($line =~ /^(?:Already up-to-date|Updated Homebrew |Updated .*tap|Successfully rebased |Pruned |This operation has freed )/) {
|
||||||
|
print $color{green}, $line, $color{nc}, "\n";
|
||||||
|
} elsif ($line =~ /^ (?:Downloaded|Verified) /) {
|
||||||
|
print $color{green}, $line, $color{nc}, "\n";
|
||||||
|
} else {
|
||||||
|
print $line, "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $buffer = "";
|
||||||
|
while (sysread(STDIN, my $chunk, 8192)) {
|
||||||
|
$buffer .= $chunk;
|
||||||
|
|
||||||
|
while (length $buffer) {
|
||||||
|
my $newline = index($buffer, "\n");
|
||||||
|
my $carriage = index($buffer, "\r");
|
||||||
|
my $position;
|
||||||
|
if ($newline < 0) {
|
||||||
|
$position = $carriage;
|
||||||
|
} elsif ($carriage < 0) {
|
||||||
|
$position = $newline;
|
||||||
|
} else {
|
||||||
|
$position = $newline < $carriage ? $newline : $carriage;
|
||||||
|
}
|
||||||
|
last if $position < 0;
|
||||||
|
|
||||||
|
my $separator = substr($buffer, $position, 1);
|
||||||
|
# 分块时 CR 可能正好在 CRLF 的边界,等待下一块再决定它是换行还是进度重绘。
|
||||||
|
last if $separator eq "\r" && $position + 1 == length($buffer);
|
||||||
|
|
||||||
|
my $record = substr($buffer, 0, $position);
|
||||||
|
if ($separator eq "\r" && substr($buffer, $position + 1, 1) eq "\n") {
|
||||||
|
substr($buffer, 0, $position + 2, "");
|
||||||
|
colorize_line($record);
|
||||||
|
} elsif ($separator eq "\n") {
|
||||||
|
substr($buffer, 0, $position + 1, "");
|
||||||
|
colorize_line($record);
|
||||||
|
} else {
|
||||||
|
substr($buffer, 0, $position + 1, "");
|
||||||
|
print $record, "\r";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length $buffer) {
|
||||||
|
if ($buffer =~ s/\r\z//) {
|
||||||
|
print $buffer, "\r";
|
||||||
|
} else {
|
||||||
|
colorize_line($buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
run_brew_colored() {
|
run_brew_colored() {
|
||||||
local pipeline_status=()
|
local pipeline_status=()
|
||||||
|
|
||||||
# Homebrew 6 的并行下载队列只有在 stdout 是 TTY 时才会绘制
|
# 伪终端保留 Homebrew 的实时进度,流式着色器只处理完整文本行,
|
||||||
# 实时进度。交互式终端中直接运行 brew,保留它的原生进度条与
|
# 不会缓冲或改写用 CR 刷新的进度输出。
|
||||||
# 动态下载状态;重定向到文件或 CI 时再使用按行语义着色。
|
if can_stream_colorize_tty_output; then
|
||||||
if [[ -t 1 ]]; then
|
if /usr/bin/script -q /dev/null env -u HOMEBREW_COLOR HOMEBREW_NO_COLOR=1 brew "$@" | colorize_brew_tty_stream; 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]}"
|
||||||
|
elif [[ -t 1 ]]; then
|
||||||
env -u HOMEBREW_COLOR brew "$@"
|
env -u HOMEBREW_COLOR brew "$@"
|
||||||
return $?
|
return $?
|
||||||
fi
|
fi
|
||||||
@@ -704,14 +849,17 @@ run_brew_with_retries() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
: > "$ACTIVE_LOG_FILE"
|
: > "$ACTIVE_LOG_FILE"
|
||||||
if [[ -t 1 && "$(uname -s 2>/dev/null || true)" == "Darwin" && -x /usr/bin/script ]]; then
|
if can_stream_colorize_tty_output; then
|
||||||
# tee 会把 brew 的 stdout 变成管道,导致 Homebrew 关闭实时进度。
|
# 为错误分类保留转录,同时通过伪终端和流式着色兼顾进度条与版本配色。
|
||||||
# macOS script(1) 为 brew 分配伪终端,同时把完整输出记录到
|
if /usr/bin/script -qF "$ACTIVE_LOG_FILE" env -u HOMEBREW_COLOR HOMEBREW_NO_COLOR=1 brew "$@" | colorize_brew_tty_stream; then
|
||||||
# 日志,供后续错误分类与重试决策使用。-F 确保记录实时刷新。
|
|
||||||
if /usr/bin/script -qF "$ACTIVE_LOG_FILE" env -u HOMEBREW_COLOR brew "$@"; then
|
|
||||||
status=0
|
status=0
|
||||||
else
|
else
|
||||||
status=$?
|
pipeline_status=("${PIPESTATUS[@]}")
|
||||||
|
status="${pipeline_status[0]}"
|
||||||
|
if ((${pipeline_status[1]:-0} != 0)); then
|
||||||
|
print_error "failed to colorize Homebrew output."
|
||||||
|
status="${pipeline_status[1]}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
elif env -u HOMEBREW_COLOR brew "$@" 2>&1 | tee "$ACTIVE_LOG_FILE" | colorize_brew_output; then
|
elif env -u HOMEBREW_COLOR brew "$@" 2>&1 | tee "$ACTIVE_LOG_FILE" | colorize_brew_output; then
|
||||||
status=0
|
status=0
|
||||||
|
|||||||
Reference in New Issue
Block a user