mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
feat: 使用 uv 锁定主程序依赖并强化插件恢复边界 (#6364)
This commit is contained in:
@@ -4,26 +4,22 @@ import os
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
|
||||
PackageBackend = Literal["uv", "pip"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageInstallRequest:
|
||||
"""
|
||||
Python 包安装请求,集中描述依赖文件、工具缓存、代理和本地 wheels 候选源。
|
||||
"""
|
||||
|
||||
requirements_file: Path
|
||||
dependency_file: Path
|
||||
python_bin: Path
|
||||
find_links_dirs: list[Path] = field(default_factory=list)
|
||||
constraints_file: Path | None = None
|
||||
config_dir: Path = Path("/config")
|
||||
package_cache_root: Path | None = None
|
||||
pip_index_url: str | None = None
|
||||
package_index_url: str | None = None
|
||||
proxy_url: str | None = None
|
||||
purpose: str = "plugin"
|
||||
|
||||
@@ -35,7 +31,6 @@ class PackageInstallStrategy:
|
||||
"""
|
||||
|
||||
strategy_name: str
|
||||
backend: PackageBackend
|
||||
command: list[str]
|
||||
env: dict[str, str]
|
||||
safe_log_command: list[str]
|
||||
@@ -61,7 +56,7 @@ def redact_command(command: list[str]) -> list[str]:
|
||||
|
||||
def build_package_install_env(request: PackageInstallRequest, include_moviepilot_proxy: bool = True) -> dict[str, str]:
|
||||
"""
|
||||
构造 pip/uv 安装子进程环境,默认把包下载缓存放到持久化配置目录。
|
||||
构造 uv 安装子进程环境,默认把包下载缓存放到持久化配置目录。
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
config_dir = Path(request.config_dir)
|
||||
@@ -71,7 +66,6 @@ def build_package_install_env(request: PackageInstallRequest, include_moviepilot
|
||||
else:
|
||||
package_cache_root = Path(env.get("PACKAGE_CACHE_ROOT") or config_dir / ".cache")
|
||||
env.setdefault("PACKAGE_CACHE_ROOT", str(package_cache_root))
|
||||
env.setdefault("PIP_CACHE_DIR", str(package_cache_root / "pip"))
|
||||
env.setdefault("UV_CACHE_DIR", str(package_cache_root / "uv"))
|
||||
proxy = (request.proxy_url or "").strip()
|
||||
if proxy and include_moviepilot_proxy:
|
||||
@@ -80,9 +74,9 @@ def build_package_install_env(request: PackageInstallRequest, include_moviepilot
|
||||
return env
|
||||
|
||||
|
||||
def _find_uv(python_bin: Path) -> Path | None:
|
||||
def find_uv(python_bin: Path) -> Path | None:
|
||||
"""
|
||||
优先使用解释器同目录 uv,保证虚拟环境内 wrapper 与真实安装环境一致。
|
||||
优先使用解释器同目录 uv,保证安装器与目标运行环境使用同一版本。
|
||||
"""
|
||||
uv_name = "uv.exe" if os.name == "nt" else "uv"
|
||||
sibling = python_bin.with_name(uv_name)
|
||||
@@ -98,12 +92,12 @@ 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.requirements_file)])
|
||||
args.extend(["-r", str(request.dependency_file)])
|
||||
return args
|
||||
|
||||
|
||||
def _network_variants(request: PackageInstallRequest) -> list[tuple[str, bool, bool]]:
|
||||
has_index = bool((request.pip_index_url or "").strip())
|
||||
has_index = bool((request.package_index_url or "").strip())
|
||||
has_proxy = bool((request.proxy_url or "").strip())
|
||||
variants: list[tuple[str, bool, bool]] = []
|
||||
if has_index and has_proxy:
|
||||
@@ -118,49 +112,67 @@ def _network_variants(request: PackageInstallRequest) -> list[tuple[str, bool, b
|
||||
|
||||
def _build_uv_command(uv_bin: Path, request: PackageInstallRequest, use_index: bool) -> list[str]:
|
||||
command = [str(uv_bin), "pip", "install", "--python", str(request.python_bin)]
|
||||
if use_index and request.pip_index_url:
|
||||
command.extend(["--default-index", request.pip_index_url])
|
||||
if use_index and request.package_index_url:
|
||||
command.extend(["--default-index", request.package_index_url])
|
||||
command.extend(_base_install_args(request))
|
||||
return command
|
||||
|
||||
|
||||
def _build_pip_command(request: PackageInstallRequest, use_index: bool) -> list[str]:
|
||||
command = [str(request.python_bin), "-m", "pip", "install"]
|
||||
if use_index and request.pip_index_url:
|
||||
command.extend(["-i", request.pip_index_url])
|
||||
command.extend(_base_install_args(request))
|
||||
def _build_uv_sync_command(uv_bin: Path, request: PackageInstallRequest, use_index: bool) -> list[str]:
|
||||
command = [
|
||||
str(uv_bin),
|
||||
"sync",
|
||||
"--project",
|
||||
str(request.dependency_file.parent),
|
||||
"--locked",
|
||||
"--no-dev",
|
||||
"--no-install-project",
|
||||
"--inexact",
|
||||
]
|
||||
if use_index and request.package_index_url:
|
||||
command.extend(["--default-index", request.package_index_url])
|
||||
return command
|
||||
|
||||
|
||||
def build_package_install_strategies(request: PackageInstallRequest) -> list[PackageInstallStrategy]:
|
||||
"""
|
||||
按 uv 优先、pip 兜底顺序构造网络降级策略。
|
||||
为固定 uv 安装器构造镜像、代理和直连降级策略。
|
||||
"""
|
||||
strategies: list[PackageInstallStrategy] = []
|
||||
variants = _network_variants(request)
|
||||
uv_bin = _find_uv(Path(request.python_bin))
|
||||
|
||||
if uv_bin:
|
||||
for variant_name, use_index, use_proxy in variants:
|
||||
command = _build_uv_command(uv_bin, request, use_index)
|
||||
env = build_package_install_env(request, include_moviepilot_proxy=use_proxy)
|
||||
strategies.append(
|
||||
PackageInstallStrategy(
|
||||
strategy_name=f"uv:{variant_name}",
|
||||
backend="uv",
|
||||
command=command,
|
||||
env=env,
|
||||
safe_log_command=redact_command(command),
|
||||
)
|
||||
)
|
||||
uv_bin = find_uv(Path(request.python_bin))
|
||||
if not uv_bin:
|
||||
return strategies
|
||||
|
||||
for variant_name, use_index, use_proxy in variants:
|
||||
command = _build_pip_command(request, use_index)
|
||||
command = _build_uv_command(uv_bin, request, use_index)
|
||||
env = build_package_install_env(request, include_moviepilot_proxy=use_proxy)
|
||||
strategies.append(
|
||||
PackageInstallStrategy(
|
||||
strategy_name=f"pip:{variant_name}",
|
||||
backend="pip",
|
||||
strategy_name=f"uv:{variant_name}",
|
||||
command=command,
|
||||
env=env,
|
||||
safe_log_command=redact_command(command),
|
||||
)
|
||||
)
|
||||
return strategies
|
||||
|
||||
|
||||
def build_project_sync_strategies(request: PackageInstallRequest) -> list[PackageInstallStrategy]:
|
||||
"""为主项目锁定依赖恢复构造 uv 网络降级策略。"""
|
||||
uv_bin = find_uv(Path(request.python_bin))
|
||||
if not uv_bin:
|
||||
return []
|
||||
|
||||
strategies = []
|
||||
project_environment = request.python_bin.parent.parent
|
||||
for variant_name, use_index, use_proxy in _network_variants(request):
|
||||
command = _build_uv_sync_command(uv_bin, request, use_index)
|
||||
env = build_package_install_env(request, include_moviepilot_proxy=use_proxy)
|
||||
env["UV_PROJECT_ENVIRONMENT"] = str(project_environment)
|
||||
strategies.append(
|
||||
PackageInstallStrategy(
|
||||
strategy_name=f"uv:{variant_name}",
|
||||
command=command,
|
||||
env=env,
|
||||
safe_log_command=redact_command(command),
|
||||
|
||||
Reference in New Issue
Block a user