mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 19:08:21 +00:00
Merge branch 'dev' into prcix9320
This commit is contained in:
@@ -6,54 +6,199 @@
|
||||
import argparse
|
||||
import importlib
|
||||
import locale
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from unilabos.utils.banner_print import print_status
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 底层安装工具
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _is_chinese_locale() -> bool:
|
||||
try:
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
return bool(lang and ("zh" in lang.lower() or "chinese" in lang.lower()))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
_USE_UV: Optional[bool] = None
|
||||
|
||||
|
||||
def _has_uv() -> bool:
|
||||
global _USE_UV
|
||||
if _USE_UV is None:
|
||||
_USE_UV = shutil.which("uv") is not None
|
||||
return _USE_UV
|
||||
|
||||
|
||||
def _install_packages(
|
||||
packages: List[str],
|
||||
upgrade: bool = False,
|
||||
label: str = "",
|
||||
) -> bool:
|
||||
"""
|
||||
安装/升级一组包。优先 uv pip install,回退 sys pip。
|
||||
逐个安装,任意一个失败不影响后续包。
|
||||
|
||||
Returns:
|
||||
True if all succeeded, False otherwise.
|
||||
"""
|
||||
if not packages:
|
||||
return True
|
||||
|
||||
is_chinese = _is_chinese_locale()
|
||||
use_uv = _has_uv()
|
||||
failed: List[str] = []
|
||||
|
||||
for pkg in packages:
|
||||
action_word = "升级" if upgrade else "安装"
|
||||
if label:
|
||||
print_status(f"[{label}] 正在{action_word} {pkg}...", "info")
|
||||
else:
|
||||
print_status(f"正在{action_word} {pkg}...", "info")
|
||||
|
||||
if use_uv:
|
||||
cmd = ["uv", "pip", "install"]
|
||||
if upgrade:
|
||||
cmd.append("--upgrade")
|
||||
cmd.append(pkg)
|
||||
if is_chinese:
|
||||
cmd.extend(["--index-url", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
||||
else:
|
||||
cmd = [sys.executable, "-m", "pip", "install"]
|
||||
if upgrade:
|
||||
cmd.append("--upgrade")
|
||||
cmd.append(pkg)
|
||||
if is_chinese:
|
||||
cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
if result.returncode == 0:
|
||||
installer = "uv" if use_uv else "pip"
|
||||
print_status(f"✓ {pkg} {action_word}成功 (via {installer})", "success")
|
||||
else:
|
||||
stderr_short = result.stderr.strip().split("\n")[-1] if result.stderr else "unknown error"
|
||||
print_status(f"× {pkg} {action_word}失败: {stderr_short}", "error")
|
||||
failed.append(pkg)
|
||||
except subprocess.TimeoutExpired:
|
||||
print_status(f"× {pkg} {action_word}超时 (300s)", "error")
|
||||
failed.append(pkg)
|
||||
except Exception as e:
|
||||
print_status(f"× {pkg} {action_word}异常: {e}", "error")
|
||||
failed.append(pkg)
|
||||
|
||||
if failed:
|
||||
print_status(f"有 {len(failed)} 个包操作失败: {', '.join(failed)}", "error")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# requirements.txt 安装(可多次调用)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def install_requirements_txt(req_path: str | Path, label: str = "") -> bool:
|
||||
"""
|
||||
读取一个 requirements.txt 文件,检查缺失的包并安装。
|
||||
|
||||
Args:
|
||||
req_path: requirements.txt 文件路径
|
||||
label: 日志前缀标签(如 "device_package_sim")
|
||||
|
||||
Returns:
|
||||
True if all ok, False if any install failed.
|
||||
"""
|
||||
req_path = Path(req_path)
|
||||
if not req_path.exists():
|
||||
return True
|
||||
|
||||
tag = label or req_path.parent.name
|
||||
print_status(f"[{tag}] 检查依赖: {req_path}", "info")
|
||||
|
||||
reqs: List[str] = []
|
||||
with open(req_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and not line.startswith("-"):
|
||||
reqs.append(line)
|
||||
|
||||
if not reqs:
|
||||
return True
|
||||
|
||||
missing: List[str] = []
|
||||
for req in reqs:
|
||||
pkg_import = req.split(">=")[0].split("==")[0].split("<")[0].split("[")[0].split(">")[0].strip()
|
||||
pkg_import = pkg_import.replace("-", "_")
|
||||
try:
|
||||
importlib.import_module(pkg_import)
|
||||
except ImportError:
|
||||
missing.append(req)
|
||||
|
||||
if not missing:
|
||||
print_status(f"[{tag}] ✓ 依赖检查通过 ({len(reqs)} 个包)", "success")
|
||||
return True
|
||||
|
||||
print_status(f"[{tag}] 缺失 {len(missing)} 个依赖: {', '.join(missing)}", "warning")
|
||||
return _install_packages(missing, label=tag)
|
||||
|
||||
|
||||
def check_device_package_requirements(devices_dirs: list[str]) -> bool:
|
||||
"""
|
||||
检查 --devices 指定的所有外部设备包目录中的 requirements.txt。
|
||||
对每个目录查找 requirements.txt(先在目录内找,再在父目录找)。
|
||||
"""
|
||||
if not devices_dirs:
|
||||
return True
|
||||
|
||||
all_ok = True
|
||||
for d in devices_dirs:
|
||||
d_path = Path(d).resolve()
|
||||
req_file = d_path / "requirements.txt"
|
||||
if not req_file.exists():
|
||||
req_file = d_path.parent / "requirements.txt"
|
||||
if not req_file.exists():
|
||||
continue
|
||||
if not install_requirements_txt(req_file, label=d_path.name):
|
||||
all_ok = False
|
||||
|
||||
return all_ok
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UniLabOS 核心环境检查
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class EnvironmentChecker:
|
||||
"""环境检查器"""
|
||||
|
||||
def __init__(self):
|
||||
# 定义必需的包及其安装名称的映射
|
||||
self.required_packages = {
|
||||
# 包导入名 : pip安装名
|
||||
# "pymodbus.framer.FramerType": "pymodbus==3.9.2",
|
||||
"websockets": "websockets",
|
||||
"msgcenterpy": "msgcenterpy",
|
||||
"orjson": "orjson",
|
||||
"opentrons_shared_data": "opentrons_shared_data",
|
||||
"typing_extensions": "typing_extensions",
|
||||
"crcmod": "crcmod-plus",
|
||||
}
|
||||
|
||||
# 特殊安装包(需要特殊处理的包)
|
||||
self.special_packages = {"pylabrobot": "git+https://github.com/Xuwznln/pylabrobot.git"}
|
||||
|
||||
# 包版本要求(包名: 最低版本)
|
||||
self.version_requirements = {
|
||||
"msgcenterpy": "0.1.5", # msgcenterpy 最低版本要求
|
||||
"msgcenterpy": "0.1.8",
|
||||
}
|
||||
|
||||
self.missing_packages = []
|
||||
self.failed_installs = []
|
||||
self.packages_need_upgrade = []
|
||||
|
||||
# 检测系统语言
|
||||
self.is_chinese = self._is_chinese_locale()
|
||||
|
||||
def _is_chinese_locale(self) -> bool:
|
||||
"""检测系统是否为中文环境"""
|
||||
try:
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
if lang and ("zh" in lang.lower() or "chinese" in lang.lower()):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
self.missing_packages: List[tuple] = []
|
||||
self.failed_installs: List[tuple] = []
|
||||
self.packages_need_upgrade: List[tuple] = []
|
||||
|
||||
def check_package_installed(self, package_name: str) -> bool:
|
||||
"""检查包是否已安装"""
|
||||
try:
|
||||
importlib.import_module(package_name)
|
||||
return True
|
||||
@@ -61,7 +206,6 @@ class EnvironmentChecker:
|
||||
return False
|
||||
|
||||
def get_package_version(self, package_name: str) -> str | None:
|
||||
"""获取已安装包的版本"""
|
||||
try:
|
||||
module = importlib.import_module(package_name)
|
||||
return getattr(module, "__version__", None)
|
||||
@@ -69,88 +213,32 @@ class EnvironmentChecker:
|
||||
return None
|
||||
|
||||
def compare_version(self, current: str, required: str) -> bool:
|
||||
"""
|
||||
比较版本号
|
||||
Returns:
|
||||
True: current >= required
|
||||
False: current < required
|
||||
"""
|
||||
try:
|
||||
current_parts = [int(x) for x in current.split(".")]
|
||||
required_parts = [int(x) for x in required.split(".")]
|
||||
|
||||
# 补齐长度
|
||||
max_len = max(len(current_parts), len(required_parts))
|
||||
current_parts.extend([0] * (max_len - len(current_parts)))
|
||||
required_parts.extend([0] * (max_len - len(required_parts)))
|
||||
|
||||
return current_parts >= required_parts
|
||||
except Exception:
|
||||
return True # 如果无法比较,假设版本满足要求
|
||||
|
||||
def install_package(self, package_name: str, pip_name: str, upgrade: bool = False) -> bool:
|
||||
"""安装包"""
|
||||
try:
|
||||
action = "升级" if upgrade else "安装"
|
||||
print_status(f"正在{action} {package_name} ({pip_name})...", "info")
|
||||
|
||||
# 构建安装命令
|
||||
cmd = [sys.executable, "-m", "pip", "install"]
|
||||
|
||||
# 如果是升级操作,添加 --upgrade 参数
|
||||
if upgrade:
|
||||
cmd.append("--upgrade")
|
||||
|
||||
cmd.append(pip_name)
|
||||
|
||||
# 如果是中文环境,使用清华镜像源
|
||||
if self.is_chinese:
|
||||
cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
||||
|
||||
# 执行安装
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) # 5分钟超时
|
||||
|
||||
if result.returncode == 0:
|
||||
print_status(f"✓ {package_name} {action}成功", "success")
|
||||
return True
|
||||
else:
|
||||
print_status(f"× {package_name} {action}失败: {result.stderr}", "error")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print_status(f"× {package_name} {action}超时", "error")
|
||||
return False
|
||||
except Exception as e:
|
||||
print_status(f"× {package_name} {action}异常: {str(e)}", "error")
|
||||
return False
|
||||
|
||||
def upgrade_package(self, package_name: str, pip_name: str) -> bool:
|
||||
"""升级包"""
|
||||
return self.install_package(package_name, pip_name, upgrade=True)
|
||||
return True
|
||||
|
||||
def check_all_packages(self) -> bool:
|
||||
"""检查所有必需的包"""
|
||||
print_status("开始检查环境依赖...", "info")
|
||||
|
||||
# 检查常规包
|
||||
for import_name, pip_name in self.required_packages.items():
|
||||
if not self.check_package_installed(import_name):
|
||||
self.missing_packages.append((import_name, pip_name))
|
||||
else:
|
||||
# 检查版本要求
|
||||
if import_name in self.version_requirements:
|
||||
current_version = self.get_package_version(import_name)
|
||||
required_version = self.version_requirements[import_name]
|
||||
elif import_name in self.version_requirements:
|
||||
current_version = self.get_package_version(import_name)
|
||||
required_version = self.version_requirements[import_name]
|
||||
if current_version and not self.compare_version(current_version, required_version):
|
||||
print_status(
|
||||
f"{import_name} 版本过低 (当前: {current_version}, 需要: >={required_version})",
|
||||
"warning",
|
||||
)
|
||||
self.packages_need_upgrade.append((import_name, pip_name))
|
||||
|
||||
if current_version:
|
||||
if not self.compare_version(current_version, required_version):
|
||||
print_status(
|
||||
f"{import_name} 版本过低 (当前: {current_version}, 需要: >={required_version})",
|
||||
"warning",
|
||||
)
|
||||
self.packages_need_upgrade.append((import_name, pip_name))
|
||||
|
||||
# 检查特殊包
|
||||
for package_name, install_url in self.special_packages.items():
|
||||
if not self.check_package_installed(package_name):
|
||||
self.missing_packages.append((package_name, install_url))
|
||||
@@ -169,7 +257,6 @@ class EnvironmentChecker:
|
||||
return False
|
||||
|
||||
def install_missing_packages(self, auto_install: bool = True) -> bool:
|
||||
"""安装缺失的包"""
|
||||
if not self.missing_packages and not self.packages_need_upgrade:
|
||||
return True
|
||||
|
||||
@@ -177,62 +264,36 @@ class EnvironmentChecker:
|
||||
if self.missing_packages:
|
||||
print_status("缺失以下包:", "warning")
|
||||
for import_name, pip_name in self.missing_packages:
|
||||
print_status(f" - {import_name} (pip install {pip_name})", "warning")
|
||||
print_status(f" - {import_name} ({pip_name})", "warning")
|
||||
if self.packages_need_upgrade:
|
||||
print_status("需要升级以下包:", "warning")
|
||||
for import_name, pip_name in self.packages_need_upgrade:
|
||||
print_status(f" - {import_name} (pip install --upgrade {pip_name})", "warning")
|
||||
print_status(f" - {import_name} ({pip_name})", "warning")
|
||||
return False
|
||||
|
||||
# 安装缺失的包
|
||||
if self.missing_packages:
|
||||
print_status(f"开始自动安装 {len(self.missing_packages)} 个缺失的包...", "info")
|
||||
pkgs = [pip_name for _, pip_name in self.missing_packages]
|
||||
if not _install_packages(pkgs, label="unilabos"):
|
||||
self.failed_installs.extend(self.missing_packages)
|
||||
|
||||
success_count = 0
|
||||
for import_name, pip_name in self.missing_packages:
|
||||
if self.install_package(import_name, pip_name):
|
||||
success_count += 1
|
||||
else:
|
||||
self.failed_installs.append((import_name, pip_name))
|
||||
|
||||
print_status(f"✓ 成功安装 {success_count}/{len(self.missing_packages)} 个包", "success")
|
||||
|
||||
# 升级需要更新的包
|
||||
if self.packages_need_upgrade:
|
||||
print_status(f"开始自动升级 {len(self.packages_need_upgrade)} 个包...", "info")
|
||||
pkgs = [pip_name for _, pip_name in self.packages_need_upgrade]
|
||||
if not _install_packages(pkgs, upgrade=True, label="unilabos"):
|
||||
self.failed_installs.extend(self.packages_need_upgrade)
|
||||
|
||||
upgrade_success_count = 0
|
||||
for import_name, pip_name in self.packages_need_upgrade:
|
||||
if self.upgrade_package(import_name, pip_name):
|
||||
upgrade_success_count += 1
|
||||
else:
|
||||
self.failed_installs.append((import_name, pip_name))
|
||||
|
||||
print_status(f"✓ 成功升级 {upgrade_success_count}/{len(self.packages_need_upgrade)} 个包", "success")
|
||||
|
||||
if self.failed_installs:
|
||||
print_status(f"有 {len(self.failed_installs)} 个包操作失败:", "error")
|
||||
for import_name, pip_name in self.failed_installs:
|
||||
print_status(f" - {import_name} ({pip_name})", "error")
|
||||
return False
|
||||
|
||||
return True
|
||||
return not self.failed_installs
|
||||
|
||||
def verify_installation(self) -> bool:
|
||||
"""验证安装结果"""
|
||||
if not self.missing_packages and not self.packages_need_upgrade:
|
||||
return True
|
||||
|
||||
print_status("验证安装结果...", "info")
|
||||
|
||||
failed_verification = []
|
||||
|
||||
# 验证新安装的包
|
||||
for import_name, pip_name in self.missing_packages:
|
||||
if not self.check_package_installed(import_name):
|
||||
failed_verification.append((import_name, pip_name))
|
||||
|
||||
# 验证升级的包
|
||||
for import_name, pip_name in self.packages_need_upgrade:
|
||||
if not self.check_package_installed(import_name):
|
||||
failed_verification.append((import_name, pip_name))
|
||||
@@ -269,17 +330,14 @@ def check_environment(auto_install: bool = True, show_details: bool = True) -> b
|
||||
"""
|
||||
checker = EnvironmentChecker()
|
||||
|
||||
# 检查包
|
||||
if checker.check_all_packages():
|
||||
return True
|
||||
|
||||
# 安装缺失的包
|
||||
if not checker.install_missing_packages(auto_install):
|
||||
if show_details:
|
||||
print_status("请手动安装缺失的包后重新启动程序", "error")
|
||||
return False
|
||||
|
||||
# 验证安装
|
||||
if not checker.verify_installation():
|
||||
if show_details:
|
||||
print_status("安装验证失败,请检查网络连接或手动安装", "error")
|
||||
@@ -289,14 +347,12 @@ def check_environment(auto_install: bool = True, show_details: bool = True) -> b
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 命令行参数解析
|
||||
parser = argparse.ArgumentParser(description="UniLabOS 环境依赖检查工具")
|
||||
parser.add_argument("--no-auto-install", action="store_true", help="仅检查环境,不自动安装缺失的包")
|
||||
parser.add_argument("--silent", action="store_true", help="静默模式,不显示详细信息")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 执行环境检查
|
||||
auto_install = not args.no_auto_install
|
||||
show_details = not args.silent
|
||||
|
||||
|
||||
Reference in New Issue
Block a user