fix(plugins): preserve dependency manifest sources (#6370)

This commit is contained in:
InfinityPacer
2026-08-20 20:51:35 +08:00
committed by GitHub
parent 638ea6f01e
commit 2d80f9f04f
7 changed files with 189 additions and 61 deletions
+29 -13
View File
@@ -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",
+8 -4
View File
@@ -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",
+16 -18
View File
@@ -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]:
"""在线程池中扫描缺失依赖,避免阻塞事件循环。"""