#!/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