404 lines
16 KiB
Python
404 lines
16 KiB
Python
import importlib
|
||
import importlib.util
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from packaging import version
|
||
from packaging.requirements import Requirement
|
||
|
||
from src.common.logger import get_logger
|
||
from src.plugin_system.base.component_types import PythonDependency
|
||
from src.plugin_system.utils.dependency_alias import INSTALL_NAME_TO_IMPORT_NAME
|
||
|
||
logger = get_logger("dependency_manager")
|
||
|
||
|
||
class VenvDetector:
|
||
"""虚拟环境检测器"""
|
||
|
||
@staticmethod
|
||
def detect_venv_type() -> str | None:
|
||
"""
|
||
检测虚拟环境类型
|
||
返回: 'uv' | 'venv' | 'conda' | None
|
||
"""
|
||
# 检查是否在虚拟环境中
|
||
in_venv = hasattr(sys, "real_prefix") or (
|
||
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
|
||
)
|
||
|
||
if not in_venv:
|
||
logger.warning("当前不在虚拟环境中")
|
||
return None
|
||
|
||
venv_path = Path(sys.prefix)
|
||
|
||
# 1. 检测 uv (优先检查 pyvenv.cfg 文件)
|
||
pyvenv_cfg = venv_path / "pyvenv.cfg"
|
||
if pyvenv_cfg.exists():
|
||
try:
|
||
with open(pyvenv_cfg, encoding="utf-8") as f:
|
||
content = f.read()
|
||
if "uv = " in content:
|
||
logger.info("检测到 uv 虚拟环境")
|
||
return "uv"
|
||
except Exception as e:
|
||
logger.warning(f"读取 pyvenv.cfg 失败: {e}")
|
||
|
||
# 2. 检测 conda (检查环境变量和路径)
|
||
if "CONDA_DEFAULT_ENV" in os.environ or "CONDA_PREFIX" in os.environ:
|
||
logger.info("检测到 conda 虚拟环境")
|
||
return "conda"
|
||
|
||
# 通过路径特征检测 conda
|
||
if "conda" in str(venv_path).lower() or "anaconda" in str(venv_path).lower():
|
||
logger.info(f"检测到 conda 虚拟环境 (路径: {venv_path})")
|
||
return "conda"
|
||
|
||
# 3. 默认为 venv (标准 Python 虚拟环境)
|
||
logger.info(f"检测到标准 venv 虚拟环境 (路径: {venv_path})")
|
||
return "venv"
|
||
|
||
@staticmethod
|
||
def get_install_command(venv_type: str | None) -> list[str]:
|
||
"""
|
||
根据虚拟环境类型获取安装命令
|
||
|
||
Args:
|
||
venv_type: 虚拟环境类型 ('uv' | 'venv' | 'conda' | None)
|
||
|
||
Returns:
|
||
安装命令列表 (不包括包名)
|
||
"""
|
||
if venv_type == "uv":
|
||
# 检查 uv 是否可用
|
||
uv_path = shutil.which("uv")
|
||
if uv_path:
|
||
logger.debug("使用 uv pip 安装")
|
||
return [uv_path, "pip", "install"]
|
||
else:
|
||
logger.warning("未找到 uv 命令,回退到标准 pip")
|
||
return [sys.executable, "-m", "pip", "install"]
|
||
|
||
elif venv_type == "conda":
|
||
# 获取当前 conda 环境名
|
||
conda_env = os.environ.get("CONDA_DEFAULT_ENV")
|
||
if conda_env:
|
||
logger.debug(f"使用 conda 在环境 {conda_env} 中安装")
|
||
return ["conda", "install", "-n", conda_env, "-y"]
|
||
else:
|
||
logger.warning("未找到 conda 环境名,回退到 pip")
|
||
return [sys.executable, "-m", "pip", "install"]
|
||
|
||
else:
|
||
# 默认使用 pip
|
||
logger.debug("使用标准 pip 安装")
|
||
return [sys.executable, "-m", "pip", "install"]
|
||
class DependencyManager:
|
||
"""Python包依赖管理器 (整合配置和虚拟环境检测)
|
||
|
||
负责检查和自动安装插件的Python包依赖
|
||
"""
|
||
|
||
def __init__(self, auto_install: bool = True, use_mirror: bool = False, mirror_url: str | None = None):
|
||
"""初始化依赖管理器
|
||
|
||
Args:
|
||
auto_install: 是否自动安装缺失的依赖
|
||
use_mirror: 是否使用PyPI镜像源
|
||
mirror_url: PyPI镜像源URL
|
||
"""
|
||
# 延迟导入配置以避免循环依赖
|
||
try:
|
||
from src.config.config import global_config
|
||
|
||
dep_config = global_config.dependency_management
|
||
# 优先使用配置文件中的设置,参数作为覆盖
|
||
self.auto_install = dep_config.auto_install if auto_install is True else auto_install
|
||
self.use_mirror = dep_config.use_mirror if use_mirror is False else use_mirror
|
||
self.mirror_url = dep_config.mirror_url if mirror_url is None else mirror_url
|
||
self.install_timeout = dep_config.auto_install_timeout
|
||
self.prompt_before_install = dep_config.prompt_before_install
|
||
|
||
except Exception as e:
|
||
logger.warning(f"无法加载依赖配置,使用默认设置: {e}")
|
||
self.auto_install = auto_install
|
||
self.use_mirror = use_mirror or False
|
||
self.mirror_url = mirror_url or ""
|
||
self.install_timeout = 300
|
||
self.prompt_before_install = False
|
||
|
||
# 检测虚拟环境类型
|
||
self.venv_type = VenvDetector.detect_venv_type()
|
||
if self.venv_type:
|
||
logger.info(f"依赖管理器初始化完成,虚拟环境类型: {self.venv_type}")
|
||
else:
|
||
logger.warning("依赖管理器初始化完成,但未检测到虚拟环境")
|
||
# ========== 依赖检查和安装核心方法 ==========
|
||
|
||
def check_dependencies(self, dependencies: Any, plugin_name: str = "") -> tuple[bool, list[str], list[str]]:
|
||
"""检查依赖包是否满足要求
|
||
|
||
Args:
|
||
dependencies: 依赖列表,支持字符串或PythonDependency对象
|
||
plugin_name: 插件名称,用于日志记录
|
||
|
||
Returns:
|
||
Tuple[bool, List[str], List[str]]: (是否全部满足, 缺失的包, 错误信息)
|
||
"""
|
||
missing_packages = []
|
||
error_messages = []
|
||
log_prefix = f"[Plugin:{plugin_name}] " if plugin_name else ""
|
||
|
||
# 标准化依赖格式
|
||
normalized_deps = self._normalize_dependencies(dependencies)
|
||
|
||
for dep in normalized_deps:
|
||
try:
|
||
if not self._check_single_dependency(dep):
|
||
logger.info(f"{log_prefix}缺少依赖包: {dep.get_pip_requirement()}")
|
||
missing_packages.append(dep.get_pip_requirement())
|
||
except Exception as e:
|
||
error_msg = f"检查依赖 {dep.package_name} 时发生错误: {e!s}"
|
||
error_messages.append(error_msg)
|
||
logger.error(f"{log_prefix}{error_msg}")
|
||
|
||
all_satisfied = len(missing_packages) == 0 and len(error_messages) == 0
|
||
|
||
if all_satisfied:
|
||
logger.debug(f"{log_prefix}所有Python依赖检查通过")
|
||
else:
|
||
logger.warning(
|
||
f"{log_prefix}Python依赖检查失败: 缺失{len(missing_packages)}个包, {len(error_messages)}个错误"
|
||
)
|
||
|
||
return all_satisfied, missing_packages, error_messages
|
||
|
||
def install_dependencies(self, packages: list[str], plugin_name: str = "") -> tuple[bool, list[str]]:
|
||
"""自动安装缺失的依赖包
|
||
|
||
Args:
|
||
packages: 要安装的包列表
|
||
plugin_name: 插件名称,用于日志记录
|
||
|
||
Returns:
|
||
Tuple[bool, List[str]]: (是否全部安装成功, 失败的包列表)
|
||
"""
|
||
if not packages:
|
||
return True, []
|
||
|
||
if not self.auto_install:
|
||
logger.info(f"[Plugin:{plugin_name}] 自动安装已禁用,跳过安装: {packages}")
|
||
return False, packages
|
||
|
||
log_prefix = f"[Plugin:{plugin_name}] " if plugin_name else ""
|
||
logger.info(f"{log_prefix}开始自动安装Python依赖: {packages}")
|
||
|
||
failed_packages = []
|
||
|
||
for package in packages:
|
||
try:
|
||
if self._install_single_package(package, plugin_name):
|
||
logger.info(f"{log_prefix} 成功安装: {package}")
|
||
else:
|
||
failed_packages.append(package)
|
||
logger.error(f"{log_prefix} 安装失败: {package}")
|
||
except Exception as e:
|
||
failed_packages.append(package)
|
||
logger.error(f"{log_prefix} 安装 {package} 时发生异常: {e!s}")
|
||
|
||
success = len(failed_packages) == 0
|
||
if success:
|
||
logger.info(f"{log_prefix} 所有依赖安装完成")
|
||
else:
|
||
logger.error(f"{log_prefix} 部分依赖安装失败: {failed_packages}")
|
||
|
||
return success, failed_packages
|
||
|
||
def check_and_install_dependencies(self, dependencies: Any, plugin_name: str = "") -> tuple[bool, list[str]]:
|
||
"""检查并自动安装依赖(组合操作)
|
||
|
||
Args:
|
||
dependencies: 依赖列表
|
||
plugin_name: 插件名称
|
||
|
||
Returns:
|
||
Tuple[bool, List[str]]: (是否全部满足, 错误信息列表)
|
||
"""
|
||
# 第一步:检查依赖
|
||
all_satisfied, missing_packages, check_errors = self.check_dependencies(dependencies, plugin_name)
|
||
|
||
if all_satisfied:
|
||
return True, []
|
||
|
||
all_errors = check_errors.copy()
|
||
|
||
# 第二步:尝试安装缺失的包
|
||
if missing_packages and self.auto_install:
|
||
install_success, failed_packages = self.install_dependencies(missing_packages, plugin_name)
|
||
|
||
if not install_success:
|
||
all_errors.extend([f"安装失败: {pkg}" for pkg in failed_packages])
|
||
else:
|
||
# 安装成功后重新检查
|
||
recheck_satisfied, recheck_missing, recheck_errors = self.check_dependencies(dependencies, plugin_name)
|
||
if not recheck_satisfied:
|
||
all_errors.extend(recheck_errors)
|
||
all_errors.extend([f"安装后仍缺失: {pkg}" for pkg in recheck_missing])
|
||
else:
|
||
return True, []
|
||
else:
|
||
all_errors.extend([f"缺失依赖: {pkg}" for pkg in missing_packages])
|
||
|
||
return False, all_errors
|
||
|
||
@staticmethod
|
||
def _normalize_dependencies(dependencies: Any) -> list[PythonDependency]:
|
||
"""将依赖列表标准化为PythonDependency对象"""
|
||
normalized = []
|
||
|
||
for dep in dependencies:
|
||
if isinstance(dep, str):
|
||
# 解析字符串格式的依赖
|
||
try:
|
||
# 尝试解析为requirement格式 (如 "package>=1.0.0")
|
||
req = Requirement(dep)
|
||
version_spec = str(req.specifier) if req.specifier else ""
|
||
|
||
normalized.append(
|
||
PythonDependency(
|
||
package_name=req.name,
|
||
version=version_spec,
|
||
install_name=dep, # 保持原始的安装名称
|
||
)
|
||
)
|
||
except Exception:
|
||
# 如果解析失败,作为简单包名处理
|
||
normalized.append(PythonDependency(package_name=dep, install_name=dep))
|
||
elif isinstance(dep, PythonDependency):
|
||
normalized.append(dep)
|
||
else:
|
||
logger.warning(f"未知的依赖格式: {dep}")
|
||
|
||
return normalized
|
||
|
||
@staticmethod
|
||
def _check_single_dependency(dep: PythonDependency) -> bool:
|
||
"""检查单个依赖是否满足要求"""
|
||
|
||
def _try_check(import_name: str) -> bool:
|
||
"""尝试使用给定的导入名进行检查"""
|
||
try:
|
||
spec = importlib.util.find_spec(import_name)
|
||
if spec is None:
|
||
return False
|
||
|
||
# 如果没有版本要求,导入成功就够了
|
||
if not dep.version:
|
||
return True
|
||
|
||
# 检查版本要求
|
||
try:
|
||
module = importlib.import_module(import_name)
|
||
installed_version = getattr(module, "__version__", None)
|
||
|
||
if installed_version is None:
|
||
# 尝试其他常见的版本属性
|
||
installed_version = getattr(module, "VERSION", None)
|
||
if installed_version is None:
|
||
logger.debug(f"无法获取包 {import_name} 的版本信息,假设满足要求")
|
||
return True
|
||
|
||
# 解析版本要求
|
||
req = Requirement(f"{dep.package_name}{dep.version}")
|
||
return version.parse(str(installed_version)) in req.specifier
|
||
|
||
except Exception as e:
|
||
logger.debug(f"检查包 {import_name} 版本时出错: {e}")
|
||
return True # 如果无法检查版本,假设满足要求
|
||
|
||
except ImportError:
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"检查依赖 {import_name} 时发生未知错误: {e}")
|
||
return False
|
||
|
||
# 1. 首先尝试使用原始的 package_name 进行检查
|
||
if _try_check(dep.package_name):
|
||
return True
|
||
|
||
# 2. 如果失败,查询别名映射表
|
||
# 注意:此时 dep.package_name 可能是 simple "requests" 或 "beautifulsoup4"
|
||
import_alias = INSTALL_NAME_TO_IMPORT_NAME.get(dep.package_name)
|
||
if import_alias:
|
||
logger.debug(f"依赖 '{dep.package_name}' 导入失败, 尝试使用别名 '{import_alias}'")
|
||
if _try_check(import_alias):
|
||
return True
|
||
|
||
# 3. 如果别名也失败了,或者没有别名,最终确认失败
|
||
return False
|
||
|
||
def _install_single_package(self, package: str, plugin_name: str = "") -> bool:
|
||
"""安装单个包 (支持虚拟环境自动检测)"""
|
||
try:
|
||
log_prefix = f"[Plugin:{plugin_name}] " if plugin_name else ""
|
||
|
||
# 根据虚拟环境类型构建安装命令
|
||
cmd = VenvDetector.get_install_command(self.venv_type)
|
||
cmd.append(package)
|
||
|
||
# 添加镜像源设置 (仅对 pip/uv 有效)
|
||
if self.use_mirror and self.mirror_url and "pip" in cmd:
|
||
cmd.extend(["-i", self.mirror_url])
|
||
logger.debug(f"{log_prefix}使用PyPI镜像源: {self.mirror_url}")
|
||
|
||
logger.info(f"{log_prefix}执行安装命令: {' '.join(cmd)}")
|
||
|
||
result = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="ignore",
|
||
timeout=self.install_timeout,
|
||
check=False,
|
||
)
|
||
|
||
if result.returncode == 0:
|
||
logger.info(f"{log_prefix}安装成功: {package}")
|
||
return True
|
||
else:
|
||
logger.error(f"{log_prefix}安装失败: {result.stderr}")
|
||
return False
|
||
|
||
except subprocess.TimeoutExpired:
|
||
logger.error(f"[Plugin:{plugin_name}] 安装 {package} 超时")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"[Plugin:{plugin_name}] 安装 {package} 时发生异常: {e}")
|
||
return False
|
||
|
||
|
||
# 全局依赖管理器实例
|
||
_global_dependency_manager: DependencyManager | None = None
|
||
|
||
|
||
def get_dependency_manager() -> DependencyManager:
|
||
"""获取全局依赖管理器实例"""
|
||
global _global_dependency_manager
|
||
if _global_dependency_manager is None:
|
||
_global_dependency_manager = DependencyManager()
|
||
return _global_dependency_manager
|
||
|
||
|
||
def configure_dependency_manager(auto_install: bool = True, use_mirror: bool = False, mirror_url: str | None = None):
|
||
"""配置全局依赖管理器"""
|
||
global _global_dependency_manager
|
||
_global_dependency_manager = DependencyManager(
|
||
auto_install=auto_install, use_mirror=use_mirror, mirror_url=mirror_url
|
||
)
|