fix(cli): prefer project runtime Python

This commit is contained in:
jxxghp
2026-09-30 22:35:15 +08:00
parent 1f79f4b558
commit 00f396601f
5 changed files with 83 additions and 8 deletions

View File

@@ -402,6 +402,7 @@ moviepilot version
说明:
- `start` 会先启动后端,再启动前端;普通 `start` / `restart` 不检查、不下载、不安装更新,DEV 模式也不例外
- 本地 CLI 会优先使用项目根目录 `venv/bin/moviepilot-python`;该入口不存在时回退到同一目录的 `venv/bin/python`,手工启动、更新和开机自启保持一致
- `start --safe` 会以安全模式启动后端,本次启动跳过插件、调度器、监控、命令和工作流等后台扩展能力,不修改用户配置
- `MOVIEPILOT_AUTO_UPDATE` 为布尔开关,默认 `false`;只有 `true` 启用后台 Release 检查和版本提醒,保存后定时服务热更新。`AUTO_UPDATE_RESOURCE` 独立控制站点资源检查和提醒;任一开关开启即启用检测服务,且只检查对应目标,两者均关闭才移除服务。`MOVIEPILOT_UPDATE_DEV` 为独立布尔开关,默认 `false`;设为 `true` 时手动 `moviepilot update` 默认使用 DEV 更新模式。旧 `dev/release` 值统一转换为 `MOVIEPILOT_AUTO_UPDATE=true`;旧 `dev` 在未显式配置新开关时迁移为 `MOVIEPILOT_UPDATE_DEV=true`
- Release 更新由后台每 6 小时检查 GitHub Release;管理员确认后先静默下载安装包并显示进度,下载完成后再次确认重启,通过专门的内部更新入口安装已下载且通过 SHA-256 校验的包后再启动服务

View File

@@ -456,11 +456,11 @@ legacy_config_exists() {
}
run_runtime_cli() {
if [ ! -x "$VENV_PYTHON" ]; then
if [ ! -x "$MOVIEPILOT_PYTHON" ]; then
echo "未找到项目虚拟环境,请先执行 moviepilot install deps 或 moviepilot setup" >&2
exit 1
fi
exec "$VENV_PYTHON" -m app.cli "$@"
exec "$MOVIEPILOT_PYTHON" -m app.cli "$@"
}
show_command_help() {
@@ -541,6 +541,13 @@ done
ROOT="$(cd -P "$(dirname "$SOURCE")" && pwd)"
VENV_PYTHON="$ROOT/venv/bin/python"
# 优先使用项目环境提供的专用 Python 入口,保留其运行身份和系统权限。
PROJECT_RUNTIME_PYTHON="$ROOT/venv/bin/moviepilot-python"
if [ -x "$PROJECT_RUNTIME_PYTHON" ]; then
MOVIEPILOT_PYTHON="$PROJECT_RUNTIME_PYTHON"
else
MOVIEPILOT_PYTHON="$VENV_PYTHON"
fi
SETUP_SCRIPT="$ROOT/scripts/local_setup.py"
if [ -z "${CONFIG_DIR:-}" ] && [ -f "$ROOT/.moviepilot.env" ]; then
@@ -559,8 +566,8 @@ export CONFIG_DIR
BOOTSTRAP_PYTHON=""
EXTERNAL_BOOTSTRAP_PYTHON="$(find_external_python || true)"
if [ -x "$VENV_PYTHON" ]; then
BOOTSTRAP_PYTHON="$VENV_PYTHON"
if [ -x "$MOVIEPILOT_PYTHON" ]; then
BOOTSTRAP_PYTHON="$MOVIEPILOT_PYTHON"
else
BOOTSTRAP_PYTHON="$EXTERNAL_BOOTSTRAP_PYTHON"
fi
@@ -643,9 +650,9 @@ case "${1:-}" in
;;
esac
if [ ! -x "$VENV_PYTHON" ]; then
if [ ! -x "$MOVIEPILOT_PYTHON" ]; then
echo "未找到项目虚拟环境,请先执行 moviepilot install deps 或 moviepilot setup" >&2
exit 1
fi
exec "$VENV_PYTHON" -m app.cli "$@"
exec "$MOVIEPILOT_PYTHON" -m app.cli "$@"

View File

@@ -2997,13 +2997,19 @@ def _startup_platform_name() -> str:
def _runtime_python_candidates(
runtime_python: Optional[Path], venv_dir: Optional[Path]
) -> list[Path]:
"""按优先级列出启动解释器,并保留 venv 的 Python 启动器软链接。"""
"""按优先级列出项目专用入口、venv 解释器和外部启动解释器。"""
candidates: list[Path] = []
seen: set[str] = set()
resolved_venv_dir = (venv_dir or (ROOT / "venv")).expanduser().resolve()
venv_bin_dir = get_venv_bin_dir(resolved_venv_dir)
project_runtime_python = venv_bin_dir / (
"moviepilot-python.exe" if os.name == "nt" else "moviepilot-python"
)
raw_candidates = [
project_runtime_python,
get_venv_python(resolved_venv_dir),
runtime_python,
get_venv_python((venv_dir or (ROOT / "venv")).expanduser().resolve()),
Path(sys.executable) if sys.executable else None,
]
for candidate in raw_candidates:

View File

@@ -67,6 +67,30 @@ def test_startup_python_resolution_preserves_virtualenv_symlink(
assert runtime_python.is_symlink()
def test_startup_python_resolution_prefers_project_runtime_launcher(
monkeypatch, tmp_path: Path
):
"""Autostart must prefer the project runtime launcher over an external interpreter."""
module = load_local_setup_module()
venv_dir = tmp_path / "venv"
runtime_launcher = module.get_venv_bin_dir(venv_dir) / "moviepilot-python"
runtime_launcher.parent.mkdir(parents=True)
runtime_launcher.symlink_to(Path(sys.executable))
expected_path = runtime_launcher.absolute()
external_python = tmp_path / "external-python"
monkeypatch.setattr(
module,
"_can_run_moviepilot_cli",
lambda candidate: candidate == expected_path,
)
runtime_python = module._resolve_runtime_python_for_startup(
external_python, venv_dir
)
assert runtime_python == expected_path
def test_directory_config_keeps_download_path_as_downloader_path(monkeypatch):
"""The wizard must preserve downloader paths without resolving them locally."""
module = load_local_setup_module()

View File

@@ -165,3 +165,40 @@ def test_recreate_excludes_custom_venv_from_external_bootstrap(
)
assert record.read_text(encoding="utf-8").startswith(f"{external_python} ")
def test_runtime_commands_prefer_project_runtime_launcher(tmp_path):
"""运行命令优先使用项目 venv 下保留专用身份的 Python 入口。"""
root = tmp_path / "moviepilot"
root.mkdir()
launcher = root / "moviepilot"
launcher.write_text(LAUNCHER.read_text(encoding="utf-8"), encoding="utf-8")
launcher.chmod(launcher.stat().st_mode | stat.S_IXUSR)
runtime_bin = root / "venv" / "bin"
runtime_bin.mkdir(parents=True)
record = tmp_path / "record"
venv_python = runtime_bin / "python"
project_runtime_python = runtime_bin / "moviepilot-python"
for python_path in (venv_python, project_runtime_python):
python_path.write_text(
"#!/bin/sh\n"
"printf '%s\\n' \"$0 $*\" > \"$MOVIEPILOT_TEST_RECORD\"\n",
encoding="utf-8",
)
python_path.chmod(python_path.stat().st_mode | stat.S_IXUSR)
env = os.environ.copy()
env["PATH"] = "/usr/bin:/bin"
env["MOVIEPILOT_TEST_RECORD"] = str(record)
subprocess.run(
[str(launcher), "doctor"],
cwd=root,
env=env,
check=True,
capture_output=True,
text=True,
)
invocation = record.read_text(encoding="utf-8")
assert invocation.startswith(f"{project_runtime_python} ")