Files
2026-04-19 21:47:08 +08:00

540 lines
16 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ==============================================================================
# DevContainer 公共函数库
# ==============================================================================
# 禁止直接执行,必须被 source
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
echo "错误: 此文件应该被 source,不能直接执行"
exit 1
fi
# ============================================================================
# 颜色代码定义
# ============================================================================
readonly COLOR_RED='\033[0;31m'
readonly COLOR_GREEN='\033[0;32m'
readonly COLOR_YELLOW='\033[0;33m'
readonly COLOR_BLUE='\033[0;34m'
readonly COLOR_PURPLE='\033[0;35m'
readonly COLOR_CYAN='\033[0;36m'
readonly COLOR_GRAY='\033[0;90m'
readonly COLOR_BOLD='\033[1m'
readonly COLOR_RESET='\033[0m'
# ============================================================================
# 状态符号函数
# ============================================================================
status_ok() {
echo -e "${COLOR_GREEN}${COLOR_RESET}"
}
status_warn() {
echo -e "${COLOR_YELLOW}⚠️${COLOR_RESET}"
}
status_miss() {
echo -e "${COLOR_RED}${COLOR_RESET}"
}
status_info() {
echo -e "${COLOR_BLUE}${COLOR_RESET}"
}
status_fail() {
echo -e "${COLOR_RED}🔴${COLOR_RESET}"
}
# ============================================================================
# 带颜色的文本函数
# ============================================================================
text_green() {
echo -e "${COLOR_GREEN}${1}${COLOR_RESET}"
}
text_yellow() {
echo -e "${COLOR_YELLOW}${1}${COLOR_RESET}"
}
text_red() {
echo -e "${COLOR_RED}${1}${COLOR_RESET}"
}
text_blue() {
echo -e "${COLOR_BLUE}${1}${COLOR_RESET}"
}
text_cyan() {
echo -e "${COLOR_CYAN}${1}${COLOR_RESET}"
}
text_gray() {
echo -e "${COLOR_GRAY}${1}${COLOR_RESET}"
}
text_bold() {
echo -e "${COLOR_BOLD}${1}${COLOR_RESET}"
}
text_purple() {
echo -e "${COLOR_PURPLE}${1}${COLOR_RESET}"
}
text_reset() {
echo -e "${COLOR_RESET}"
}
# ============================================================================
# 输出格式函数
# ============================================================================
print_header() {
local title="$1"
echo ""
echo "$(text_bold '═══════════════════════════════════════════════════════════')"
echo "$(text_bold "${title}")"
echo "$(text_bold '═══════════════════════════════════════════════════════════')"
echo ""
}
print_section() {
local title="$1"
echo ""
echo "$(text_bold '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')"
echo "$(text_bold "${title}")"
echo "$(text_bold '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')"
echo ""
}
print_step() {
local step="$1"
local desc="$2"
echo "[${step}] ${desc}"
}
print_status() {
local status="$1"
local message="$2"
printf " %s %s\n" "${status}" "${message}"
}
# ============================================================================
# 辅助函数
# ============================================================================
# 追加配置到文件(如果不存在标记)
append_config() {
local file="$1"
local marker="$2"
local content="$3"
grep -q "$marker" "$file" 2>/dev/null || echo "$content" >> "$file"
}
# 检查工具是否安装
check_tool() {
local name="$1"
local cmd="$2"
local version_cmd="${3:-$cmd --version}"
if command -v "$cmd" &>/dev/null; then
echo " $(status_ok) ${name}: $($version_cmd 2>/dev/null | head -n1)"
else
echo " $(status_miss) ${name}"
fi
}
# 检查配置文件
check_config() {
local file="$1"
local pattern="$2"
local desc="$3"
if [ -f "$file" ] && grep -q "$pattern" "$file" 2>/dev/null; then
echo " $(status_ok) ${desc}"
else
local status="配置异常"
[ ! -f "$file" ] && status="文件不存在"
echo " $(status_warn) ${desc} ($(text_red "$status"))"
fi
}
# 测试 URL 可用性
test_url() {
local name="$1"
local url="$2"
local status="$(status_fail)"
wget -q --spider --connect-timeout=3 --timeout=5 "$url" 2>/dev/null && status="$(status_ok)"
echo " ${name}: ${status}"
}
# 计算字符串显示宽度(中文字符算2个宽度)
calc_width() {
local str=$1
local width=0
local char
while read -r -n1 char; do
local code
code=$(printf '%d' "'${char}" 2>/dev/null || echo 0)
((width += (code > 127 ? 2 : 1)))
done <<< "$str"
echo "$width"
}
# ============================================================================
# 文件验证函数
# ============================================================================
# 检查文件是否存在且非空
file_exists() {
[ -f "$1" ] && [ -s "$1" ]
}
# 检查通配符文件是否存在
glob_exists() {
local file
file=$(ls $1 2>/dev/null | head -n1)
[ -n "${file}" ] && [ -f "${file}" ] && [ -s "${file}" ]
}
# 检查目录是否有效(非空)
dir_valid() {
[ -d "$1" ] && [ "$(find "$1" -maxdepth 1 -mindepth 1 -not -name ".git" 2>/dev/null | wc -l)" -gt 0 ]
}
# ============================================================================
# 下载函数(带重试和校验)
# ============================================================================
# 计算 SHA256 校验和
compute_sha256() {
local file=$1
if [ -f "${file}" ]; then
sha256sum "${file}" 2>/dev/null | cut -d' ' -f1
else
echo ""
fi
}
# 从 GitHub releases 获取预期校验和
get_github_release_checksum() {
local repo=$1
local version=$2
local filename=$3
local checksums_url="https://github.com/${repo}/releases/download/${version}/checksums.txt"
local temp_checksums
temp_checksums=$(mktemp)
if wget -q --timeout=30 -O "${temp_checksums}" "${checksums_url}" 2>/dev/null; then
local checksum
checksum=$(grep "${filename}" "${temp_checksums}" 2>/dev/null | awk '{print $1}' | head -n1)
rm -f "${temp_checksums}"
echo "${checksum}"
else
rm -f "${temp_checksums}"
echo ""
fi
}
# 从 SHASUMS256.txt 文件获取校验和(用于 Node.js)
get_shasums256_checksum() {
local shasums_url=$1
local filename=$2
local temp_shasums
temp_shasums=$(mktemp)
if wget -q --timeout=30 -O "${temp_shasums}" "${shasums_url}" 2>/dev/null; then
local checksum
checksum=$(grep "${filename}" "${temp_shasums}" 2>/dev/null | awk '{print $1}' | head -n1)
rm -f "${temp_shasums}"
echo "${checksum}"
else
rm -f "${temp_shasums}"
echo ""
fi
}
# 验证文件完整性
verify_file_integrity() {
local file=$1
local expected_checksum=$2
local min_size=${3:-1024}
if [ ! -f "${file}" ] || [ ! -s "${file}" ]; then
printf " %s 文件不存在或为空: %s\n" "$(status_fail)" "$(basename "${file}")"
return 1
fi
local file_size
file_size=$(stat -c%s "${file}" 2>/dev/null || echo 0)
if [ -n "${min_size}" ] && [ "${file_size}" -lt "${min_size}" ]; then
printf " %s 文件过小: %s 字节 (最小: %s)\n" "$(status_fail)" "${file_size}" "${min_size}"
return 1
fi
if [ -n "${expected_checksum}" ]; then
local actual_checksum
actual_checksum=$(compute_sha256 "${file}")
if [ "${actual_checksum}" != "${expected_checksum}" ]; then
printf " %s 校验和不匹配\n" "$(status_fail)"
printf " 预期: %s\n" "${expected_checksum}"
printf " 实际: %s\n" "${actual_checksum}"
return 1
fi
printf " %s 校验和验证通过\n" "$(status_ok)"
fi
return 0
}
# 下载文件(带重试)
download_file() {
local url=$1
local output_file=$2
local desc=$3
local expected_checksum=$4
local min_size=${5:-1024}
local max_retries=3
local retry_count=0
if file_exists "${output_file}"; then
if [ -n "${expected_checksum}" ]; then
if verify_file_integrity "${output_file}" "${expected_checksum}" "${min_size}"; then
print_status_skip "$(basename "${output_file}")" "已验证"
return 0
else
printf " %s 文件损坏,重新下载...\n" "$(status_warn)"
rm -f "${output_file}"
fi
else
if verify_file_integrity "${output_file}" "" "${min_size}"; then
print_status_skip "$(basename "${output_file}")"
return 0
fi
fi
fi
while [ $retry_count -lt $max_retries ]; do
print_status_get "${desc}"
printf " %s 下载地址: %s\n" "$(status_info)" "${url}"
local wget_exit_code=0
if [ -f "${output_file}" ] && [ -s "${output_file}" ]; then
# 文件存在但校验失败,使用断点续传
wget -c --tries=3 --waitretry=2 --show-progress \
--connect-timeout=10 --timeout=300 \
-O "${output_file}" "${url}" 2>&1 || wget_exit_code=$?
else
wget --tries=3 --waitretry=2 --show-progress \
--connect-timeout=10 --timeout=300 \
-O "${output_file}" "${url}" 2>&1 || wget_exit_code=$?
fi
if [ ${wget_exit_code} -eq 0 ]; then
if verify_file_integrity "${output_file}" "${expected_checksum}" "${min_size}"; then
print_status_ok "${desc}"
return 0
else
((retry_count++))
if [ $retry_count -lt $max_retries ]; then
print_status_retry "下载" "${retry_count}"
sleep 2
rm -f "${output_file}"
fi
fi
else
((retry_count++))
if [ $retry_count -lt $max_retries ]; then
print_status_retry "下载失败 (wget=${wget_exit_code})" "${retry_count}"
sleep 2
fi
fi
done
print_status_fail "${desc}" "已重试 ${max_retries} 次"
rm -f "${output_file}"
return 1
}
# 克隆仓库
clone_repo() {
local url=$1
local dest_dir=$2
local branch=$3
local desc=$4
if dir_valid "${dest_dir}"; then
print_status_skip "${desc}"
return 0
fi
rm -rf "${dest_dir}" 2>/dev/null
printf " %s %s\n" "$(text_cyan "⟳ ")" "${desc}"
local clone_args=("--depth=1")
[ -n "${branch}" ] && clone_args+=("--branch" "${branch}")
if git clone "${clone_args[@]}" "${url}" "${dest_dir}" && dir_valid "${dest_dir}"; then
rm -rf "${dest_dir}/.git"
print_status_ok "${desc}"
return 0
fi
print_status_fail "${desc}"
rm -rf "${dest_dir}"
return 1
}
# ============================================================================
# VSCode 扩展函数
# ============================================================================
# 从 .vsix 文件中提取扩展版本
get_vscode_extension_version() {
local vsix_file=$1
if [ -f "${vsix_file}" ]; then
unzip -p "${vsix_file}" "extension/package.json" 2>/dev/null | \
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | \
cut -d'"' -f4 | head -n1
else
echo ""
fi
}
# 从 Marketplace API 获取扩展的最新版本
get_vscode_latest_version() {
local extension_id=$1
local publisher
local extension_name
publisher=${extension_id%%.*}
extension_name=${extension_id#*.}
local api_url="https://marketplace.visualstudio.com/items?itemName=${extension_id}"
# 从 Marketplace 页面提取版本号
wget -q --timeout=30 -U "Mozilla/5.0" -O - "${api_url}" 2>/dev/null | \
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | \
cut -d'"' -f4 | head -n1
}
# ============================================================================
# NPM 包版本函数
# ============================================================================
# 从 .tgz 文件中提取 npm 包版本
get_npm_package_version() {
local tgz_file=$1
if [ -f "${tgz_file}" ]; then
# npm pack 生成的文件结构为 package/package.json
tar -xzf "${tgz_file}" -O package/package.json 2>/dev/null | \
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | \
cut -d'"' -f4 | head -n1
else
echo ""
fi
}
# 从 npm registry 获取包的最新版本
get_npm_latest_version() {
local package_name=$1
local registry=${2:-"https://registry.npmmirror.com"}
# 使用 npm view 命令获取版本
npm view "${package_name}" version --registry "${registry}" 2>/dev/null | head -n1
}
# ============================================================================
# 下载状态输出函数
# ============================================================================
# 统一的下载状态输出
print_status_check() {
local item=$1
printf " %s %s\n" "$(status_info)" "${item}"
}
print_status_get() {
local item=$1
printf " %s %s\n" "$(text_cyan "↓ ")" "${item}"
}
print_status_skip() {
local item=$1
local version=${2:-""}
if [ -n "${version}" ]; then
printf " %s %s (版本: %s)\n" "$(status_ok)" "${item}" "${version}"
else
printf " %s %s\n" "$(status_ok)" "${item}"
fi
}
print_status_update() {
local item=$1
local old_version=$2
local new_version=$3
printf " %s %s: %s → %s\n" "$(text_yellow "↻ ")" "${item}" "${old_version}" "${new_version}"
}
print_status_ok() {
local item=$1
printf " %s %s\n" "$(status_ok)" "${item}"
}
print_status_fail() {
local item=$1
local reason=${2:-""}
if [ -n "${reason}" ]; then
printf " %s %s (%s)\n" "$(status_fail)" "${item}" "${reason}"
else
printf " %s %s\n" "$(status_fail)" "${item}"
fi
}
print_status_warn() {
local item=$1
local message=${2:-""}
if [ -n "${message}" ]; then
printf " %s %s: %s\n" "$(status_warn)" "${item}" "${message}"
else
printf " %s %s\n" "$(status_warn)" "${item}"
fi
}
print_status_retry() {
local item=$1
local count=$2
printf " %s %s - 第 %s 次重试...\n" "$(status_warn)" "${item}" "${count}"
}
print_status_info() {
local message=$1
printf " %s %s\n" "$(status_info)" "${message}"
}
print_step_extra() {
local label=${1:-"+1"}
printf "%s " "$(text_yellow "[${label}]")"
}
# ============================================================================
# 导出所有函数
# ============================================================================
export -f status_ok status_warn status_miss status_info status_fail
export -f text_green text_yellow text_red text_blue text_cyan text_gray text_bold text_purple text_reset
export -f print_header print_section print_step print_status
export -f append_config check_tool check_config test_url calc_width
export -f file_exists glob_exists dir_valid
export -f compute_sha256 get_github_release_checksum get_shasums256_checksum verify_file_integrity download_file clone_repo
export -f get_vscode_extension_version get_vscode_latest_version
export -f get_npm_package_version get_npm_latest_version
export -f print_status_check print_status_get print_status_skip print_status_update print_status_ok print_status_fail print_status_warn print_status_retry print_step_extra