6da59cfb07
* fix: install plugin requirements before first load * fix: handle pip option arguments correctly * fix: harden pip install input parsing * refactor: simplify pip install input parsing * fix: align plugin dependency install handling * fix: respect configured pip index overrides * test: parameterize plugin dependency install flows * refactor: simplify multiline pip input parsing * fix: install plugin dependencies before loading * fix: protect core dependencies from downgrades and simplify package input splitting * fix: enhance dependency conflict reporting and improve user-facing warnings * refactor: preserve pip log indentation and fix CodeQL URL sanitization alert * fix: explicit re-export for DependencyConflictError to satisfy ruff F401 * test: enhance index override verification in pip installer tests * fix: correctly map pip ERROR and WARNING outputs to proper log levels * refactor: show specific version conflicts in DependencyConflictError and revert log level mapping * refactor: simplify install() by decoupling pip logging, failure classification and constraint file management * refactor: further simplify pip installer and requirement parsing logic * refactor: simplify dependency installation logic and improve circular requirement reporting * style: organize imports in astrbot/core/__init__.py * refactor: optimize requirement parsing efficiency and flatten pip installer API * style: fix import sorting in astrbot/core/__init__.py * refactor: consolidate requirement parsing, optimize core protection, and improve exception propagation * fix: preserve valid pip requirement parsing * fix: skip empty pip installs and preserve blank output * chore: normalize gitignore entry style * fix: tighten pip trust and requirement parsing * refactor: centralize pip install parsing and failure handling * fix: redact pip argv credentials in logs * fix: surface plugin dependency install errors * fix: cache core constraints and clarify requirement installs * fix: harden pip requirement parsing for plugin installs * fix: simplify pip installer parsing internals * fix: tighten pip installer parsing and redaction * refactor: simplify plugin dependency install flow * fix: preserve core constraint conflict errors * fix: harden pip installer fallback resolution * refactor: split pip requirement and constraint helpers * refactor: simplify pip installer helper flow * refactor: streamline requirement precheck helpers * refactor: clarify core constraint resolution * fix: surface pip install failures explicitly * refactor: separate pip conflict context parsing * fix: harden core constraint resolution * test: cover pip installer failure call sites * refactor: remove dead requirements fallback helper * refactor: narrow core constraint error handling * refactor: unify requirement iteration * refactor: share requirement name parsing * test: align pip helper coverage * fix: bind pip output limit at runtime * refactor: reuse core requirement parser for tokens
122 lines
3.5 KiB
Python
122 lines
3.5 KiB
Python
import contextlib
|
|
import functools
|
|
import importlib.metadata as importlib_metadata
|
|
import logging
|
|
import os
|
|
from collections.abc import Iterator
|
|
|
|
from packaging.requirements import Requirement
|
|
|
|
from astrbot.core.utils.requirements_utils import (
|
|
canonicalize_distribution_name,
|
|
collect_installed_distribution_versions,
|
|
get_requirement_check_paths,
|
|
)
|
|
|
|
logger = logging.getLogger("astrbot")
|
|
|
|
|
|
def _resolve_core_dist_name(core_dist_name: str | None) -> str | None:
|
|
if core_dist_name:
|
|
try:
|
|
importlib_metadata.distribution(core_dist_name)
|
|
return core_dist_name
|
|
except importlib_metadata.PackageNotFoundError:
|
|
return None
|
|
|
|
try:
|
|
importlib_metadata.distribution("AstrBot")
|
|
return "AstrBot"
|
|
except importlib_metadata.PackageNotFoundError:
|
|
pass
|
|
|
|
if not __package__:
|
|
return None
|
|
|
|
top_pkg = __package__.split(".")[0]
|
|
for dist in importlib_metadata.distributions():
|
|
try:
|
|
top_level = dist.read_text("top_level.txt") or ""
|
|
except Exception:
|
|
continue
|
|
if top_pkg in top_level.splitlines():
|
|
if "Name" in dist.metadata:
|
|
return dist.metadata["Name"]
|
|
|
|
return None
|
|
|
|
|
|
@functools.cache
|
|
def _get_core_constraints(core_dist_name: str | None) -> tuple[str, ...]:
|
|
try:
|
|
resolved_core_dist_name = _resolve_core_dist_name(core_dist_name)
|
|
except Exception as exc:
|
|
logger.warning("解析核心分发名称失败: %s", exc)
|
|
return ()
|
|
|
|
if not resolved_core_dist_name:
|
|
return ()
|
|
|
|
try:
|
|
dist = importlib_metadata.distribution(resolved_core_dist_name)
|
|
except importlib_metadata.PackageNotFoundError:
|
|
return ()
|
|
except Exception as exc:
|
|
logger.warning("读取核心分发元数据失败 (%s): %s", resolved_core_dist_name, exc)
|
|
return ()
|
|
|
|
if not dist or not dist.requires:
|
|
return ()
|
|
|
|
installed = collect_installed_distribution_versions(get_requirement_check_paths())
|
|
if not installed:
|
|
return ()
|
|
|
|
constraints: list[str] = []
|
|
for req_str in dist.requires:
|
|
try:
|
|
req = Requirement(req_str)
|
|
if req.marker and not req.marker.evaluate():
|
|
continue
|
|
name = canonicalize_distribution_name(req.name)
|
|
if name in installed:
|
|
constraints.append(f"{name}=={installed[name]}")
|
|
except Exception:
|
|
continue
|
|
|
|
return tuple(constraints)
|
|
|
|
|
|
class CoreConstraintsProvider:
|
|
def __init__(self, core_dist_name: str | None) -> None:
|
|
self._core_dist_name = core_dist_name
|
|
|
|
@contextlib.contextmanager
|
|
def constraints_file(self) -> Iterator[str | None]:
|
|
constraints = _get_core_constraints(self._core_dist_name)
|
|
if not constraints:
|
|
yield None
|
|
return
|
|
|
|
path: str | None = None
|
|
try:
|
|
import tempfile
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix="_constraints.txt", delete=False, encoding="utf-8"
|
|
) as f:
|
|
f.write("\n".join(constraints))
|
|
path = f.name
|
|
logger.info("已启用核心依赖版本保护 (%d 个约束)", len(constraints))
|
|
except Exception as exc:
|
|
logger.warning("创建临时约束文件失败: %s", exc)
|
|
yield None
|
|
return
|
|
|
|
try:
|
|
yield path
|
|
finally:
|
|
if path and os.path.exists(path):
|
|
with contextlib.suppress(Exception):
|
|
os.remove(path)
|