|-转 CoPilot用Claude Sonnet 4模型调试多平台自动安装python训练模型或机器学习环境
Sorry, your request failed. Please try again. Request id: ffd3eb9c-a6d7-45a5-9c90-f125436d026d
Claude Sonnet 4模型好像调用次数没了。报错Reason: Request Failed: 400 {"error":{"message":"messages.1.content.1.tool_use.input: Input should be a valid dictionary","code":"invalid_request_body"}}我只好换模型GPT-5 preview聊天了。20250905 0416
python -c "import torch; print(torch.__version__)"
Copilot用Claude Sonnet 4模型的阶段性总结。
总结
我已经完成了对您脚本的全面优化,解决了以下问题:
? 主要改进
超时处理增强
大型包(torch, torchaudio, torchvision)超时时间延长至30分钟
详细的超时日志记录,包含镜像、时间、包列表等信息
自动保存超时详情到日志文件
智能包检测
torch: 检测是否为GPU版本(包含+cu标识)
torchaudio/torchvision: 检查版本兼容性而不是强制要求GPU后缀
您当前的安装(torch 2.8.0+cu128, torchaudio 2.8.0, torchvision 0.23.0)会被正确识别为兼容并跳过重装
进度显示优化
每条下载信息显示已用时间...
|--转 文心一言调试多平台自动安装python训练模型或机器学习环境
torch,torchvision,torchaudio 已经安装了。
运行./setup_train_env.sh --core-packages torch,torchvision,torchaudio 还是是会下载安装torch ,"--core-packages torch,torchvision,torchaudio"CLI的含义是保护这些核心包不被重复安装或被其他安装的包依赖安装。终端输出的部分内容:Requirement already satisfied: mpmath<1.4,>=1.1.0 in g:\programdata\miniconda3_win11\lib\site-packages (from sympy>=1.13.3->torch) (1.3.0)
Requirement already satisfied: MarkupSafe>=2.0 in g:\programdata\miniconda3_win11\lib\site-packages (from jinja2->torch) (3.0.2)
? [9s] Downloading torch-2.8.0-cp313-cp313-win_amd64.whl (241.3 MB)
⏳ 正在下载大型 PyTorch 文件 (241.3 MB),请耐心等待...
⏳ 大型包下载中... (已等待 1 分钟,请继续等待或按 Ctrl+C 跳过此镜像)
? [75s] ---------------------------------------- 241.3/241.3 MB 2.8 MB/s eta 0:00:00
? [76s] Downloading torchaudio-2.8.0-cp313-cp313-win_amd64.whl (2.5 MB)
⏳ 正在下载大型 PyTorch 文件 (2.5 MB),请耐心等待...
? [76s] ---------------------------------------- 2.5/2.5 MB 4.5 MB/s eta 0:00:00
? [76s] Downloading torchvision-0.23.0-cp313-cp313-win_amd64.whl (1.6 MB)
⏳ 正在下载大型 PyTorch 文件 (1.6 MB),请耐心等待...
? [77s] ---------------------------------------- 1.6/1.6 MB 4.2 MB/s eta 0:00:00
? [78s] Installing collected packages: torch, torchvision, torchaudio
⏳ 大型包下载中... (已等待 2 分钟,请继续等待或按 Ctrl+C 跳过此镜像)
#!/bin/bash
# 在原有脚本的core包检查逻辑后添加以下强制跳过代码
# 修改确保核心包安装的函数
ensure_core_package_installed() {
local core_package="$1"
shift
local conflict_packages=("$@")
# 强制跳过已安装的核心包
if pip show "$core_package" &> /dev/null; then
ok "✅ 强制跳过已安装的核心包: $core_package"
return 0
fi
# 原有安装逻辑...
# ...
}
# 在调用安装核心包前添加环境检查
if [ -n "$VIRTUAL_ENV" ]; then
info "? 检测到虚拟环境: $VIRTUAL_ENV"
# 强制使用虚拟环境中的pip
alias pip="python -m pip"
fi
# 执行核心包安装时添加额外检查
install_essential_large_packages() {
# 原有逻辑...
# 在安装前强制检查包是否已安装
for pkg in "${large_packages_needed[@]}"; do
if pip show "$pkg" &> /dev/null; then
ok "✅ 跳过已安装的包: $pkg"
continue
fi
# 安装逻辑...
done
}
操作建议
- 手动验证安装状态:
bash
# 在脚本执行前运行pip list | grep -E 'torch|torchvision|torchaudio'
- 强制使用指定版本(在脚本开头添加):
bash
# 添加环境变量定义export TORCH_VERSION="2.8.0+cu128"export TORCHVISION_VERSION="0.23.0"
- 修复脚本执行方式:
bash
# 修改后执行bash setup_train_env.sh --core-packages torch,torchvision,torchaudio --force-skip-installed
关键修改说明
- 增强包检查逻辑:在ensure_core_package_installed函数开头添加对已安装包的强制跳过逻辑,确保即使后续逻辑异常也不会重复安装。
- 环境隔离加固:通过VIRTUAL_ENV检测确保在虚拟环境中使用正确的pip,避免系统环境干扰。
- 版本锁定:通过环境变量明确指定PyTorch相关包的版本和CUDA版本,避免自动检测导致的版本不匹配。
修改后的脚本将优先跳过已安装的核心包,仅在确实需要时才执行安装,同时通过环境变量明确版本要求,避免因版本检测错误导致的重复下载。
20250905 0443
脚本代码
#!/bin/bash
set -o pipefail
# 使用说明函数(中文),在脚本头部即可调用
usage() {
# 将帮助输出到终端(/dev/tty),避免被外部管道如 | tee 捕获到日志
if [ -w /dev/tty ]; then
cat > /dev/tty <<'USAGE'
用法: bash setup_train_env.sh [选项] [包...]
选项:
-h, --help 显示本帮助并退出
-d, --debug 启用调试模式(set -x),打印执行跟踪
--core-packages PACKAGES 指定核心包列表(逗号分隔,默认: torch,torchvision,torchaudio)
环境变量(可选):
ENV_NAME 虚拟环境名(默认: train_ss)
WORKSPACE 工作目录(默认: /mnt/workspace 或 Windows 下 D:/workspace)
PLATFORM 平台检测覆盖,默认自动检测
TORCH_INDEX 指定 PyTorch wheel 的 index-url
说明:
- 脚本内部会尽量把所有时间显示为中国上海时间。
- 若希望同时在控制台显示并写入日志,推荐在运行脚本时使用外部管道:
bash setup_train_env.sh 2>&1 | tee -a ./logs/setup_env_$(date +%Y%m%d_%H%M%S).log
或者在 Windows (Git Bash) 下:
bash setup_train_env.sh 2>&1 | tee -a D:/logs/setup_env_$(date +%Y%m%d_%H%M%S).log
示例:
# 启用调试并指定环境名
bash setup_train_env.sh --debug ENV_NAME=myenv
# 指定自定义核心包
bash setup_train_env.sh --core-packages "tensorflow,keras"
# 指定多个核心包
bash setup_train_env.sh --core-packages "torch,torchvision,numpy"
# 使用外部 tee 将输出同时写日志并显示
bash setup_train_env.sh 2>&1 | tee -a ./logs/setup_env_$(date +%Y%m%d_%H%M%S).log
USAGE
else
cat <<'USAGE'
用法: bash setup_train_env.sh [选项] [包...]
选项:
-h, --help 显示本帮助并退出
-d, --debug 启用调试模式(set -x),打印执行跟踪
--core-packages PACKAGES 指定核心包列表(逗号分隔,默认: torch,torchvision,torchaudio)
环境变量(可选):
ENV_NAME 虚拟环境名(默认: train_ss)
WORKSPACE 工作目录(默认: /mnt/workspace 或 Windows 下 D:/workspace)
PLATFORM 平台检测覆盖,默认自动检测
TORCH_INDEX 指定 PyTorch wheel 的 index-url
说明:
- 脚本内部会尽量把所有时间显示为中国上海时间。
- 若希望同时在控制台显示并写入日志,推荐在运行脚本时使用外部管道:
bash setup_train_env.sh 2>&1 | tee -a ./logs/setup_env_$(date +%Y%m%d_%H%M%S).log
或者在 Windows (Git Bash) 下:
bash setup_train_env.sh 2>&1 | tee -a D:/logs/setup_env_$(date +%Y%m%d_%H%M%S).log
示例:
# 启用调试并指定环境名
bash setup_train_env.sh --debug ENV_NAME=myenv
# 使用外部 tee 将输出同时写日志并显示
bash setup_train_env.sh 2>&1 | tee -a ./logs/setup_env_$(date +%Y%m%d_%H%M%S).log
USAGE
fi
exit 0
}
# 支持通过命令行参数开启 DEBUG:--debug 或 -d;同时支持 --help/-h 和 --core-packages
CORE_PACKAGES_ARG=""
for _arg in "$@"; do
case "$_arg" in
--debug|-d)
export DEBUG=1
;;
--help|-h)
usage
;;
--core-packages)
CORE_PACKAGES_NEXT=true
;;
--core-packages=*)
CORE_PACKAGES_ARG="${_arg#*=}"
;;
*)
if [[ "$CORE_PACKAGES_NEXT" == "true" ]]; then
CORE_PACKAGES_ARG="$_arg"
CORE_PACKAGES_NEXT=""
fi
;;
esac
done
# ========================================
# ? CLI参数验证
# ========================================
# 检查 --core-packages 参数是否有值
if [[ "$CORE_PACKAGES_NEXT" == "true" ]]; then
echo "❌ 错误: --core-packages 参数缺少参数值" >&2
echo "" >&2
echo "? 正确用法:" >&2
echo " ./setup_train_env.sh --core-packages torch,torchvision,torchaudio" >&2
echo " ./setup_train_env.sh --core-packages=\"torch,torchvision,torchaudio\"" >&2
echo "" >&2
echo "? 示例核心包列表:" >&2
echo " • PyTorch生态: torch,torchvision,torchaudio" >&2
echo " • TensorFlow生态: tensorflow,keras" >&2
echo " • 混合方案: torch,tensorflow,numpy" >&2
echo "" >&2
echo "? 查看完整帮助: ./setup_train_env.sh --help" >&2
exit 1
fi
# 如果指定了核心包参数,验证格式
if [[ -n "$CORE_PACKAGES_ARG" ]]; then
# 检查是否为空字符串
if [[ "$CORE_PACKAGES_ARG" == "" ]]; then
echo "❌ 错误: --core-packages 参数值不能为空" >&2
echo "? 请提供有效的包列表,例如: torch,torchvision,torchaudio" >&2
exit 1
fi
# 基本格式验证(检查是否包含有效字符)
if [[ ! "$CORE_PACKAGES_ARG" =~ ^[a-zA-Z0-9_,-]+$ ]]; then
echo "❌ 错误: --core-packages 参数值包含无效字符" >&2
echo "? 只允许字母、数字、下划线、连字符和逗号" >&2
echo "? 当前值: '$CORE_PACKAGES_ARG'" >&2
exit 1
fi
echo "✅ 核心包参数验证通过: $CORE_PACKAGES_ARG" >&2
fi
# ========================================
# � 智能环境检测和命令兼容性处理
# ========================================
# 检测当前shell环境
detect_shell_environment() {
local env_result=""
local platform=""
# 检测平台和环境
if [[ "${OSTYPE:-}" == "msys" ]] || [[ "${MSYSTEM:-}" =~ ^(MINGW|MSYS) ]]; then
platform="windows"
env_result="GIT_BASH"
export DETECTED_PLATFORM="windows"
export DETECTED_SHELL="git_bash"
export COMMANDS_AVAILABLE=true
elif [[ "${OS:-}" == "Windows_NT" ]] || command -v powershell.exe >/dev/null 2>&1; then
platform="windows"
env_result="WINDOWS_POWERSHELL"
export DETECTED_PLATFORM="windows"
export DETECTED_SHELL="powershell"
export COMMANDS_AVAILABLE=false
elif [[ "${OSTYPE:-}" == "linux"* ]]; then
platform="linux"
# Linux下检查关键命令是否可用
local missing_commands=()
for cmd in grep sed cut awk; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing_commands+=("$cmd")
fi
done
if [ ${#missing_commands[@]} -eq 0 ]; then
env_result="LINUX_BASH"
export COMMANDS_AVAILABLE=true
else
env_result="LINUX_MINIMAL"
export COMMANDS_AVAILABLE=false
export MISSING_COMMANDS=("${missing_commands[@]}")
fi
export DETECTED_PLATFORM="linux"
export DETECTED_SHELL="bash"
elif [[ "${OSTYPE:-}" == "darwin"* ]]; then
platform="macos"
env_result="MACOS_BASH"
export DETECTED_PLATFORM="macos"
export DETECTED_SHELL="bash"
export COMMANDS_AVAILABLE=true
else
# 未知环境,通过命令可用性判断
if command -v grep >/dev/null 2>&1 && command -v sed >/dev/null 2>&1 && command -v cut >/dev/null 2>&1; then
env_result="UNIX_COMPATIBLE"
export COMMANDS_AVAILABLE=true
else
env_result="LIMITED_SHELL"
export COMMANDS_AVAILABLE=false
fi
export DETECTED_PLATFORM="unknown"
export DETECTED_SHELL="unknown"
fi
echo "$env_result"
}
# ? Linux系统自动安装缺失命令
install_missing_commands_linux() {
local missing_commands=("$@")
echo "? 尝试自动安装缺失命令: ${missing_commands[*]}" >&2
# 检测包管理器并安装
if command -v apt-get >/dev/null 2>&1; then
echo "? 使用 apt-get 安装..." >&2
sudo apt-get update >/dev/null 2>&1
sudo apt-get install -y gawk sed grep coreutils >/dev/null 2>&1
elif command -v yum >/dev/null 2>&1; then
echo "? 使用 yum 安装..." >&2
sudo yum install -y gawk sed grep coreutils >/dev/null 2>&1
elif command -v dnf >/dev/null 2>&1; then
echo "? 使用 dnf 安装..." >&2
sudo dnf install -y gawk sed grep coreutils >/dev/null 2>&1
elif command -v pacman >/dev/null 2>&1; then
echo "? 使用 pacman 安装..." >&2
sudo pacman -S --noconfirm gawk sed grep coreutils >/dev/null 2>&1
elif command -v apk >/dev/null 2>&1; then
echo "? 使用 apk 安装..." >&2
sudo apk add gawk sed grep coreutils >/dev/null 2>&1
elif command -v zypper >/dev/null 2>&1; then
echo "? 使用 zypper 安装..." >&2
sudo zypper install -y gawk sed grep coreutils >/dev/null 2>&1
else
echo "❌ 无法识别包管理器,请手动安装: ${missing_commands[*]}" >&2
echo "" >&2
echo "? 常见安装命令:" >&2
echo " Ubuntu/Debian: sudo apt-get install gawk sed grep coreutils" >&2
echo " CentOS/RHEL: sudo yum install gawk sed grep coreutils" >&2
echo " Fedora: sudo dnf install gawk sed grep coreutils" >&2
echo " Arch Linux: sudo pacman -S gawk sed grep coreutils" >&2
echo " Alpine: sudo apk add gawk sed grep coreutils" >&2
return 1
fi
# 验证安装结果
local still_missing=()
for cmd in "${missing_commands[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
still_missing+=("$cmd")
fi
done
if [ ${#still_missing[@]} -eq 0 ]; then
echo "✅ 所有命令安装成功" >&2
return 0
else
echo "⚠️ 部分命令安装失败: ${still_missing[*]}" >&2
return 1
fi
}
# 智能命令调用:根据环境选择最佳实现
smart_grep() {
local pattern="$1"
shift
local args=("$@")
if [ "${COMMANDS_AVAILABLE:-false}" = true ] && command -v grep >/dev/null 2>&1; then
# 使用原生grep
grep "$pattern" "${args[@]}"
else
# 使用纯bash实现
pure_bash_grep "$pattern" "${args[@]}"
fi
}
smart_cut() {
local delimiter=""
local field=""
local input=""
# 解析参数
while [[ $# -gt 0 ]]; do
case "$1" in
-d) delimiter="$2"; shift 2 ;;
-f) field="$2"; shift 2 ;;
*) input="$1"; shift ;;
esac
done
if [ "${COMMANDS_AVAILABLE:-false}" = true ] && command -v cut >/dev/null 2>&1; then
# 使用原生cut
if [ -n "$input" ]; then
echo "$input" | cut -d"$delimiter" -f"$field"
else
cut -d"$delimiter" -f"$field"
fi
else
# 使用纯bash实现
pure_bash_cut -d "$delimiter" -f "$field" "$input"
fi
}
smart_sed() {
local expression="$1"
shift
if [ "${COMMANDS_AVAILABLE:-false}" = true ] && command -v sed >/dev/null 2>&1; then
# 使用原生sed
sed "$expression" "$@"
else
# 使用纯bash实现(简化版)
pure_bash_sed "$expression" "$@"
fi
}
# 纯bash实现的文本处理函数(备用)
pure_bash_grep() {
local pattern="$1"
local file="$2"
local line
if [ -f "$file" ]; then
while IFS= read -r line; do
case "$line" in
*"$pattern"*) echo "$line" ;;
esac
done < "$file"
else
# 从stdin读取
while IFS= read -r line; do
case "$line" in
*"$pattern"*) echo "$line" ;;
esac
done
fi
}
pure_bash_cut() {
local options="$1"
local input="$2"
local delimiter=" "
local field=1
# 解析cut参数
case "$options" in
*-d*)
delimiter="${options#*-d}"
if [ -z "$delimiter" ]; then
delimiter=" "
fi
;;
*-f*)
field="${options#*-f}"
;;
esac
# 处理输入
if [ -n "$input" ]; then
echo "$input" | while IFS="$delimiter" read -ra parts; do
if [ ${#parts[@]} -ge "$field" ]; then
echo "${parts[$((field-1))]}"
fi
done
else
while IFS="$delimiter" read -ra parts; do
if [ ${#parts[@]} -ge "$field" ]; then
echo "${parts[$((field-1))]}"
fi
done
fi
}
# 环境检测和初始化
SHELL_ENV=$(detect_shell_environment)
case "$SHELL_ENV" in
"WINDOWS_POWERSHELL")
echo "⚠️ 检测到 Windows PowerShell 环境" >&2
echo "? 建议使用 Git Bash 运行此脚本以获得最佳体验:" >&2
echo " 1. 安装 Git for Windows (如果未安装): https://git-scm.com/download/win" >&2
echo " 2. 右键点击文件夹,选择 'Git Bash Here'" >&2
echo " 3. 运行命令:./setup_train_env.sh" >&2
echo "" >&2
echo "? 当前环境的限制:" >&2
echo " • 缺少 grep, sed, cut 等命令" >&2
echo " • 某些功能可能较慢或不可用" >&2
echo "" >&2
read -p "❓ 是否继续在当前环境中运行?(y/N): " continue_powershell
if [[ ! "$continue_powershell" =~ ^[Yy]$ ]]; then
echo "? 请在 Git Bash 中运行脚本以获得最佳体验" >&2
exit 1
fi
echo "? 启用兼容模式继续运行..." >&2
;;
"LINUX_MINIMAL")
echo "⚠️ 检测到精简 Linux 环境,缺少必要命令" >&2
echo "? 缺失命令: ${MISSING_COMMANDS[*]}" >&2
echo "" >&2
echo "? 自动安装选项:" >&2
read -p "❓ 是否尝试自动安装缺失命令?(y/N): " install_missing
if [[ "$install_missing" =~ ^[Yy]$ ]]; then
if install_missing_commands_linux "${MISSING_COMMANDS[@]}"; then
echo "✅ 命令安装成功,重新检测环境..." >&2
SHELL_ENV=$(detect_shell_environment)
if [ "$COMMANDS_AVAILABLE" = true ]; then
echo "✅ 环境准备完成" >&2
else
echo "⚠️ 某些命令可能未正确安装,使用兼容模式" >&2
fi
else
echo "❌ 自动安装失败,使用兼容模式" >&2
fi
else
echo "? 使用内置兼容模式继续运行..." >&2
fi
;;
"LIMITED_SHELL")
echo "⚠️ 检测到受限shell环境" >&2
echo "? 建议在支持标准命令的环境中运行此脚本" >&2
echo "" >&2
read -p "❓ 是否继续在当前环境中运行?(y/N): " continue_limited
if [[ ! "$continue_limited" =~ ^[Yy]$ ]]; then
echo "? 请在支持的环境中运行脚本" >&2
exit 1
fi
echo "? 使用兼容模式继续运行..." >&2
;;
"GIT_BASH")
echo "✅ Git Bash 环境检测成功,使用完整功能" >&2
;;
"LINUX_BASH"|"MACOS_BASH"|"UNIX_COMPATIBLE")
echo "✅ Unix 兼容环境检测成功,使用完整功能" >&2
;;
*)
echo "ℹ️ 未知shell环境,尝试自动适配" >&2
;;
esac
# ========================================
# ?️ 全局变量(可从外部传入或默认)
# ========================================
# 开启调试跟踪(set -x)仅在显式需要时打开,避免把执行跟踪写入控制台/日志
# 可通过环境变量或命令行参数启用,如: DEBUG=1 bash setup_train_env.sh 或 bash setup_train_env.sh --debug
if [[ "${DEBUG:-0}" -eq 1 ]]; then
set -x
fi
# 确保终端环境正常
export TERM="${TERM:-xterm-256color}"
export PYTHONIOENCODING=utf-8
# ========================================
# ? 脚本开始:记录开始时间
# ========================================
# 使用中国上海时区时间(兼容不同环境)
export TZ='Asia/Shanghai'
# sh_date: 使用 TZ=Asia/Shanghai 调用 date,保证在不同平台下输出为上海时间
sh_date() {
# 优先使用系统时区数据(如果存在)
if [ -r "/usr/share/zoneinfo/Asia/Shanghai" ]; then
TZ='Asia/Shanghai' date "$@"
return
fi
# 回退:如果系统缺少 zoneinfo,尝试使用 python/python3 将 UTC 时间 +8h 来格式化
local args=("$@")
local fmt=""
local epoch=""
local i
for ((i=0;i<${#args[@]};i++)); do
case "${args[i]}" in
-d)
# 支持 -d @EPOCH 或 -d @ EPOCH 两种形式
if [ $((i+1)) -lt ${#args[@]} ]; then
val="${args[i+1]}"
if [ "${val}" = "@" ] && [ $((i+2)) -lt ${#args[@]} ]; then
epoch="@${args[i+2]}"
i=$((i+2))
else
epoch="${val}"
i=$((i+1))
fi
fi
;;
@*)
epoch="${args[i]}"
;;
+*)
fmt="${args[i]}"
;;
*)
;;
esac
done
if [ -z "$fmt" ]; then
fmt='+%Y-%m-%d %H:%M:%S'
fi
local pyfmt="${fmt#+}"
# 选择 python 可执行文件
local pycmd=python
if ! command -v "$pycmd" >/dev/null 2>&1; then
pycmd=python3
fi
if ! command -v "$pycmd" >/dev/null 2>&1; then
# 无 python 可用,回退到系统 date(使用 sh_date 回退到上海时间或尽力保持一致)
sh_date "$@"
return
fi
if [ -n "$epoch" ]; then
epoch="${epoch#@}"
if [ -z "$epoch" ]; then
# epoch 为空,回退到 sh_date(避免 python int('') 错误)
sh_date "$@"
return
fi
"$pycmd" - "$pyfmt" "$epoch" <<'PY'
import sys,datetime
try:
fmt = sys.argv[1]
epoch = int(sys.argv[2])
# 使用时区感知的 UTC 时间并转换为上海时区(UTC+8)
dt = datetime.datetime.fromtimestamp(epoch, datetime.timezone.utc).astimezone(datetime.timezone(datetime.timedelta(hours=8)))
print(dt.strftime(fmt))
except Exception:
pass
PY
else
"$pycmd" - "$pyfmt" <<'PY'
import sys,datetime
try:
fmt = sys.argv[1]
dt = datetime.datetime.now(datetime.timezone.utc).astimezone(datetime.timezone(datetime.timedelta(hours=8)))
print(dt.strftime(fmt))
except Exception:
pass
PY
fi
}
# 记录脚本开始时间(epoch)
START_TIME=$(sh_date +%s)
# 在函数顶部添加:
local extra_index_url=""
local trusted_hosts=()
# 在参数解析循环中:
for arg in "$@"; do
if [[ "$arg" == --index-url ]]; then
index_url_next=true
elif [[ "$arg" == --extra-index-url ]]; then
extra_index_url_next=true
elif [[ "$arg" == --trusted-host ]]; then
trusted_host_next=true
elif $index_url_next; then
index_url="$arg"
index_url_next=false
elif $extra_index_url_next; then
extra_index_url="$arg"
extra_index_url_next=false
elif $trusted_host_next; then
trusted_hosts+=("$arg")
trusted_host_next=false
elif [[ "$arg" == --* ]]; then
extra_args+=("$arg")
else
packages+=("$arg")
fi
done
# ========================================
# ?️ 全局变量(可从外部传入或默认)
# ========================================
ENV_NAME="${ENV_NAME:-train_ss}" # 虚拟环境名称
WORKSPACE="${WORKSPACE:-/mnt/workspace}" # 工作空间根目录(可外部传入)
# --- 修改 2: PLATFORM 优先外部传入 ---
PLATFORM="${PLATFORM:-auto}" # 平台类型
if [[ "$PLATFORM" == "auto" ]]; then
PLATFORM=$(detect_platform)
fi
# ========================================
# ?️ 跨平台虚拟环境检测函数
# ========================================
check_virtual_env_support() {
local has_venv=false
local has_virtualenv=false
local has_conda=false
local python_cmd=""
echo "? 检测虚拟环境支持..."
# 1. 检测 Python 命令
if command -v python3 >/dev/null 2>&1; then
python_cmd="python3"
elif command -v python >/dev/null 2>&1; then
python_cmd="python"
fi
if [ -z "$python_cmd" ]; then
echo "❌ 未检测到 Python"
return 1
fi
echo "✅ 检测到 Python: $python_cmd ($($python_cmd --version 2>&1))"
# 2. 检测 venv 模块(Python 3.3+ 内置)
if $python_cmd -m venv --help >/dev/null 2>&1; then
has_venv=true
echo "✅ 支持 venv 模块"
else
echo "❌ 不支持 venv 模块"
fi
# 3. 检测 virtualenv(第三方包)
if command -v virtualenv >/dev/null 2>&1; then
has_virtualenv=true
echo "✅ 支持 virtualenv: $(virtualenv --version 2>&1)"
elif $python_cmd -m virtualenv --help >/dev/null 2>&1; then
has_virtualenv=true
echo "✅ 支持 virtualenv (通过 python -m)"
else
echo "❌ 不支持 virtualenv"
fi
# 4. 检测 conda
if command -v conda >/dev/null 2>&1; then
has_conda=true
echo "✅ 支持 conda: $(conda --version 2>&1)"
else
echo "❌ 不支持 conda"
fi
# 5. 总结
echo ""
echo "? 虚拟环境支持总结:"
if $has_conda; then
echo " ? Conda: 可用 (推荐)"
fi
if $has_venv; then
echo " ? venv: 可用 (Python 内置)"
fi
if $has_virtualenv; then
echo " ? virtualenv: 可用 (第三方)"
fi
if ! $has_venv && ! $has_virtualenv && ! $has_conda; then
echo " ❌ 无可用的虚拟环境工具"
echo ""
echo "? 建议安装方法:"
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo " Ubuntu/Debian: sudo apt update && sudo apt install python3-venv"
echo " CentOS/RHEL: sudo yum install python3-venv 或 sudo dnf install python3-venv"
elif [[ "$OSTYPE" == "msys"* ]] || [[ "$OSTYPE" == "cygwin"* ]]; then
echo " Windows: pip install virtualenv 或安装 Anaconda/Miniconda"
fi
echo " 通用方法: pip install virtualenv"
return 1
fi
return 0
}
# ========================================
# ?️ 封装函数:get_activate_cmd 获取环境激活cmd命令
# ========================================
get_activate_cmd() {
local ENV_NAME="${1:-train_ss}"
local VENV_PATH="${2:-/mnt/workspace/envs/${ENV_NAME}}"
local ACTIVATE_CMD=""
local ENV_TYPE=""
local OS_TYPE="linux"
# 1. 判断操作系统
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then
OS_TYPE="windows"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS_TYPE="linux"
else
OS_TYPE="linux"
fi
echo "? 检测到操作系统: $OS_TYPE"
# 2. 判断环境类型
if command -v conda &> /dev/null; then
if conda env list 2>/dev/null | smart_grep -q "^\s*${ENV_NAME}\s*" ; then
ENV_TYPE="conda"
echo "? 检测到 Conda 环境: ${ENV_NAME}"
fi
fi
if [[ -z "$ENV_TYPE" ]] && [ -f "${VENV_PATH}/bin/activate" ]; then
ENV_TYPE="venv"
echo "? 检测到 venv 环境: ${VENV_PATH}"
fi
if [[ -z "$ENV_TYPE" ]]; then
echo "❌ 错误:未找到环境 '${ENV_NAME}',既不是 conda 也不是 venv。"
export ACTIVATE_CMD="NOT_FOUND"
export DETECTED_ENV_TYPE="unknown"
return 1
fi
# 3. 生成激活命令
case "${ENV_TYPE}_${OS_TYPE}" in
"conda_linux"|"conda_windows")
if conda --version &> /dev/null; then
local CONDA_VERSION=$(conda --version | smart_extract_field 2)
if version_compare "4.4" "$CONDA_VERSION"; then
ACTIVATE_CMD="conda activate ${ENV_NAME}"
else
ACTIVATE_CMD="source activate ${ENV_NAME}"
fi
else
ACTIVATE_CMD="source activate ${ENV_NAME}"
fi
;;
"venv_linux")
ACTIVATE_CMD="source ${VENV_PATH}/bin/activate"
;;
"venv_windows")
ACTIVATE_CMD="${VENV_PATH}\\Scripts\\activate.bat"
;;
*)
ACTIVATE_CMD="UNKNOWN"
;;
esac
# 导出变量供外部使用
export ACTIVATE_CMD
export DETECTED_ENV_TYPE="$ENV_TYPE"
echo "✅ ACTIVATE_CMD: ${ACTIVATE_CMD}"
}
# ========================================
# ?️ 定义 log_success 函数
# ========================================
log_success() {
local PLATFORM="$1"
local ENV_NAME="$2"
local LOG_FILE_PATH="$3"
local WORKSPACE="$4"
local ACTIVATE_CMD="$5"
local DETECTED_ENV_TYPE="$6"
# 获取 GPU 信息
local GPU_INFO
GPU_INFO=$(python -c '
import torch
if torch.cuda.is_available():
print(f"✅ {torch.cuda.get_device_name(0)} (CUDA {torch.version.cuda})")
else:
print("❌ 未启用")
' 2>/dev/null) || GPU_INFO="❌ 导入 PyTorch 失败"
local CURRENT_TIME=$(sh_date '+%Y-%m-%d %H:%M:%S %Z')
local END_TIME=$(sh_date +%s)
local ELAPSED_TIME=$((END_TIME - START_TIME))
local DURATION="$((ELAPSED_TIME / 60))m$((ELAPSED_TIME % 60))s"
# ? 1. 输出到终端(彩色,仅 stderr)
{
echo
echo -e "✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨"
echo -e "? \033[1;33m环境配置成功!All Set!\033[0m ?"
echo
echo -e "? 平台: \033[1;36m$PLATFORM\033[0m"
echo -e "? 环境: \033[1;32m$ENV_NAME\033[0m"
echo -e "? 环境类型: \033[1;35m${DETECTED_ENV_TYPE^}\033[0m" # 首字母大写
echo -e "? 日志: \033[3;37m$LOG_FILE\033[0m"
echo
echo -e "? 激活命令: \033[1;33m$ACTIVATE_CMD\033[0m" # ✅ 使用动态命令
echo -e "? GPU 支持: \033[1;32m$GPU_INFO\033[0m"
echo
echo -e "✅ 当前环境已就绪,可直接运行训练脚本。"
echo -e "? 完成时间: $CURRENT_TIME"
echo -e "⏱️ 执行耗时: $DURATION (开始: $(sh_date -d "@${START_TIME}" '+%H:%M:%S'))"
echo -e ""
echo -e "支持的 CLI 参数(可直接用于命令行)"
echo -e "--help, -h 说明:显示中文帮助(直接输出到终端,不写入日志),随后脚本退出。"
echo -e "用法示例:bash setup_train_env.sh --help 或./setup_train_env.sh --help"
echo -e "--debug, -d 说明:启用调试模式(等同于设置 DEBUG=1),脚本会执行 set -x 输出命令跟踪(用于排查)。"
echo -e "用法示例:bash setup_train_env.sh --debug 或./setup_train_env.sh --debug "
echo -e "✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨"
echo
} >&2
# ? 2. 写入日志(纯文本,结构化)
{
echo "----------------------------------------"
echo "✅ ENV_SETUP_SUCCESS"
echo "PLATFORM: $PLATFORM"
echo "ENV_NAME: $ENV_NAME"
echo "ENV_TYPE: $DETECTED_ENV_TYPE"
echo "LOG_FILE: $LOG_FILE"
echo "ACTIVATE_CMD: $ACTIVATE_CMD"
echo "GPU_INFO: $GPU_INFO"
echo "START_TIME: $(sh_date -d "@${START_TIME}" '+%Y-%m-%d %H:%M:%S')"
echo "COMPLETION_TIME: $CURRENT_TIME"
echo "ELAPSED_SECONDS: $ELAPSED_TIME"
echo "EXECUTION_DURATION: $DURATION"
echo "STATUS: OK"
echo "----------------------------------------"
} >> "$LOG_FILE" 2>&1
}
# 函数:记录带时间戳的日志
log() {
echo "[$(sh_date '+%Y-%m-%d %H:%M:%S %Z')] $1" | tee -a "$LOG_FILE"
}
# 错误处理函数
on_error() {
local exit_code=$?
cat >&2 << EOF
? 脚本执行失败!
行号: $1
命令: $2
退出码: $exit_code
日志: ${LOG_FILE:-未定义}
? 建议:
- 检查 conda 是否可用: conda --version
- 检查 D:/logs 是否可写
- 手动运行失败命令
EOF
exit "$exit_code"
}
trap 'on_error $LINENO "$BASH_COMMAND"' ERR
# 工具函数
info() { echo "ℹ️ $1"; }
warn() { echo "⚠️ $1"; }
# 增强的错误函数:自动输出详细错误信息并保存到日志
error() {
local error_msg="$*"
local error_time=$(sh_date '+%Y-%m-%d %H:%M:%S %Z')
local error_details=""
# 构建详细错误信息
error_details="
====================================
❌ 错误发生时间: $error_time
❌ 错误信息: $error_msg
❌ 错误位置: ${BASH_SOURCE[1]:-未知}:${BASH_LINENO[0]:-未知}
❌ 当前工作目录: $(pwd)
❌ 环境信息:
- PLATFORM: ${PLATFORM:-未设置}
- ENV_NAME: ${ENV_NAME:-未设置}
- WORKSPACE: ${WORKSPACE:-未设置}
- Python可用性: $(command -v python >/dev/null 2>&1 && echo "python: $(python --version 2>&1)" || echo "python: 不可用")$(command -v python3 >/dev/null 2>&1 && echo " | python3: $(python3 --version 2>&1)" || echo " | python3: 不可用")
- Conda可用性: $(command -v conda >/dev/null 2>&1 && echo "conda: $(conda --version 2>&1)" || echo "conda: 不可用")
- 当前激活环境: ${CONDA_DEFAULT_ENV:-无}
====================================
"
# 输出到控制台
echo "$error_details" >&2
# 保存到日志文件(如果已设置)
if [[ -n "${LOG_FILE:-}" && -w "$(dirname "$LOG_FILE")" ]]; then
echo "$error_details" >> "$LOG_FILE" 2>/dev/null || true
fi
exit 1
}
# 增强的错误追踪函数:用于关键操作失败时的详细诊断
trace_error() {
local operation="$1"
local cmd_output="$2"
local exit_code="${3:-$?}"
local error_time=$(sh_date '+%Y-%m-%d %H:%M:%S %Z')
local trace_details="
? ========== 错误追踪报告 ==========
⏰ 时间: $error_time
? 操作: $operation
? 退出码: $exit_code
? 位置: ${BASH_SOURCE[1]:-未知}:${BASH_LINENO[0]:-未知}
? 工作目录: $(pwd)
? 命令输出:
$cmd_output
? 环境诊断:
- PLATFORM: ${PLATFORM:-未设置}
- ENV_NAME: ${ENV_NAME:-未设置}
- WORKSPACE: ${WORKSPACE:-未设置}
- LOG_FILE: ${LOG_FILE:-未设置}
? Python 环境:
- which python: $(which python 2>/dev/null || echo "未找到")
- which python3: $(which python3 2>/dev/null || echo "未找到")
- PYTHONPATH: ${PYTHONPATH:-未设置}
?️ Anaconda/Miniconda:
- which conda: $(which conda 2>/dev/null || echo "未找到")
- CONDA_DEFAULT_ENV: ${CONDA_DEFAULT_ENV:-未设置}
- CONDA_PREFIX: ${CONDA_PREFIX:-未设置}
? 磁盘空间:
$(df -h . 2>/dev/null | tail -1 || echo "无法获取磁盘信息")
? 网络连接测试:
$(ping -c 1 8.8.8.8 >/dev/null 2>&1 && echo "✅ 网络连接正常" || echo "❌ 网络连接异常")
==================================================
"
# 输出到控制台
echo "$trace_details" >&2
# 保存到日志文件
if [[ -n "${LOG_FILE:-}" ]]; then
echo "$trace_details" >> "$LOG_FILE" 2>/dev/null || true
fi
}
ok() { echo "✅ $1"; }
# 平台检测 + 变量初始化
# === 在变量定义后,立即设置日志并重定向 ===
# 先通过平台检测确定 LOG_DIR
detect_platform() {
case "$OSTYPE" in
msys*|mingw*|cygwin*)
echo "local-windows"
return
;;
esac
[[ -d "/home/aistudio" ]] && echo "aistudio" && return
[[ -n "${MODELSCOPE_CACHE:-}" ]] && echo "modelscope" && return
[[ -n "${SSH_CLIENT:-}" ]] && echo "server" && return
[[ -d "/mnt/workspace" ]] && echo "server" && return
echo "local-linux"
}
setup_paths() {
case "$PLATFORM" in
"local-windows")
DEFAULT_DRIVE="D:/"
if [[ -w "$DEFAULT_DRIVE" ]]; then
WORKSPACE="$DEFAULT_DRIVE/workspace"
LOG_DIR="$DEFAULT_DRIVE/logs"
else
WORKSPACE="./workspace"
LOG_DIR="./logs"
fi
;;
"aistudio")
WORKSPACE="/home/aistudio"
LOG_DIR="/home/aistudio/logs"
;;
"modelscope")
WORKSPACE="/mnt/workspace"
LOG_DIR="/mnt/workspace/logs"
;;
"server"|"local-linux")
WORKSPACE="./workspace"
LOG_DIR="./logs"
;;
*)
warn "未知平台: $PLATFORM,使用默认配置"
WORKSPACE="./workspace"
LOG_DIR="./logs"
;;
esac
}
# ? 检测平台
PLATFORM=$(detect_platform)
info "? 检测到平台: $PLATFORM"
# ? 设置路径
setup_paths
# ✅ 创建目录
mkdir -p "$WORKSPACE" || {
error "无法创建 WORKSPACE: $WORKSPACE"
error "? 请检查该路径是否存在且有写权限"
}
mkdir -p "$LOG_DIR" || {
error "无法创建日志目录: $LOG_DIR"
error "? 请检查该路径是否存在且有写权限"
}
# ✅ 定义日志文件(使用上海时区时间)
LOG_FILE="$LOG_DIR/setup_env_$(sh_date +%Y%m%d_%H%M%S).log"
# 规范化路径工具:将连续的多个 '/' 合并为一个(保留 UNC 前缀 '//')
normalize_path() {
local p="$1"
# 针对 Windows 下(MSYS/Cygwin)路径做专门处理
if [[ "${PLATFORM:-}" == "local-windows" || "$p" =~ ^[A-Za-z]: ]]; then
# 将反斜杠转换为正斜杠
local out="${p//\\//}"
# 把连续多个 '/' 合并为一个
while [[ "$out" == *//* ]]; do out="${out//\/\//\/}"; done
# 确保驱动器后为单个斜杠,例如 D:/logs 而非 D:logs 或 D://logs
if [[ "$out" =~ ^([A-Za-z]):/*(.*) ]]; then
local drive="${BASH_REMATCH[1]}"
local rest="${BASH_REMATCH[2]}"
printf '%s' "${drive}:/${rest}"
return
fi
printf '%s' "$out"
return
fi
# 默认行为:合并重复斜杠(保留 UNC 前缀 '//')
if [[ "$p" == '//*' ]]; then
local lead='//'
local rest="${p#//}"
while [[ "$rest" == *//* ]]; do rest="${rest//\/\//\/}"; done
printf '%s' "$lead$rest"
else
local out="$p"
while [[ "$out" == *//* ]]; do out="${out//\/\//\/}"; done
printf '%s' "$out"
fi
}
# ? 智能包检查:效率优先,准确保障
#
# � 验证策略分析:
# - pip show 未找到 → 99.9% 准确(直接返回未安装)
# - pip show 找到 + 关键包 → 需要 import 验证(torch, numpy等)
# - pip show 找到 + 大型包 → 检查 Location 存在性(轻量级)
# - pip show 找到 + 普通包 → 直接信任(高效)
#
# ? 性能优化:
# - 90% 包只用 pip show(1-2ms)
# - 5% 包加 Location 检查(+1ms)
# - 5% 关键包用 import 验证(+50ms)
check_package_installed() {
local package="$1"
local package_name=""
# 提取包名(去除版本号和其他修饰符)
if [[ "$package" =~ ^([a-zA-Z0-9_-]+) ]]; then
package_name="${BASH_REMATCH[1]}"
else
package_name="$package"
fi
# 特殊处理一些包名映射
case "$package_name" in
"torch")
package_name="torch"
# 对于torch,需要特别检查是否为GPU版本
if pip show "$package_name" >/dev/null 2>&1; then
# 检查是否为GPU版本
local torch_gpu_check=""
torch_gpu_check=$(python -c "
try:
import torch
version = torch.__version__
is_gpu = '+cu' in version or torch.cuda.is_available()
# 检查CUDA版本兼容性
if is_gpu and '+cu' in version:
# 提取CUDA版本号 (如 +cu118, +cu128)
cuda_parts = version.split('+cu')
if len(cuda_parts) > 1:
cuda_version = cuda_parts[1]
print('INSTALLED_CUDA_' + cuda_version)
else:
print('GPU_VERSION_OK')
elif is_gpu:
print('GPU_VERSION_OK')
else:
print('CPU_VERSION_ON***')
except:
print('IMPORT_FAILED')
" 2>/dev/null)
# 检查是否需要CUDA版本升级
if echo "$torch_gpu_check" | smart_grep -q "GPU_VERSION_OK"; then
return 0 # GPU版本已安装且CUDA可用
elif echo "$torch_gpu_check" | smart_grep -q "INSTALLED_CUDA_"; then
# 检查CUDA版本是否匹配当前环境需求
local installed_cuda=$(echo "$torch_gpu_check" | smart_sed 's/INSTALLED_CUDA_//')
local required_cuda=""
# 从TORCH_INDEX提取需求的CUDA版本
if echo "$TORCH_INDEX" | smart_grep -q "cu118"; then
required_cuda="118"
elif echo "$TORCH_INDEX" | smart_grep -q "cu128"; then
required_cuda="128"
elif echo "$TORCH_INDEX" | smart_grep -q "cu117"; then
required_cuda="117"
fi
if [ "$installed_cuda" = "$required_cuda" ]; then
return 0 # CUDA版本匹配,无需重装
else
info "? 检测到CUDA版本不匹配: 已安装cu$installed_cuda, 需要cu$required_cuda"
return 1 # 需要升级CUDA版本
fi
elif echo "$torch_gpu_check" | smart_grep -q "CPU_VERSION_ON***"; then
# CPU版本已安装,但我们需要GPU版本
return 1 # 需要重新安装GPU版本
else
return 1 # 安装有问题,需要重新安装
fi
else
return 1 # 未安装
fi
;;
"torchvision")
package_name="torchvision"
;;
"torchaudio")
package_name="torchaudio"
;;
"opencv-python"|"opencv-contrib-python")
package_name="cv2"
;;
"pillow")
package_name="PIL"
;;
"scikit-learn")
package_name="sklearn"
;;
"beautifulsoup4")
package_name="bs4"
;;
"pyyaml")
package_name="yaml"
;;
esac
# ? 高效的包检查策略:平衡效率和准确性
# Step 1: 快速检查 - pip show (高效,覆盖99%的情况)
local pip_show_result=""
pip_show_result=$(pip show "$package_name" 2>/dev/null)
local pip_show_exit_code=$?
# 如果 pip show 显示未安装,直接返回未安装(这个结果是可靠的)
if [ $pip_show_exit_code -ne 0 ]; then
return 1 # 未安装,pip show未找到包
fi
# Step 2: pip show 显示已安装,但需要额外验证的情况
local need_import_check=false
# 检查是否需要额外验证(仅在这些情况下)
case "$package_name" in
# 关键包:安装错误代价高,需要双重验证
torch|tensorflow|numpy|scipy)
need_import_check=true
;;
# 大型包:安装时间长,需要确保真正可用
torchvision|torchaudio|opencv*|pillow)
# 检查包的Location是否存在(轻量级验证)
local package_location=""
package_location=$(echo "$pip_show_result" | smart_grep "Location:" | smart_cut -d' ' -f2)
if [ -n "$package_location" ] && [ -d "$package_location" ]; then
need_import_check=false # 位置存在,trust pip show
else
need_import_check=true # 位置不存在,需要import验证
fi
;;
# 普通包:pip show结果可信,不需要额外验证
*)
need_import_check=false
;;
esac
# Step 3: 仅在必要时进行 import 验证(保持高效)
if [ "$need_import_check" = true ]; then
# 对关键包进行实际导入测试
if python -c "import $package_name" >/dev/null 2>&1; then
return 0 # 双重验证通过,确实已安装
else
return 1 # pip show说已安装,但实际导入失败(包损坏)
fi
else
# pip show 结果可信,直接信任
return 0 # 已安装
fi
}
# ? 智能包安装函数:过滤已安装的包
filter_installed_packages() {
local packages=("$@")
local uninstalled_packages=()
local installed_packages=()
# 将信息输出重定向到stderr,避免污染返回值
echo "? 正在检查包安装状态..." >&2
for package in "${packages[@]}"; do
if check_package_installed "$package"; then
installed_packages+=("$package")
echo "ℹ️ ✅ $package - 已安装,跳过" >&2
else
uninstalled_packages+=("$package")
echo "ℹ️ ? $package - 需要安装" >&2
fi
done
# 显示统计信息 - 重定向到stderr
if [ ${#installed_packages[@]} -gt 0 ]; then
echo "ℹ️ ? 跳过已安装的包 (${#installed_packages[@]}个): ${installed_packages[*]}" >&2
fi
if [ ${#uninstalled_packages[@]} -gt 0 ]; then
echo "ℹ️ ⬇️ 需要安装的包 (${#uninstalled_packages[@]}个): ${uninstalled_packages[*]}" >&2
else
echo "ℹ️ ? 所有包都已安装,无需下载!" >&2
fi
# 返回未安装的包列表(仅包名,无额外输出)
for pkg in "${uninstalled_packages[@]}"; do
echo "$pkg"
done
}
# 规范化 LOG_DIR 和 LOG_FILE,避免出现 D://logs 这样的路径
LOG_DIR=$(normalize_path "$LOG_DIR")
LOG_FILE=$(normalize_path "$LOG_FILE")
# ✅ 开始重定向(从现在开始,所有输出都进日志)
# 优先使用 tee 将输出同时写到日志和控制台(需要 bash 的 process substitution)
if command -v tee >/dev/null 2>&1; then
# 将 stdout/stderr 都通过 tee 追加到日志文件,并继续在控制台显示
if ! exec > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2); then
echo "❌ 无法通过 tee 重定向到日志文件: $LOG_FILE" >&2
exit 1
fi
else
# 如果系统没有 tee,提醒用户并保持在控制台输出(推荐:安装 tee 或使用外部管道运行脚本)
cat >&2 <<'EOF'
⚠️ 未检测到系统命令 "tee"。
为保证同时在控制台显示并写入日志,建议执行以下任一操作:
1) 在系统上安装 tee(Ubuntu/Debian: sudo apt update && sudo apt install coreutils -y)。
2) 或在运行脚本时使用外部管道:
bash setup_train_env.sh 2>&1 | tee -a "${LOG_FILE}"
脚本将继续在控制台输出(不再自动把所有输出重定向到日志文件),
如果你选择使用外部管道运行,日志会被完整保存到上面的文件。
EOF
# 不执行 exec 重定向,保留控制台输出,按用户要求不自动改为 FIFO 回退
fi
# ? 此后所有输出都记录在日志中
echo "----------------------------------------"
info "✅ 日志重定向已生效"
info "✅ 日志文件已创建: $LOG_FILE"
info "? 开始执行后续安装步骤..."
echo "----------------------------------------"
log "安装脚本开始执行"
# ? 检测虚拟环境支持情况
echo "----------------------------------------"
check_virtual_env_support
if [ $? -ne 0 ]; then
warn "⚠️ 虚拟环境支持不完整,脚本可能无法正常创建环境"
warn "? 建议先安装虚拟环境工具再继续"
fi
echo "----------------------------------------"
PYTHON_VERSIONS=("3.10.13" "3.9" "3.8" "3.7")
# -----------------------------
# ? 检查 Conda 是否可用,否则使用 venv
# -----------------------------
create_or_use_env() {
local env_name=$1
local py_versions=("${@:2}")
# 定义统一路径(用于 venv 和 Linux 上的 Conda)
ENV_ROOT="$WORKSPACE/envs"
ENV_PATH="$ENV_ROOT/$env_name"
# -----------------------------
# 1. 检查 Conda 是否可用
# -----------------------------
if command -v conda &> /dev/null; then
# 判断是否是 Linux 系统
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
info "✅ Linux + Conda:使用统一路径 $ENV_PATH"
# 检查指定路径的环境是否存在且完整
if [ -d "$ENV_PATH" ] && [ -f "$ENV_PATH/bin/python" ]; then
ok "Conda 环境已存在: $ENV_PATH"
else
# 清理残缺环境
if [ -d "$ENV_PATH" ]; then
warn "⚠️ 环境目录不完整,正在清理: $ENV_PATH"
rm -rf "$ENV_PATH"
fi
info "? 创建 Conda 环境: $ENV_PATH"
local creation_output=""
local creation_failed=false
for py_ver in "${py_versions[@]}"; do
creation_output=$(conda create -p "$ENV_PATH" python="$py_ver" --yes -v 2>&1)
if [ $? -eq 0 ]; then
ok "✅ Conda 环境创建成功"
creation_failed=false
break
else
creation_failed=true
fi
done
if [ "$creation_failed" = true ] || [ ! -f "$ENV_PATH/bin/python" ]; then
trace_error "Conda 环境创建失败" "$creation_output"
error "Conda 环境创建失败: $ENV_PATH"
exit 1
fi
fi
# 激活 Conda 环境(使用完整路径)
local activation_output=""
eval "$(conda shell.bash hook)" 2>/dev/null || true
activation_output=$(conda activate "$ENV_PATH" 2>&1)
local activation_exit_code=$?
if [ $activation_exit_code -ne 0 ]; then
trace_error "Conda 环境激活失败" "$activation_output" "$activation_exit_code"
error "Conda 环境激活失败: $ENV_PATH"
fi
return $activation_exit_code
else
# ✅ 非 Linux(如 Windows),使用 Conda 默认路径
info "✅ Conda 可用(非 Linux),使用默认路径: $env_name"
if conda env list | smart_grep -E "^$env_name[[:space:]]" > /dev/null; then
ok "Conda 环境已存在: $env_name"
else
info "? 创建 Conda 环境: $env_name"
local conda_creation_output=""
conda_creation_output=$(conda create -n "$env_name" python="${py_versions[0]}" --yes 2>&1)
local conda_creation_exit_code=$?
if [ $conda_creation_exit_code -ne 0 ]; then
trace_error "Conda 环境创建失败 (非Linux)" "$conda_creation_output" "$conda_creation_exit_code"
error "Conda 环境创建失败: $env_name"
fi
fi
local conda_activation_output=""
eval "$(conda shell.bash hook)" 2>/dev/null || true
conda_activation_output=$(conda activate "$env_name" 2>&1)
local conda_activation_exit_code=$?
if [ $conda_activation_exit_code -ne 0 ]; then
trace_error "Conda 环境激活失败 (非Linux)" "$conda_activation_output" "$conda_activation_exit_code"
error "Conda 环境激活失败: $env_name"
fi
return $conda_activation_exit_code
fi
# -----------------------------
# 2. Conda 不可用,使用 venv(Linux 和 Windows 都用统一路径)
# -----------------------------
else
info "⚠️ Conda 未安装,使用 venv: $ENV_PATH"
if [ -d "$ENV_PATH" ]; then
if [ -f "$ENV_PATH/bin/activate" ] && [ -f "$ENV_PATH/bin/python" ]; then
ok "✅ venv 环境已存在且完整"
else
warn "⚠️ venv 不完整,正在清理"
rm -rf "$ENV_PATH"
fi
fi
if [ ! -d "$ENV_PATH" ]; then
mkdir -p "$ENV_ROOT"
local success=false
local venv_creation_output=""
for py_ver in "${py_versions[@]}"; do
if command -v "python$py_ver" &> /dev/null; then
venv_creation_output=$(python"$py_ver" -m venv "$ENV_PATH" 2>&1)
if [ $? -eq 0 ]; then
success=true
break
fi
elif command -v "python${py_ver%.*}" &> /dev/null; then
venv_creation_output=$(python"${py_ver%.*}" -m venv "$ENV_PATH" 2>&1)
if [ $? -eq 0 ]; then
success=true
break
fi
fi
done
if [ "$success" = false ]; then
trace_error "标准 venv 创建失败" "$venv_creation_output"
info "⚠️ 标准 venv 创建失败,使用 --without-pip + 手动安装 pip"
local without_pip_output=""
without_pip_output=$(python3 -m venv "$ENV_PATH" --without-pip 2>&1)
local without_pip_exit_code=$?
if [ $without_pip_exit_code -ne 0 ]; then
trace_error "venv --without-pip 创建失败" "$without_pip_output" "$without_pip_exit_code"
error "❌ 虚拟环境创建完全失败:$ENV_PATH"
fi
if [ -f "$ENV_PATH/bin/python" ]; then
info "? 正在手动安装 pip..."
# ✅ 使用清华镜像,避免网络问题
GET_PIP_URL="https://pypi.tuna.tsinghua.edu.cn/bootstrap/pip/get-pip.py"
GET_PIP_PY="/tmp/get-pip-$$.py"
# ✅ 添加超时,防止卡死
local curl_output=""
curl_output=$(curl -fsSL --connect-timeout 10 --max-time 30 "$GET_PIP_URL" -o "$GET_PIP_PY" 2>&1)
local curl_exit_code=$?
if [ $curl_exit_code -eq 0 ]; then
local pip_install_output=""
pip_install_output=$("$ENV_PATH/bin/python" "$GET_PIP_PY" --quiet --no-warn-script-location 2>&1)
local pip_exit=$?
rm -f "$GET_PIP_PY"
if [ $pip_exit -eq 0 ]; then
ok "✅ pip 安装成功"
else
trace_error "pip 手动安装失败" "$pip_install_output" "$pip_exit"
error "❌ pip 安装失败"
exit 1
fi
else
trace_error "下载 get-pip.py 失败" "$curl_output" "$curl_exit_code"
error "❌ 下载 get-pip.py 失败(URL: $GET_PIP_URL)"
error "? 建议检查网络或更换镜像源"
exit 1
fi
else
trace_error "虚拟环境创建失败" "无法创建 $ENV_PATH/bin/python"
error "❌ 虚拟环境创建失败:$ENV_PATH/bin/python 不存在"
exit 1
fi
fi
fi
# 激活 venv
if [ -f "$ENV_PATH/bin/activate" ]; then
local venv_activation_output=""
venv_activation_output=$(source "$ENV_PATH/bin/activate" 2>&1)
local venv_activation_exit_code=$?
if [ $venv_activation_exit_code -eq 0 ]; then
ok "✅ venv 环境激活成功: $ENV_PATH"
else
trace_error "venv 环境激活失败" "$venv_activation_output" "$venv_activation_exit_code"
error "❌ venv 激活失败: $ENV_PATH/bin/activate 存在但激活失败"
exit 1
fi
else
trace_error "venv 激活文件缺失" "activate 脚本不存在: $ENV_PATH/bin/activate"
error "❌ venv 激活失败: $ENV_PATH/bin/activate 不存在"
exit 1
fi
fi
}
# -----------------------------
# 多源容错安装函数
# -----------------------------
# robust_pip_install()调用举例
# # 安装普通包
# robust_pip_install "requests" "flask"
# # 安装 PyTorch(带 extra-index-url)
# robust_pip_install "torch" "torchvision" \
# --index-url "https://download.pytorch.org/whl/cu118" \
# --extra-index-url "https://pypi.org/simple" \
# --trusted-host "pypi.org"
# # 安装私有包
# robust_pip_install "mypackage" \
# --index-url "https://private-pypi.example.com/simple" \
# --trusted-host "private-pypi.example.com"
robust_pip_install() {
local packages=()
local extra_args=()
local index_url=""
local extra_index_url=""
local trusted_hosts=()
local index_url_next=false
local extra_index_url_next=false
local trusted_host_next=false
# 解析参数
for arg in "$@"; do
case "$arg" in
--index-url)
index_url_next=true
;;
--extra-index-url)
extra_index_url_next=true
;;
--trusted-host)
trusted_host_next=true
;;
--*)
if $index_url_next; then
index_url="$arg"
index_url_next=false
elif $extra_index_url_next; then
extra_index_url="$arg"
extra_index_url_next=false
elif $trusted_host_next; then
trusted_hosts+=("$arg")
trusted_host_next=false
else
extra_args+=("$arg")
fi
;;
*)
if $index_url_next; then
index_url="$arg"
index_url_next=false
elif $extra_index_url_next; then
extra_index_url="$arg"
extra_index_url_next=false
elif $trusted_host_next; then
trusted_hosts+=("$arg")
trusted_host_next=false
else
packages+=("$arg")
fi
;;
esac
done
# 如果没传包,直接返回
if [ ${#packages[@]} -eq 0 ]; then
warn "⚠️ robust_pip_install: 未指定包"
return 1
fi
# ? 智能过滤:检查已安装的包,避免重复下载
local filtered_packages=()
# 使用子shell确保输出分离,并安静地获取过滤结果
{
info "? 正在检查包安装状态以避免重复下载..."
readarray -t filtered_packages < <(filter_installed_packages "${packages[@]}")
}
# 如果所有包都已安装,直接返回成功
if [ ${#filtered_packages[@]} -eq 0 ]; then
ok "? 所有包都已安装,跳过下载!"
return 0
fi
# 更新包列表为需要安装的包
packages=("${filtered_packages[@]}")
info "? 将安装 ${#packages[@]} 个包: ${packages[*]}"
local max_retries=3
local total_attempts=0
local max_total_attempts=6
local mirrors=(
"https://pypi.tuna.tsinghua.edu.cn/simple/|pypi.tuna.tsinghua.edu.cn"
"https://mirrors.aliyun.com/pypi/simple/|mirrors.aliyun.com"
"https://pypi.douban.com/simple/|pypi.douban.com"
"https://download.pytorch.org/whl/cu118|download.pytorch.org"
"https://pypi.org/simple|pypi.org"
)
info "? 开始安装包: ${packages[*]}"
# 快速检测网络连通性
info "? 检测网络连接..."
local network_ok=false
for test_host in "pypi.tuna.tsinghua.edu.cn" "mirrors.aliyun.com" "pypi.org"; do
if ping -c 1 -W 3 "$test_host" >/dev/null 2>&1; then
network_ok=true
info "✅ 网络连接正常 (测试主机: $test_host)"
break
fi
done
if [ "$network_ok" = false ]; then
warn "⚠️ 网络连接检测失败,但仍尝试安装"
fi
local failed_packages=()
local current_packages=("${packages[@]}")
local mirror_index=0
while [ ${#current_packages[@]} -gt 0 ] && [ $total_attempts -lt $max_total_attempts ]; do
((total_attempts++))
# 在每次尝试前再次过滤已安装的包(防止长时间下载已存在的 wheel)
# 允许 package 规格(如 pkg==1.2.3 或 pkg[extra])被规整为基础包名再检查
local to_install=()
local skipped=()
for pkg in "${current_packages[@]}"; do
# 规范化包名:去掉版本约束和 extras
local pkg_base="$pkg"
pkg_base=$(echo "$pkg_base" | smart_sed -E 's/([<>=!].*)$//')
pkg_base=$(echo "$pkg_base" | smart_sed -E 's/\[.*\]//')
if check_package_installed "$pkg_base"; then
skipped+=("$pkg")
else
to_install+=("$pkg")
fi
done
if [ ${#skipped[@]} -gt 0 ]; then
info "ℹ️ 跳过已安装的包: ${skipped[*]}"
fi
# 如果没有需要安装的包,直接退出成功
if [ ${#to_install[@]} -eq 0 ]; then
ok "? 剩余包均已安装,跳过下载和安装。"
return 0
fi
# 将当前安装列表替换为需要安装的包
current_packages=("${to_install[@]}")
if [ $mirror_index -ge ${#mirrors[@]} ]; then
local final_failure_details="
所有镜像源都已尝试完毕,仍有以下包无法安装:
失败包列表: ${current_packages[*]}
总尝试次数: $total_attempts
已尝试的镜像源:"
for mirror in "${mirrors[@]}"; do
IFS='|' read -r mirror_url mirror_host <<< "$mirror"
final_failure_details="$final_failure_details
- $mirror_host ($mirror_url)"
done
trace_error "包安装完全失败" "$final_failure_details"
error "❌ 所有源尝试失败,仍无法安装: ${current_packages[*]}"
return 1
fi
IFS='|' read -r mirror_url mirror_host <<< "${mirrors[$mirror_index]}"
info "? 使用镜像: $mirror_host (第 $total_attempts 次)"
# 根据包的大小调整时间预估
local estimated_time="1-3 分钟"
for pkg in "${current_packages[@]}"; do
case "$pkg" in
torch|torchvision|torchaudio)
estimated_time="10-15 分钟"
break
;;
tensorflow*|numpy|scipy|pandas)
estimated_time="3-8 分钟"
break
;;
esac
done
# 显示即将执行的包安装
info "⏳ 正在安装: ${current_packages[*]} (预计需要 ${estimated_time})"
# 根据包大小设置超时时间
local timeout_seconds=120
# 检查是否包含大型包(PyTorch相关)
for pkg in "${current_packages[@]}"; do
case "$pkg" in
torch|torchvision|torchaudio|tensorflow|tensorflow-gpu)
timeout_seconds=1800 # 30分钟,足够下载大型包
info "? 检测到大型包 $pkg,延长超时时间至 30 分钟"
break
;;
esac
done
# 构造完整的 pip install 命令(使用数组避免引号问题)
local pip_cmd=("pip" "install" "--no-cache-dir" "--timeout" "$timeout_seconds")
# 添加进度显示参数
pip_cmd+=( "--progress-bar" "on" ) # 启用进度条 (on/off/raw,pretty无效)
pip_cmd+=( "--verbose" ) # 显示详细信息包括下载速度
# 添加其他参数
pip_cmd+=( "${extra_args[@]}" )
# 添加 index-url
pip_cmd+=( "--index-url" "$mirror_url" )
pip_cmd+=( "--trusted-host" "$mirror_host" )
# 添加额外的 index-url(如果存在)
if [ -n "$extra_index_url" ]; then
pip_cmd+=( "--extra-index-url" "$extra_index_url" )
fi
# 添加所有 trusted-host(来自参数)
for host in "${trusted_hosts[@]}"; do
pip_cmd+=( "--trusted-host" "$host" )
done
# 构建安装包列表时,避免把 torchvision/torchaudio 在 torch 已存在时与其它包一起安装
# 因为 pip 在依赖解析时可能会重新下载 torch。我们将把需要 --no-deps 的包单独安装。
local bulk_pkgs=()
local nodeps_pkgs=()
for pkg in "${current_packages[@]}"; do
case "$pkg" in
torchvision|torchaudio)
# 如果 torch 已安装且可 import,则单独安装这些包并使用 --no-deps
if check_package_installed "torch"; then
nodeps_pkgs+=("$pkg")
else
bulk_pkgs+=("$pkg")
fi
;;
*)
bulk_pkgs+=("$pkg")
;;
esac
done
# 默认将 bulk_pkgs 加入主 pip_cmd
if [ ${#bulk_pkgs[@]} -gt 0 ]; then
pip_cmd+=( "${bulk_pkgs[@]}" )
fi
# 显示完整命令(调试用)
if [[ "${DEBUG:-0}" -eq 1 ]]; then
info "? 执行命令: ${pip_cmd[*]}"
fi
# 执行命令并显示实时进度
local pip_install_output=""
local pip_install_exit_code
info "⚡ 开始下载和安装..."
info "? 提示: 按 Ctrl+C 可跳过当前镜像安装 ${current_packages[*]} 并尝试下一个 (不会退出脚本)"
# 如果包含大型包,提供更详细的信息
if [ "$is_large_package" = true ]; then
info "? 检测到大型包,可能需要下载 100MB+ 文件"
info "? 将显示详细的下载进度和速度信息"
info "⏰ 预计下载时间: 1-10分钟 (取决于网络速度)"
fi
# 启动后台进度提示任务(仅对大型包)
local progress_pid=""
local is_large_package=false
for pkg in "${current_packages[@]}"; do
case "$pkg" in
torch|torchvision|torchaudio|tensorflow|tensorflow-gpu)
is_large_package=true
break
;;
esac
done
if [ "$is_large_package" = true ]; then
# 启动后台进度提示
(
local count=0
while true; do
sleep 60 # 每分钟提示一次
count=$((count + 1))
echo "⏳ 大型包下载中... (已等待 ${count} 分钟,请继续等待或按 Ctrl+C 跳过此镜像)"
done
) &
progress_pid=$!
fi
# 设置局部的 SIGINT 处理,只影响这个安装过程
local pip_interrupted=false
local original_trap=$(trap -p INT)
# 定义中断处理函数
handle_pip_interrupt() {
pip_interrupted=true
# 停止后台进度提示
if [ -n "$progress_pid" ]; then
kill "$progress_pid" 2>/dev/null || true
wait "$progress_pid" 2>/dev/null || true
fi
echo ""
warn "⚠️ 用户中断安装 ${current_packages[*]},将尝试下一个镜像..."
return 0
}
# 设置新的中断处理
trap 'handle_pip_interrupt' INT
# 执行 pip 安装,显示实时进度
local start_time=$(date +%s)
"${pip_cmd[@]}" 2>&1 | while read -r line; do
# 检查是否被中断
if [ "$pip_interrupted" = true ]; then
break
fi
# 显示进度信息和时间戳
local current_time=$(date +%s)
local elapsed_time=$((current_time - start_time))
local time_str="[${elapsed_time}s]"
case "$line" in
*"Downloading"*|*"downloading"*)
# 提取文件大小信息
if [[ "$line" =~ ([0-9]+(\.[0-9]+)?[[:space:]]*[MGK]B) ]]; then
local file_size="${BASH_REMATCH[1]}"
echo "? $time_str $line"
# 大文件特别提示
if [[ "$line" =~ [0-9]+(\.[0-9]+)?[[:space:]]*MB && "$line" =~ torch ]]; then
echo "⏳ 正在下载大型 PyTorch 文件 ($file_size),请耐心等待..."
fi
else
echo "? $time_str $line"
fi
;;
*"Installing"*|*"installing"*)
echo "? $time_str $line"
;;
*"Successfully installed"*)
echo "✅ $time_str $line"
;;
*"Collecting"*)
echo "? $line"
;;
*"Building"*|*"building"*)
echo "? $time_str $line"
;;
# 新增:识别pip进度条
*"%"*|*"━"*|*"█"*)
# 检查是否是进度条(包含百分比或进度字符)
if [[ "$line" =~ [0-9]+% ]] || [[ "$line" =~ [━█▌▎] ]]; then
echo "⏬ $time_str $line"
else
echo "$line"
fi
;;
# 新增:识别下载速度信息
*"MB/s"*|*"KB/s"*|*"kB/s"*|*"mB/s"*)
echo "? $time_str $line"
;;
# 新增:识别文件大小信息
*" MB"*|*" KB"*|*" GB"*)
if [[ "$line" =~ (Downloading|downloading) ]]; then
echo "? $time_str $line"
else
echo "? $time_str $line"
fi
;;
*"ERROR"*|*"Error"*|*"error"*)
echo "❌ $time_str $line" >&2
;;
*"WARNING"*|*"Warning"*|*"warning"*)
echo "⚠️ $time_str $line"
;;
*)
# 检查是否包含重要信息
if [[ "$line" =~ (Processing|Preparing|Cached|Found|Using) ]]; then
echo "ℹ️ $line"
else
# 其他信息正常显示
echo "$line"
fi
;;
esac
done
# 获取退出码
pip_install_exit_code=${PIPESTATUS[0]}
# 如果 bulk 安装成功且存在需要单独安装的 nodeps_pkgs,则单独安装它们,使用 --no-deps
if [ $pip_install_exit_code -eq 0 ] && [ ${#nodeps_pkgs[@]} -gt 0 ]; then
info "? 开始单独安装不带依赖的包: ${nodeps_pkgs[*]} (使用 --no-deps,避免触发 torch 重下载)"
local nodeps_cmd=("pip" "install" "--no-deps" "--no-cache-dir" "--timeout" "$timeout_seconds" "--progress-bar" "on" "--verbose" "--index-url" "$mirror_url" "--trusted-host" "$mirror_host")
if [ -n "$extra_index_url" ]; then
nodeps_cmd+=("--extra-index-url" "$extra_index_url")
fi
for host in "${trusted_hosts[@]}"; do
nodeps_cmd+=("--trusted-host" "$host")
done
nodeps_cmd+=("${nodeps_pkgs[@]}")
# 执行 nodeps 安装并显示输出
"${nodeps_cmd[@]}" 2>&1 | while read -r nline; do
echo "$nline"
done
local nodeps_exit=${PIPESTATUS[0]}
if [ $nodeps_exit -ne 0 ]; then
warn "⚠️ 单独安装 ${nodeps_pkgs[*]} 失败 (exit $nodeps_exit),将视为当前镜像失败并尝试下一个镜像"
pip_install_exit_code=1
else
ok "✅ 单独安装 ${nodeps_pkgs[*]} 成功"
fi
fi
# 停止后台进度提示
if [ -n "$progress_pid" ]; then
kill "$progress_pid" 2>/dev/null || true
wait "$progress_pid" 2>/dev/null || true
fi
# 检查是否被用户中断
if [ "$pip_interrupted" = true ]; then
pip_install_exit_code=130 # 用户中断退出码
pip_install_output="用户中断安装"
fi
# 恢复原来的中断处理
if [ -n "$original_trap" ]; then
eval "$original_trap"
else
trap - INT
fi
if [ $pip_install_exit_code -eq 0 ]; then
local end_time=$(date +%s)
local elapsed_time=$((end_time - start_time))
local elapsed_min=$((elapsed_time / 60))
local elapsed_sec=$((elapsed_time % 60))
if [ $elapsed_time -gt 60 ]; then
ok "✅ 所有包安装成功 (耗时: ${elapsed_min}分${elapsed_sec}秒)"
else
ok "✅ 所有包安装成功 (耗时: ${elapsed_time}秒)"
fi
# 如果是大型包,显示额外的性能信息
if [ "$is_large_package" = true ]; then
info "? 大型包安装完成统计:"
info " ⏱️ 总耗时: ${elapsed_min}分${elapsed_sec}秒"
info " ? 使用镜像: $mirror_host"
info " ? 包数量: ${#current_packages[@]}"
fi
return 0
elif [ $pip_install_exit_code -eq 124 ]; then
local timeout_msg="⏰ 镜像 $mirror_host 安装 ${current_packages[*]} 超时 (${timeout_seconds}秒),尝试下一个镜像"
warn "$timeout_msg"
# 记录详细的超时信息到日志
local timeout_details="
超时详情:
- 镜像: $mirror_host
- 超时时间: ${timeout_seconds}秒 ($(echo "scale=1; ${timeout_seconds}/60" | bc 2>/dev/null || echo "$((timeout_seconds/60))")分钟)
- 包列表: ${current_packages[*]}
- 时间: $(sh_date)
- 建议: 尝试使用更快的网络或减少包数量"
echo "$timeout_details" | tee -a "${LOG_FILE:-/dev/null}" >/dev/null
trace_error "pip 安装超时 (镜像: $mirror_host, 包: ${current_packages[*]})" "$timeout_details" "124"
elif [ $pip_install_exit_code -eq 130 ]; then
warn "? 用户跳过镜像 $mirror_host (安装 ${current_packages[*]}),尝试下一个镜像"
else
warn "⚠️ 镜像 $mirror_host 安装 ${current_packages[*]} 失败,尝试下一个镜像"
if [[ "${DEBUG:-0}" -eq 1 ]]; then
local mirror_failure_details="
镜像: $mirror_host
包列表: ${current_packages[*]}
命令: ${pip_cmd[*]}
退出码: $pip_install_exit_code
输出:
$pip_install_output"
trace_error "pip 安装失败 (镜像: $mirror_host, 包: ${current_packages[*]})" "$mirror_failure_details" "$pip_install_exit_code"
fi
fi
failed_packages=()
for pkg in "${current_packages[@]}"; do
if ! pip show "$pkg" &> /dev/null; then
failed_packages+=("$pkg")
fi
done
current_packages=("${failed_packages[@]}")
((mirror_index++))
# 如果不是超时或中断,等待一下再尝试下一个镜像
if [ $pip_install_exit_code -ne 124 ] && [ $pip_install_exit_code -ne 130 ]; then
sleep 2
fi
done
if [ ${#current_packages[@]} -gt 0 ]; then
error "❌ 安装失败: ${current_packages[*]}"
return 1
fi
ok "✅ 安装完成"
return 0
}
# ? 封装:安装 GPU 版 PyTorch 的函数
# 在调用 install_torch_gpu() 前可用:
# export TORCH_INDEX="https://download.pytorch.org/whl/cu118"
# 支持源:
# # 使用官方源(推荐)
# export TORCH_INDEX="https://download.pytorch.org/whl/cu118"
# # 使用阿里云镜像(可选)
# export TORCH_INDEX="https://mirrors.aliyun.com/pytorch-wheels/cu118/"
install_torch_gpu() {
local index_url="${1:-$TORCH_INDEX}"
# 提示用户使用了默认值
if [ -z "$TORCH_INDEX" ] && [ -z "${1:-}" ]; then
warn "⚠️ TORCH_INDEX 未设置,使用默认源: https://download.pytorch.org/whl/cu118"
fi
# 设置默认值
: "${index_url:=https://download.pytorch.org/whl/cu118}"
info "? 安装 GPU 版 PyTorch: torch torchaudio torchvision"
info "? 主源: $index_url"
info "? 备用源: https://pypi.org/simple"
if robust_pip_install \
"torch" "torchaudio" "torchvision" \
--index-url "$index_url" \
--extra-index-url "https://pypi.org/simple" \
--trusted-host "pypi.org" \
--trusted-host "$(echo "$index_url" | awk -F[/:] '{print $4}')" \
; then
ok "✅ GPU 版 PyTorch 安装成功"
return 0
else
local torch_failure_details="
PyTorch GPU 版本安装失败详情:
主源: $index_url
备用源: https://pypi.org/simple
包列表: torch torchaudio torchvision
可能的原因:
1. 网络连接问题 (访问 PyTorch 官方源失败)
2. CUDA 版本不匹配 (请检查本机 CUDA 版本)
3. Python 版本不兼容
4. 磁盘空间不足
建议的解决方案:
1. 检查 CUDA 版本: nvidia-smi 或 nvcc --version
2. 手动指定 CUDA 版本,如:
export TORCH_INDEX='https://download.pytorch.org/whl/cu117'
3. 降级到 CPU 版本: pip install torch torchaudio torchvision
4. 检查网络代理设置"
trace_error "GPU 版 PyTorch 安装失败" "$torch_failure_details"
error "❌ GPU 版 PyTorch 安装失败"
return 1
fi
}
# ? 通用 PyTorch 冲突保护安装器
# 功能: 使用 --no-deps 安装包,然后智能安装非冲突依赖
# 用法: install_with_torch_protection "package_name" [excluded_packages...]
install_with_torch_protection() {
local target_package="$1"
shift
local excluded_packages=("$@")
# 默认的 PyTorch 相关排除列表
local default_torch_exclusions=(
"torch" "torchvision" "torchaudio" "torchtext"
"pytorch-lightning" "torch-audio" "torchdiffeq"
"torchmetrics" "torchsde" "torch-optimizer"
"torch-stoi" "pytorch-ranger" "ema-pytorch"
"vector-quantize-pytorch" "clip-anytorch"
"dctorch" "kornia"
)
# 合并排除列表
local all_exclusions=("${default_torch_exclusions[@]}" "${excluded_packages[@]}")
info "?️ 开始保护性安装: $target_package"
info "? 排除包列表: ${all_exclusions[*]}"
# Step 1: 检查包是否已安装
if pip show "$target_package" &> /dev/null; then
info "✅ $target_package 已安装,跳过"
return 0
fi
# Step 2: 使用 --no-deps 安装目标包
info "? 使用 --no-deps 安装 $target_package(避免依赖冲突)"
if ! robust_pip_install "--no-deps" "$target_package"; then
trace_error "$target_package 安装失败" "robust_pip_install --no-deps $target_package 返回失败"
error "❌ $target_package 安装失败"
return 1
fi
ok "✅ $target_package 安装成功(已跳过依赖以保护 PyTorch)"
# Step 3: 智能安装依赖
info "? 智能安装 $target_package 依赖(排除冲突包)..."
install_package_deps "$target_package" "${all_exclusions[@]}"
return 0
}
# ?️ 保护性pip安装函数:防止已安装核心包被依赖重装
# 用法: protected_pip_install "package1" "package2" [pip_args...]
protected_pip_install() {
local packages=("$@")
local pip_args=()
local install_packages=()
# 分离包名和pip参数
for arg in "${packages[@]}"; do
if [[ "$arg" =~ ^-- ]]; then
pip_args+=("$arg")
else
install_packages+=("$arg")
fi
done
info "?️ 保护性安装: ${install_packages[*]}"
# 检查是否有已保护的核心包
local has_protected=false
if [ -n "${PROTECTED_CORE_PACKAGES:-}" ] && [ ${#PROTECTED_CORE_PACKAGES[@]} -gt 0 ]; then
for pkg in "${install_packages[@]}"; do
for protected in "${PROTECTED_CORE_PACKAGES[@]}"; do
if [ "$pkg" = "$protected" ]; then
warn "?️ 检测到保护包 $pkg,使用 --no-deps 防止依赖重装"
has_protected=true
break 2
fi
done
done
fi
# 如果包含保护包,使用 --no-deps 安装
if [ "$has_protected" = true ]; then
info "? 使用 --no-deps 模式保护已安装核心包"
robust_pip_install "${install_packages[@]}" "${pip_args[@]}" --no-deps
else
# 正常安装
robust_pip_install "${install_packages[@]}" "${pip_args[@]}"
fi
}
# ? 智能依赖安装器(通用版本)
# 功能: 获取包的依赖列表,排除指定包,安装其余依赖
# 用法: install_package_deps "package_name" excluded_package1 excluded_package2 ...
install_package_deps() {
local target_package="$1"
shift
local excluded_packages=("$@")
info "? 获取 $target_package 的实际依赖列表..."
# 方法1: 尝试从 pip show 获取依赖信息
local actual_deps=""
actual_deps=$(pip show "$target_package" 2>/dev/null | grep "Requires:" | sed 's/Requires: //' | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$')
# 方法2: 如果方法1失败,尝试使用 pipdeptree (如果可用)
if [ -z "$actual_deps" ] && command -v pipdeptree >/dev/null 2>&1; then
info "? 使用 pipdeptree 获取依赖..."
actual_deps=$(pipdeptree --packages "$target_package" --json 2>/dev/null | python -c "
import sys, json
try:
data = json.load(sys.stdin)
for pkg in data:
if pkg['package']['key'] == '$target_package':
deps = [dep['key'] for dep in pkg['dependencies']]
print('\n'.join(deps))
break
except:
pass
" 2>/dev/null)
fi
# 方法3: 使用包特定的已知依赖列表
if [ -z "$actual_deps" ]; then
info "? 使用预定义的 $target_package 依赖列表..."
case "$target_package" in
"prefigure")
actual_deps="tqdm
einops
matplotlib
numpy
Pillow
scipy
accelerate
diffusers
transformers"
;;
"pytorch-lightning")
actual_deps="torch
torchmetrics
lightning-utilities
typing-extensions
fsspec
packaging
pyyaml
tqdm"
;;
"diffusers")
actual_deps="torch
transformers
numpy
Pillow
requests
regex
importlib-metadata
huggingface-hub
accelerate
safetensors"
;;
"speechbrain")
actual_deps="torch
torchaudio
numpy
scipy
tqdm
joblib
ruamel.yaml
hyperpyyaml
sentencepiece
huggingface-hub"
;;
"asteroid")
actual_deps="torch
torchaudio
numpy
scipy
pandas
soundfile
torch-stoi
torch-optimizer
pytorch-lightning
huggingface-hub"
;;
*)
# 通用的机器学习包常见依赖
actual_deps="numpy
scipy
matplotlib
pandas
tqdm
Pillow
requests
packaging
typing-extensions"
;;
esac
else
info "? 从包信息获取到依赖列表"
fi
if [ -z "$actual_deps" ]; then
warn "⚠️ 无法获取 $target_package 的依赖信息,跳过依赖安装"
return 0
fi
# 处理每个依赖
local installed_count=0
local skipped_count=0
local failed_count=0
while IFS= read -r dep; do
[ -z "$dep" ] && continue
# 清理依赖名称(去掉版本要求)
local clean_dep=$(echo "$dep" | sed 's/[<>=!].*//' | sed 's/^ *//;s/ *$//')
[ -z "$clean_dep" ] && continue
# 检查是否在排除列表中
local is_excluded=false
for excluded_pkg in "${excluded_packages[@]}"; do
if [[ "$clean_dep" == "$excluded_pkg"* ]] || [[ "$clean_dep" == *"$excluded_pkg"* ]]; then
is_excluded=true
break
fi
done
if [ "$is_excluded" = true ]; then
info "⏭️ 跳过排除的依赖: $clean_dep"
((skipped_count++))
continue
fi
# 检查是否已安装
if pip show "$clean_dep" &> /dev/null; then
info "✅ $clean_dep 已安装,跳过"
else
info "? 安装 $target_package 依赖: $clean_dep"
if robust_pip_install "$clean_dep"; then
ok "✅ $clean_dep 安装成功"
((installed_count++))
else
warn "⚠️ $clean_dep 安装失败,可能影响 $target_package 功能"
((failed_count++))
fi
fi
done <<< "$actual_deps"
# 显示安装统计
info "? 依赖安装统计: 新安装 $installed_count 个,跳过 $skipped_count 个,失败 $failed_count 个"
# 验证目标包是否能正常导入
info "? 验证 $target_package 安装完整性..."
local import_test=""
import_test=$(python -c "
try:
import $target_package
print('IMPORT_OK')
version = getattr($target_package, '__version__', 'unknown')
print(f'$target_package 版本: {version}')
except ImportError as e:
print(f'IMPORT_ERROR: {e}')
except Exception as e:
print(f'OTHER_ERROR: {e}')
" 2>&1)
if echo "$import_test" | grep -q "IMPORT_OK"; then
echo "$import_test"
ok "✅ $target_package 及其依赖安装完成,可正常使用"
return 0
else
echo "$import_test"
warn "⚠️ $target_package 导入测试失败,可能缺少某些依赖"
return 1
fi
}
# ? 智能安装 prefigure 依赖(使用通用函数)
install_prefigure_deps() {
install_package_deps "prefigure" \
"torch" "torchvision" "torchaudio" "torchtext" \
"pytorch-lightning" "torch-audio" "torchdiffeq" \
"torchmetrics" "torchsde" "torch-optimizer" \
"torch-stoi" "pytorch-ranger" "ema-pytorch" \
"vector-quantize-pytorch" "clip-anytorch" \
"dctorch" "kornia"
}
# ?️ 通用核心包保护安装:确保核心包版本,并保护性安装冲突包
# 用法:ensure_core_package_installed "core_package" "conflict_package1" ["conflict_package2" ...]
# 示例:ensure_core_package_installed "torch" "prefigure" "diffusers"
# 示例:ensure_core_package_installed "tensorflow" "some_tf_conflict_package"
# 示例:ensure_core_package_installed "numpy" "some_numpy_conflict_package"
ensure_core_package_installed() {
local core_package="$1"
shift
local conflict_packages=("$@")
if [ -z "$core_package" ]; then
error "❌ ensure_core_package_installed: 需要指定核心包名"
return 1
fi
if [ ${#conflict_packages[@]} -eq 0 ]; then
error "❌ ensure_core_package_installed: 需要指定至少一个冲突包"
return 1
fi
info "?️ 开始核心包保护性安装流程"
info "? 核心包: $core_package"
info "⚠️ 冲突包列表: ${conflict_packages[*]}"
# Step 1: 使用统一的包检查函数确保核心包已安装
info "? 检查核心包 $core_package 安装状态..."
if check_package_installed "$core_package"; then
ok "✅ 检测到核心包 $core_package 已正确安装"
# 显示当前版本信息(通用版)
local version_info=""
version_info=$(python -c "
try:
import $core_package
if hasattr($core_package, '__version__'):
print(f'$core_package 版本: {$core_package.__version__}')
else:
print(f'$core_package: 已安装(无版本信息)')
# 特殊处理 torch 的 CUDA 信息
if '$core_package' == 'torch':
print(f'CUDA可用: {\"是\" if $core_package.cuda.is_available() else \"否\"}')
if $core_package.cuda.is_available():
print(f'GPU数量: {$core_package.cuda.device_count()}')
except Exception as e:
print(f'版本检查失败: {e}')
" 2>/dev/null)
echo "$version_info"
else
info "? 需要安装核心包 $core_package..."
# 根据核心包类型选择安装策略
case "$core_package" in
"torch")
if ! install_torch_gpu; then
error "❌ PyTorch GPU 版本安装失败"
return 1
fi
;;
*)
# 通用包安装
if ! robust_pip_install "$core_package"; then
error "❌ 核心包 $core_package 安装失败"
return 1
fi
;;
esac
ok "✅ 核心包 $core_package 安装完成"
fi
# Step 2: 循环保护性安装所有冲突包
for conflict_pkg in "${conflict_packages[@]}"; do
info "?️ 开始保护性安装 $conflict_pkg (避免覆盖核心包 $core_package)"
# 根据核心包类型选择保护策略
case "$core_package" in
"torch")
if ! install_with_torch_protection "$conflict_pkg"; then
warn "⚠️ $conflict_pkg 安装失败,继续处理下一个包"
continue
fi
;;
*)
# 通用保护策略:使用 --no-deps
if ! robust_pip_install "$conflict_pkg" --no-deps; then
warn "⚠️ $conflict_pkg 保护性安装失败,继续处理下一个包"
continue
fi
;;
esac
ok "✅ $conflict_pkg 保护性安装完成"
done
# Step 3: 最终验证核心包完整性
info "? 验证核心包 $core_package 是否保持完整"
local final_check=""
final_check=$(python -c "
try:
import $core_package
if hasattr($core_package, '__version__'):
print(f'最终 $core_package 版本: {$core_package.__version__}')
else:
print(f'最终 $core_package: 导入成功')
# 特殊验证逻辑
if '$core_package' == 'torch':
if $core_package.cuda.is_available():
print('✅ GPU支持正常')
print(f'GPU设备: {$core_package.cuda.get_device_name(0)}')
else:
print('❌ GPU支持异常')
else:
print('✅ 包导入正常')
except Exception as e:
print(f'❌ 验证失败: {e}')
" 2>/dev/null)
echo "$final_check"
if echo "$final_check" | grep -q "✅"; then
ok "? 核心包 $core_package 保护性安装流程完成"
return 0
else
error "❌ 核心包 $core_package 验证失败"
return 1
fi
}
# ?️ PyTorch 专用保护函数(向后兼容)
ensure_torch_gpu_be_installed() {
local conflict_packages=("$@")
info "? 调用通用核心包保护函数处理 PyTorch"
ensure_core_package_installed "torch" "${conflict_packages[@]}"
}
# ?️ 预防性安装:确保 PyTorch GPU 版本,并保护性安装 prefigure(向后兼容函数)
install_pytorch_and_prefigure_safely() {
info "? 调用通用核心包保护函数处理 prefigure"
ensure_core_package_installed "torch" "prefigure"
}
info "?️ 开始 PyTorch GPU 保护性安装流程"
# Step 1: 使用统一的包检查函数检查 PyTorch GPU 版本
info "? 检查当前 PyTorch 安装状态..."
if check_package_installed "torch"; then
ok "✅ 检测到 GPU 版本的 PyTorch 已正确安装"
# 显示当前版本信息
local version_info=""
version_info=$(python -c "
try:
import torch
print(f'PyTorch版本: {torch.__version__}')
if torch.cuda.is_available():
print(f'CUDA可用: 是')
print(f'GPU数量: {torch.cuda.device_count()}')
else:
print(f'CUDA可用: 否')
except Exception as e:
print(f'版本检查失败: {e}')
" 2>/dev/null)
echo "$version_info"
else
info "� 需要安装 GPU 版本的 PyTorch..."
if ! install_torch_gpu; then
error "❌ PyTorch GPU 版本安装失败"
return 1
fi
ok "✅ PyTorch GPU 版本安装完成"
fi
# Step 2: 安装 prefigure(使用通用保护函数)
info "?️ 开始保护性安装 prefigure (避免覆盖 PyTorch GPU 版本)"
if ! install_with_torch_protection "prefigure"; then
warn "⚠️ prefigure 安装失败,但继续执行后续步骤"
# 可选:exit 1 如果必须成功
fi
# Step 4: 验证安装后 PyTorch GPU 版本是否保持完整
info "? 验证 PyTorch GPU 版本是否保持完整"
local current_torch_check=""
current_torch_check=$(python -c "
import torch
print(f'当前PyTorch版本: {torch.__version__}')
if torch.cuda.is_available():
print('CURRENT_GPU_OK')
print(f'CUDA版本: {torch.version.cuda if hasattr(torch.version, \"cuda\") else \"unknown\"}')
print(f'GPU设备: {torch.cuda.get_device_name(0)}')
else:
print('CURRENT_CPU_ON***')
" 2>&1)
local torch_check_exit_code=$?
if [ $torch_check_exit_code -eq 0 ] && echo "$current_torch_check" | grep -q "CURRENT_GPU_OK"; then
echo "$current_torch_check"
ok "✅ PyTorch GPU 版本保持完整,prefigure 安装成功"
return 0
else
warn "⚠️ 检测到 PyTorch 问题,可能需要重新安装"
echo "$current_torch_check"
# 如果确实有问题,再重新安装 GPU 版本
info "? 重新安装 GPU 版 PyTorch 以确保完整性"
if ! install_torch_gpu; then
trace_error "GPU 版 PyTorch 恢复安装失败" "install_torch_gpu 函数在 prefigure 后调用失败"
error "❌ GPU 版 PyTorch 安装失败,请检查 CUDA 环境"
return 1
fi
fi
# Step 5: 最终验证 CUDA 是否可用
info "? 最终验证 PyTorch 是否支持 CUDA"
local cuda_verification_output=""
cuda_verification_output=$(python -c "
import torch
print(f'PyTorch: {torch.__version__}')
assert torch.cuda.is_available(), 'CUDA not available'
print(f'✅ CUDA 可用: {torch.cuda.get_device_name(0)}')
" 2>&1)
local cuda_verification_exit_code=$?
if [ $cuda_verification_exit_code -eq 0 ]; then
echo "$cuda_verification_output"
ok "✅ 成功确保 PyTorch 为 GPU 版"
else
local cuda_diagnostic_output=""
cuda_diagnostic_output=$(python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}')" 2>&1)
local full_diagnostic="
CUDA 验证失败详情:
PyTorch 验证输出:
$cuda_verification_output
CUDA 可用性诊断:
$cuda_diagnostic_output
可能的原因:
1. CUDA 驱动版本不匹配
2. PyTorch 安装的是 CPU 版本
3. CUDA 库未正确安装
4. 系统没有兼容的 GPU
建议的解决方案:
1. 检查 NVIDIA 驱动: nvidia-smi
2. 检查 CUDA 版本: nvcc --version
3. 重新安装对应 CUDA 版本的 PyTorch
4. 如无 GPU,改用 CPU 版本: pip install torch torchvision torchaudio"
trace_error "PyTorch CUDA 验证失败" "$full_diagnostic" "$cuda_verification_exit_code"
error "❌ PyTorch 不支持 CUDA,请检查驱动、CUDA、安装版本是否匹配"
exit 1
fi
}
# # ? 智能升级 pip:仅当版本过低时才升级
# # 调用函数(使用默认最低版本 23.0)
# upgrade_pip_if_needed
# # 或指定最低版本
# upgrade_pip_if_needed "24.0"
upgrade_pip_if_needed() {
local required_version="${1:-23.0}" # 建议最低版本(可传参)
local current_version
info "? 检查 pip 版本..."
# 获取当前 pip 版本
local pip_version_output=""
pip_version_output=$(python -m pip --version 2>&1)
local pip_version_exit_code=$?
if [ $pip_version_exit_code -eq 0 ]; then
current_version=$(echo "$pip_version_output" | awk '{print $2}')
else
trace_error "pip 版本检查失败" "$pip_version_output" "$pip_version_exit_code"
warn "⚠️ pip 未安装或无法获取版本,准备安装"
# 先尝试 ensurepip(某些系统不包含)
local ensurepip_output=""
ensurepip_output=$(python -m ensurepip --default-pip 2>&1)
local ensurepip_exit_code=$?
if [ $ensurepip_exit_code -ne 0 ]; then
trace_error "ensurepip 安装失败" "$ensurepip_output" "$ensurepip_exit_code"
fi
current_version=$(python -m pip --version 2>/dev/null | awk '{print $2}')
# 如果仍然没有 pip,则尝试使用 get-pip.py(使用清华镜像以提高可用性)
if [ -z "$current_version" ]; then
warn "⚠️ pip 仍未安装,尝试通过 get-pip.py 安装"
GET_PIP_URL="https://pypi.tuna.tsinghua.edu.cn/bootstrap/pip/get-pip.py"
GET_PIP_PY="/tmp/get-pip-$$.py"
local download_output=""
if command -v curl >/dev/null 2>&1; then
download_output=$(curl -fsSL --connect-timeout 10 --max-time 60 "$GET_PIP_URL" -o "$GET_PIP_PY" 2>&1)
local download_exit_code=$?
if [ $download_exit_code -ne 0 ]; then
trace_error "curl 下载 get-pip.py 失败" "$download_output" "$download_exit_code"
fi
elif command -v wget >/dev/null 2>&1; then
download_output=$(wget -q -O "$GET_PIP_PY" "$GET_PIP_URL" 2>&1)
local download_exit_code=$?
if [ $download_exit_code -ne 0 ]; then
trace_error "wget 下载 get-pip.py 失败" "$download_output" "$download_exit_code"
fi
else
trace_error "缺少下载工具" "系统未安装 curl 或 wget,无法下载 get-pip.py"
warn "⚠️ 无法下载 get-pip.py:系统未安装 curl 或 wget"
fi
if [ -f "$GET_PIP_PY" ]; then
local get_pip_output=""
get_pip_output=$(python "$GET_PIP_PY" --quiet --no-warn-script-location 2>&1)
local get_pip_exit_code=$?
if [ $get_pip_exit_code -eq 0 ]; then
ok "✅ pip 安装成功 (get-pip.py)"
else
trace_error "get-pip.py 安装失败" "$get_pip_output" "$get_pip_exit_code"
warn "⚠️ 使用 get-pip.py 安装 pip 失败"
fi
rm -f "$GET_PIP_PY"
fi
current_version=$(python -m pip --version 2>/dev/null | awk '{print $2}')
fi
fi
info "? 当前 pip 版本: $current_version,目标最低版本: $required_version"
# 比较版本(使用 sort -V 进行版本排序比较)
if printf '%s\n%s' "$required_version" "$current_version" | sort -V | head -n1 | grep -q "$required_version"; then
ok "✅ pip 版本已满足要求,跳过升级"
return 0
else
info "⏫ pip 版本过低,正在升级..."
local pip_upgrade_output=""
local pip_upgrade_exit_code
if [ -n "$PIP_INDEX" ] && [ -n "$PIP_TRUSTED_HOST" ]; then
pip_upgrade_output=$(python -m pip install --upgrade pip \
-i "$PIP_INDEX" \
--trusted-host "$PIP_TRUSTED_HOST" 2>&1)
pip_upgrade_exit_code=$?
else
pip_upgrade_output=$(python -m pip install --upgrade pip 2>&1)
pip_upgrade_exit_code=$?
fi
if [ $pip_upgrade_exit_code -eq 0 ]; then
ok "✅ pip 升级成功"
else
trace_error "pip 升级失败" "$pip_upgrade_output" "$pip_upgrade_exit_code"
warn "⚠️ pip 升级失败,但继续执行后续步骤"
fi
fi
}
# -----------------------------
# 3. 检查或创建环境(Conda 优先,venv fallback)
# -----------------------------
ENV_NAME="train_ss"
# 创建或使用已有环境(Conda 或 venv)
if ! create_or_use_env "$ENV_NAME" "${PYTHON_VERSIONS[@]}"; then
trace_error "虚拟环境创建/激活完全失败" "create_or_use_env 函数返回非零退出码"
error "虚拟环境创建或激活失败,脚本终止"
fi
# 此时环境已激活,无需额外 activate
info "✅ 环境已激活: $ENV_NAME"
echo "最新进度请查看日志文件: $LOG_FILE"
# -----------------------------
# 5. 升级 pip
# -----------------------------
upgrade_pip_if_needed
echo "最新进度请查看日志文件: $LOG_FILE"
# -----------------------------
# 6. 智能安装 PyTorch(根据 GPU)
# -----------------------------
info "? 检测 GPU..."
TORCH_INDEX="https://download.pytorch.org/whl/cu128"
if command -v nvidia-smi &> /dev/null; then
GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader,nounits -i 0 || echo "unknown")
info "GPU: $GPU_NAME"
case "$GPU_NAME" in
*"P100"*) TORCH_INDEX="https://download.pytorch.org/whl/cu118" ;;
*"T4"*|*"V100"*) info "使用 CUDA 11.8/12.x" ;;
*"A10"*|*"A100"*|*"3090"*|*"4090"*) info "使用 CUDA 12.8" ;;
*) info "未知 GPU,尝试 CUDA 12.8" ;;
esac
else
warn "⚠️ 无 nvidia-smi,使用 CPU 版"
TORCH_INDEX="https://download.pytorch.org/whl/cpu"
fi
# ? 处理核心包参数
DEFAULT_CORE_PACKAGES="torch,torchvision,torchaudio"
CORE_PACKAGES="${CORE_PACKAGES_ARG:-$DEFAULT_CORE_PACKAGES}"
# 解析核心包列表
IFS=',' read -ra CORE_PACKAGE_ARRAY <<< "$CORE_PACKAGES"
PRIMARY_CORE_PACKAGE="${CORE_PACKAGE_ARRAY[0]}"
info "?️ 核心包配置:"
info " 主核心包: $PRIMARY_CORE_PACKAGE"
if [ ${#CORE_PACKAGE_ARRAY[@]} -gt 1 ]; then
info " 相关核心包: ${CORE_PACKAGE_ARRAY[*]:1}"
fi
info " 冲突包: prefigure"
# ? 改进:对所有核心包进行保护,不仅仅是主核心包
info "? 检查所有指定的核心包安装状态..."
# 检查每个核心包是否已安装
ALL_CORE_PACKAGES_INSTALLED=true
# ? 改进:完全使用Python检查,兼容受限环境(无需grep/sed/cut等外部命令)
torch_check_result=$(python -c "
try:
import subprocess
import sys
# 检查torch包
result = subprocess.run([sys.executable, '-m', 'pip', 'show', 'torch'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
version = None
for line in result.stdout.split('\n'):
if line.startswith('Version:'):
version = line.split(':', 1)[1].strip()
break
if version:
print(f'TORCH_VERSION:{version}')
if '2.8.0' in version and ('cu' in version or '+cu' in version):
print('STATUS:2.8.0_GPU_FOUND')
elif 'cu' in version or '+cu' in version:
print('STATUS:OTHER_GPU_FOUND')
else:
print('STATUS:CPU_VERSION')
else:
print('STATUS:NO_VERSION')
else:
print('STATUS:NOT_FOUND')
# 同时检查其他核心包
other_packages = ['torchvision', 'torchaudio']
missing_packages = []
for pkg in other_packages:
pkg_result = subprocess.run([sys.executable, '-m', 'pip', 'show', pkg],
capture_output=True, text=True, timeout=10)
if pkg_result.returncode != 0:
missing_packages.append(pkg)
if missing_packages:
print(f'MISSING_PACKAGES:{\"|\".join(missing_packages)}')
else:
print('ALL_PYTORCH_PACKAGES_FOUND')
except Exception as e:
print(f'PYTHON_CHECK_FAILED:{e}')
" 2>/dev/null)
# 使用纯bash字符串处理解析结果
case "$torch_check_result" in
*"STATUS:2.8.0_GPU_FOUND"*)
# 提取版本号
torch_version=$(echo "$torch_check_result" | while read line; do
case "$line" in
TORCH_VERSION:*) echo "${line#TORCH_VERSION:}" ;;
esac
done)
ok "? 检测到 PyTorch 2.8.0 GPU版本: $torch_version,启用强制保护模式"
ALL_CORE_PACKAGES_INSTALLED=true
for core_pkg in "${CORE_PACKAGE_ARRAY[@]}"; do
ok "✅ $core_pkg - 强制保护模式,跳过重复安装"
done
;;
*"STATUS:OTHER_GPU_FOUND"*)
torch_version=$(echo "$torch_check_result" | while read line; do
case "$line" in
TORCH_VERSION:*) echo "${line#TORCH_VERSION:}" ;;
esac
done)
ok "? 检测到 PyTorch GPU版本: $torch_version,启用强制保护模式"
ALL_CORE_PACKAGES_INSTALLED=true
for core_pkg in "${CORE_PACKAGE_ARRAY[@]}"; do
ok "✅ $core_pkg - 强制保护模式,跳过重复安装"
done
;;
*"PYTHON_CHECK_FAILED"*|*"STATUS:NOT_FOUND"*)
# 环境检查失败或torch未安装,提供手动安装指导
warn "⚠️ 环境检查失败或PyTorch未安装"
echo ""
error "? 建议手动安装以下包:"
echo ""
echo "? 复制以下命令进行手动安装:"
echo ""
echo "# PyTorch GPU版本(推荐)"
echo "pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128"
echo ""
echo "# 或者CPU版本"
echo "pip install torch torchvision torchaudio"
echo ""
echo "# 其他常用包"
echo "pip install transformers diffusers opencv-python pillow numpy pandas matplotlib"
echo ""
echo "? 安装完成后,请重新运行脚本"
echo ""
read -p "⚠️ 是否继续尝试自动安装?(y/N): " continue_auto
if [[ ! "$continue_auto" =~ ^[Yy]$ ]]; then
exit 1
fi
ALL_CORE_PACKAGES_INSTALLED=false
;;
*"STATUS:CPU_VERSION"*)
warn "⚠️ 检测到PyTorch CPU版本,建议升级到GPU版本"
ALL_CORE_PACKAGES_INSTALLED=false
;;
*)
# 未知状态,进入常规检查
ALL_CORE_PACKAGES_INSTALLED=false
;;
esac
# 如果强制保护模式未激活,执行常规检查
if [ "$ALL_CORE_PACKAGES_INSTALLED" != true ]; then
# 常规检查流程
for core_pkg in "${CORE_PACKAGE_ARRAY[@]}"; do
echo "? 正在检查核心包: $core_pkg" >&2
if check_package_installed "$core_pkg"; then
ok "✅ $core_pkg - 已正确安装"
echo "✅ 核心包 $core_pkg 检查通过" >&2
else
warn "? $core_pkg - 需要安装"
echo "❌ 核心包 $core_pkg 检查失败" >&2
ALL_CORE_PACKAGES_INSTALLED=false
fi
done
fi
echo "? 最终状态: ALL_CORE_PACKAGES_INSTALLED=$ALL_CORE_PACKAGES_INSTALLED" >&2
if [ "$ALL_CORE_PACKAGES_INSTALLED" = true ]; then
ok "? 所有指定的核心包都已安装,跳过核心包保护流程"
else
# 只有在需要时才进行保护性安装
info "?️ 开始核心包保护性安装流程..."
# 确保再安装了prefigure之后安装的是指定的核心包版本
if ! ensure_core_package_installed "$PRIMARY_CORE_PACKAGE" "prefigure"; then
trace_error "核心包保护性安装流程失败" "ensure_core_package_installed 函数返回非零退出码"
error "核心包 $PRIMARY_CORE_PACKAGE 安装失败,无法继续"
fi
fi
# -----------------------------
# 6.5. ? 核心大型包专项安装阶段
# -----------------------------
# ? 核心大型包安装函数:专门处理需要大量下载的包
install_essential_large_packages() {
info "? 开始核心大型包专项安装阶段..."
echo "? 调试: ALL_CORE_PACKAGES_INSTALLED=${ALL_CORE_PACKAGES_INSTALLED:-未设置}" >&2
# ? 如果前面已经确认所有核心包都安装了,直接跳过
if [ "${ALL_CORE_PACKAGES_INSTALLED:-false}" = true ]; then
ok "? 前面已确认所有核心包都已安装,跳过大型包安装阶段"
echo "✅ 大型包安装阶段成功跳过" >&2
return 0
fi
echo "⚠️ 继续执行大型包安装流程" >&2
# �️ 创建已安装核心包保护列表 - 防止依赖重装
local PROTECTED_CORE_PACKAGES=()
if [ -n "${CORE_PACKAGE_ARRAY:-}" ] && [ ${#CORE_PACKAGE_ARRAY[@]} -gt 0 ]; then
for core_pkg in "${CORE_PACKAGE_ARRAY[@]}"; do
if check_package_installed "$core_pkg"; then
PROTECTED_CORE_PACKAGES+=("$core_pkg")
info "?️ 保护已安装核心包: $core_pkg"
fi
done
fi
# �? 智能使用 CLI 指定的核心包,而不是硬编码列表
local ESSENTIAL_LARGE_PACKAGES=()
# 如果有 CLI 指定的核心包,优先使用
if [ -n "${CORE_PACKAGE_ARRAY:-}" ] && [ ${#CORE_PACKAGE_ARRAY[@]} -gt 0 ]; then
ESSENTIAL_LARGE_PACKAGES=("${CORE_PACKAGE_ARRAY[@]}")
info "? 使用 CLI 指定的核心包: ${ESSENTIAL_LARGE_PACKAGES[*]}"
else
# 回退到默认的大型包列表
ESSENTIAL_LARGE_PACKAGES=(
"torch" "torchvision" "torchaudio"
"tensorflow" "transformers" "diffusers"
"opencv-python" "pillow"
)
info "? 使用默认大型包列表"
fi
# 检查是否有任何核心包需要安装
local large_packages_needed=()
# ?️ 应用保护过滤:从安装列表中移除已保护的核心包
local filtered_packages=()
readarray -t filtered_packages < <(filter_installed_packages "${ESSENTIAL_LARGE_PACKAGES[@]}")
# 进一步过滤:移除已保护的核心包,防止依赖冲突重装
for pkg in "${filtered_packages[@]}"; do
local is_protected=false
for protected_pkg in "${PROTECTED_CORE_PACKAGES[@]}"; do
if [ "$pkg" = "$protected_pkg" ]; then
info "?️ 跳过已保护核心包: $pkg (防止依赖重装)"
is_protected=true
break
fi
done
if [ "$is_protected" = false ]; then
large_packages_needed+=("$pkg")
fi
done
if [ ${#large_packages_needed[@]} -eq 0 ]; then
ok "? 所有核心大型包都已安装,跳过专项安装阶段"
return 0
fi
# 给用户明确的提示
info "? 检测到需要安装的核心大型包 (${#large_packages_needed[@]}个):"
for pkg in "${large_packages_needed[@]}"; do
case "$pkg" in
torch|torchvision|torchaudio)
info " ? $pkg - PyTorch生态系统 (~200-500MB)"
;;
tensorflow)
info " ? $pkg - TensorFlow框架 (~400MB)"
;;
transformers)
info " ? $pkg - Hugging Face模型库 (~100MB)"
;;
diffusers)
info " ? $pkg - 扩散模型库 (~50MB)"
;;
opencv-python)
info " ?️ $pkg - 计算机视觉库 (~90MB)"
;;
*)
info " ? $pkg - 核心依赖包"
;;
esac
done
info "⚠️ 注意: 这些包下载量较大,预计总下载量: 500MB-2GB"
info "? 将使用最快的镜像源进行下载"
info "⏰ 预计耗时: 2-15分钟 (取决于网络速度)"
info "? 下载过程中会显示详细的进度和速度信息"
echo ""
# 让用户有心理准备
read -t 10 -p "⏳ 10秒后开始下载,按回车立即开始,或按Ctrl+C取消: " || true
echo ""
# 分批安装大型包 (避免内存压力)
local batch_size=2
local total_packages=${#large_packages_needed[@]}
local current_batch=1
local total_batches=$(( (total_packages + batch_size - 1) / batch_size ))
for ((i=0; i /dev/null; then
has_gpu=true
info "✅ 检测到 GPU 环境"
else
info "ℹ️ 未检测到 GPU 环境,跳过 GPU 版本验证"
return 0
fi
# 检查 PyTorch 是否支持 CUDA
local cuda_check_output=""
cuda_check_output=$(python -c "
import torch
print(f'PyTorch版本: {torch.__version__}')
print(f'CUDA可用: {torch.cuda.is_available()}')
if torch.cuda.is_available():
print(f'CUDA版本: {torch.version.cuda}')
print(f'GPU设备: {torch.cuda.get_device_name(0)}')
print('GPU_SUPPORTED')
elif '+cu' in torch.__version__ or 'cuda' in torch.__version__.lower():
print('GPU版本已安装但CUDA不可用(可能是驱动问题)')
print('GPU_VERSION_INSTALLED')
else:
print('CPU_ON***')
" 2>&1)
local cuda_check_exit_code=$?
if [ $cuda_check_exit_code -ne 0 ]; then
trace_error "PyTorch 导入失败" "$cuda_check_output" "$cuda_check_exit_code"
error "❌ PyTorch 导入失败,请检查安装"
return 1
fi
echo "$cuda_check_output"
# 检查是否支持 GPU 或已安装 GPU 版本
if echo "$cuda_check_output" | grep -q "GPU_SUPPORTED"; then
ok "✅ PyTorch GPU 版本验证成功"
return 0
elif echo "$cuda_check_output" | grep -q "GPU_VERSION_INSTALLED"; then
warn "⚠️ PyTorch GPU版本已安装但CUDA不可用,可能是驱动问题"
ok "✅ PyTorch GPU 版本已正确安装,跳过重装"
return 0
elif echo "$cuda_check_output" | grep -q "CPU_ON***"; then
warn "⚠️ 检测到 CPU 版本的 PyTorch,需要重新安装 GPU 版本"
# 强制重新安装 GPU 版本
info "? 正在重新安装 GPU 版本的 PyTorch..."
# 先卸载 CPU 版本
info "? 卸载当前版本..."
pip uninstall torch torchvision torchaudio -y >/dev/null 2>&1 || true
# 重新安装 GPU 版本
if install_torch_gpu; then
# 再次验证
local recheck_output=""
recheck_output=$(python -c "
import torch
print(f'重新安装后 - PyTorch版本: {torch.__version__}')
print(f'CUDA可用: {torch.cuda.is_available()}')
if torch.cuda.is_available():
print(f'GPU设备: {torch.cuda.get_device_name(0)}')
print('RECHECK_GPU_OK')
else:
print('RECHECK_STILL_CPU')
" 2>&1)
if echo "$recheck_output" | grep -q "RECHECK_GPU_OK"; then
ok "✅ GPU 版本 PyTorch 重新安装成功"
echo "$recheck_output"
return 0
else
trace_error "PyTorch GPU 重新安装验证失败" "$recheck_output"
error "❌ 重新安装后仍然不支持 GPU,请检查 CUDA 环境"
return 1
fi
else
error "❌ GPU 版本 PyTorch 重新安装失败"
return 1
fi
else
trace_error "PyTorch GPU 验证异常" "$cuda_check_output"
error "❌ PyTorch GPU 验证结果异常"
return 1
fi
}
# 执行最终验证
if ! final_gpu_verification; then
error "❌ PyTorch GPU 最终验证失败"
fi
info "? 环境配置完成!"
# -----------------------------
# 9. ✅ 最终成功提示(终端彩色 + 日志纯文本)
# -----------------------------
# 调用时先获取
get_activate_cmd "$ENV_NAME" "$ENV_PATH"
log_success "$PLATFORM" "$ENV_NAME" "$LOG_FILE" "$WORKSPACE" "$ACTIVATE_CMD" "$DETECTED_ENV_TYPE"
20250905 0445 ...