540 lines
16 KiB
Bash
Executable File
540 lines
16 KiB
Bash
Executable File
#!/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
|