diff --git a/docs/cli.md b/docs/cli.md index 5a4046d31..a1e06935c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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 校验的包后再启动服务 diff --git a/moviepilot b/moviepilot index c06e9b087..900b36a03 100755 --- a/moviepilot +++ b/moviepilot @@ -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 "$@" diff --git a/scripts/local_setup.py b/scripts/local_setup.py index cee2e8975..d7724c755 100644 --- a/scripts/local_setup.py +++ b/scripts/local_setup.py @@ -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: diff --git a/tests/test_local_setup_installer_wizard.py b/tests/test_local_setup_installer_wizard.py index d59dc4ee8..096cb723f 100644 --- a/tests/test_local_setup_installer_wizard.py +++ b/tests/test_local_setup_installer_wizard.py @@ -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() diff --git a/tests/test_moviepilot_launcher.py b/tests/test_moviepilot_launcher.py index 16ae248e0..e89c2fbd1 100644 --- a/tests/test_moviepilot_launcher.py +++ b/tests/test_moviepilot_launcher.py @@ -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} ")