mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
fix(plugins): preserve dependency manifest sources (#6370)
This commit is contained in:
Vendored
+29
-13
@@ -14,7 +14,7 @@ import time
|
||||
import traceback
|
||||
import zipfile
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
from typing import Dict, List, Optional, Tuple, Set, Callable, Awaitable
|
||||
from typing import Dict, List, Optional, Tuple, Set, Callable, Awaitable, Sequence
|
||||
from urllib.parse import parse_qs, quote, unquote, urlparse, urlsplit
|
||||
|
||||
import aiofiles
|
||||
@@ -1461,7 +1461,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
@classmethod
|
||||
def __build_package_install_request(
|
||||
cls,
|
||||
dependency_file: Path,
|
||||
dependency_files: Path | Sequence[Path],
|
||||
find_links_dirs: Optional[List[Path]] = None,
|
||||
constraints_file: Optional[Path] = None,
|
||||
purpose: str = "plugin",
|
||||
@@ -1469,8 +1469,12 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
将 MoviePilot 运行配置转换为 uv 安装请求,统一缓存、镜像和代理语义。
|
||||
"""
|
||||
if isinstance(dependency_files, Path):
|
||||
resolved_dependency_files = (dependency_files,)
|
||||
else:
|
||||
resolved_dependency_files = tuple(Path(item) for item in dependency_files)
|
||||
return PackageInstallRequest(
|
||||
dependency_file=dependency_file,
|
||||
dependency_files=resolved_dependency_files,
|
||||
python_bin=Path(sys.executable),
|
||||
find_links_dirs=find_links_dirs or [],
|
||||
constraints_file=constraints_file,
|
||||
@@ -1583,18 +1587,26 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
|
||||
@classmethod
|
||||
def install_packages_with_fallback(cls,
|
||||
dependency_file: Path,
|
||||
dependency_files: Path | Sequence[Path],
|
||||
find_links_dirs: Optional[List[Path]] = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
使用自动降级策略安装依赖,并确保新安装的包可被动态导入
|
||||
:param dependency_file: 插件依赖清单路径
|
||||
:param dependency_files: 一个或多个插件依赖清单路径
|
||||
:param find_links_dirs: 额外的本地 wheels 目录列表
|
||||
:return: (是否成功, 错误信息)
|
||||
"""
|
||||
wheels_dir = dependency_file.parent / "wheels"
|
||||
if isinstance(dependency_files, Path):
|
||||
resolved_dependency_files = (dependency_files,)
|
||||
else:
|
||||
resolved_dependency_files = tuple(Path(item) for item in dependency_files)
|
||||
if not resolved_dependency_files:
|
||||
return False, "没有传入插件依赖清单"
|
||||
|
||||
candidate_dirs = []
|
||||
if wheels_dir.is_dir():
|
||||
candidate_dirs.append(wheels_dir)
|
||||
for dependency_file in resolved_dependency_files:
|
||||
wheels_dir = dependency_file.parent / "wheels"
|
||||
if wheels_dir.is_dir():
|
||||
candidate_dirs.append(wheels_dir)
|
||||
if find_links_dirs:
|
||||
candidate_dirs.extend(find_links_dirs)
|
||||
|
||||
@@ -1619,10 +1631,14 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
|
||||
installed_packages = cls.__get_installed_packages()
|
||||
protected_packages = cls.__get_protected_runtime_packages(installed_packages)
|
||||
check_ok, check_message = cls.__validate_runtime_dependency_conflicts(dependency_file, protected_packages)
|
||||
if not check_ok:
|
||||
logger.error(f"[UV] 运行环境冲突预检失败:{check_message}")
|
||||
return False, check_message
|
||||
for dependency_file in resolved_dependency_files:
|
||||
check_ok, check_message = cls.__validate_runtime_dependency_conflicts(
|
||||
dependency_file,
|
||||
protected_packages,
|
||||
)
|
||||
if not check_ok:
|
||||
logger.error(f"[UV] 运行环境冲突预检失败:{check_message}")
|
||||
return False, check_message
|
||||
|
||||
constraints_file = None
|
||||
if protected_packages:
|
||||
@@ -1633,7 +1649,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
return False, f"创建运行环境约束文件失败:{e}"
|
||||
|
||||
request = cls.__build_package_install_request(
|
||||
dependency_file,
|
||||
resolved_dependency_files,
|
||||
find_links_dirs=resolved_dirs,
|
||||
constraints_file=constraints_file,
|
||||
purpose="plugin",
|
||||
|
||||
@@ -10,10 +10,10 @@ from urllib.parse import urlsplit, urlunsplit
|
||||
@dataclass(frozen=True)
|
||||
class PackageInstallRequest:
|
||||
"""
|
||||
Python 包安装请求,集中描述依赖文件、工具缓存、代理和本地 wheels 候选源。
|
||||
Python 包安装请求,集中描述依赖清单、工具缓存、代理和本地 wheels 候选源。
|
||||
"""
|
||||
|
||||
dependency_file: Path
|
||||
dependency_files: tuple[Path, ...]
|
||||
python_bin: Path
|
||||
find_links_dirs: list[Path] = field(default_factory=list)
|
||||
constraints_file: Path | None = None
|
||||
@@ -92,7 +92,8 @@ def _base_install_args(request: PackageInstallRequest) -> list[str]:
|
||||
args.extend(["--find-links", str(directory)])
|
||||
if request.constraints_file:
|
||||
args.extend(["-c", str(request.constraints_file)])
|
||||
args.extend(["-r", str(request.dependency_file)])
|
||||
for dependency_file in request.dependency_files:
|
||||
args.extend(["-r", str(dependency_file)])
|
||||
return args
|
||||
|
||||
|
||||
@@ -119,11 +120,14 @@ def _build_uv_command(uv_bin: Path, request: PackageInstallRequest, use_index: b
|
||||
|
||||
|
||||
def _build_uv_sync_command(uv_bin: Path, request: PackageInstallRequest, use_index: bool) -> list[str]:
|
||||
if len(request.dependency_files) != 1:
|
||||
raise ValueError("主项目锁定依赖恢复只接受一个 pyproject.toml")
|
||||
project_file = request.dependency_files[0]
|
||||
command = [
|
||||
str(uv_bin),
|
||||
"sync",
|
||||
"--project",
|
||||
str(request.dependency_file.parent),
|
||||
str(project_file.parent),
|
||||
"--locked",
|
||||
"--no-dev",
|
||||
"--no-install-project",
|
||||
|
||||
@@ -248,9 +248,9 @@ class PluginDependencyInstaller:
|
||||
merged.append(Requirement(target))
|
||||
return merged
|
||||
|
||||
def _plugin_dependencies(self) -> list[Requirement]:
|
||||
"""扫描已安装插件的生效依赖清单并合并版本约束。"""
|
||||
dependencies: list[Requirement] = []
|
||||
def _plugin_manifests(self) -> list[Any]:
|
||||
"""返回已安装插件当前生效的依赖清单。"""
|
||||
manifests = []
|
||||
installed_plugins = {
|
||||
plugin_id.lower()
|
||||
for plugin_id in self._installed_plugins_provider() or []
|
||||
@@ -259,7 +259,7 @@ class PluginDependencyInstaller:
|
||||
plugin_dirs = list(self._plugin_dir.iterdir())
|
||||
except (FileNotFoundError, OSError):
|
||||
return []
|
||||
for plugin_dir in plugin_dirs:
|
||||
for plugin_dir in sorted(plugin_dirs, key=lambda item: item.name):
|
||||
if not plugin_dir.is_dir():
|
||||
continue
|
||||
if plugin_dir.name not in installed_plugins:
|
||||
@@ -268,6 +268,13 @@ class PluginDependencyInstaller:
|
||||
manifest = load_dependency_manifest(plugin_dir)
|
||||
if manifest is None:
|
||||
continue
|
||||
manifests.append(manifest)
|
||||
return manifests
|
||||
|
||||
def _plugin_dependencies(self) -> list[Requirement]:
|
||||
"""扫描已安装插件的生效依赖清单并合并版本约束。"""
|
||||
dependencies: list[Requirement] = []
|
||||
for manifest in self._plugin_manifests():
|
||||
for requirement in manifest.dependencies:
|
||||
if requirement.marker and not requirement.marker.evaluate():
|
||||
continue
|
||||
@@ -304,29 +311,20 @@ class PluginDependencyInstaller:
|
||||
return list(dict.fromkeys(result))
|
||||
|
||||
def install(self, dependencies: list[str]) -> tuple[bool, str]:
|
||||
"""把依赖写入临时 requirements 并调用统一包安装策略。"""
|
||||
"""把已安装插件的原始清单交给一次统一包安装。"""
|
||||
if not dependencies:
|
||||
return False, "没有传入需要安装的依赖项"
|
||||
requirements_file = (
|
||||
Path(settings.TEMP_PATH)
|
||||
/ "plugin_dependencies"
|
||||
/ "requirements.txt"
|
||||
)
|
||||
try:
|
||||
requirements_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
requirements_file.write_text(
|
||||
"".join(f"{dependency}\n" for dependency in dependencies),
|
||||
encoding="utf-8",
|
||||
)
|
||||
manifest_paths = [manifest.path for manifest in self._plugin_manifests()]
|
||||
if not manifest_paths:
|
||||
return False, "没有找到已安装插件的依赖清单"
|
||||
return self._helper.install_packages_with_fallback(
|
||||
requirements_file,
|
||||
manifest_paths,
|
||||
self._wheels_dirs(),
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(f"安装依赖项时发生错误:{err}")
|
||||
return False, f"安装依赖项时发生错误:{err}"
|
||||
finally:
|
||||
requirements_file.unlink(missing_ok=True)
|
||||
|
||||
async def async_find_missing(self) -> list[str]:
|
||||
"""在线程池中扫描缺失依赖,避免阻塞事件循环。"""
|
||||
|
||||
Reference in New Issue
Block a user