From 810cb0a203008217df0b6050c59d94b3e9b3691c Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 16 Apr 2026 23:13:45 +0800 Subject: [PATCH] relax local install python requirement to 3.11 --- docs/cli.md | 11 +++-- docs/development-setup.md | 4 +- moviepilot | 94 ++++++++++++++++++++------------------ scripts/bootstrap-local.sh | 87 +++++++++++++++++++++++++---------- scripts/local_setup.py | 52 ++++++++++++++++++--- 5 files changed, 167 insertions(+), 81 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 87523844..67527fb7 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -11,7 +11,7 @@ curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v2/scripts/bootst 脚本会自动: - 检测操作系统 -- 自动检查并尽量安装 `git`、`curl`、`Python 3.12+` +- 自动检查并尽量安装 `git`、`curl`、`Python 3.11+` - 克隆 `MoviePilot` - 安装后端依赖 - 下载 `MoviePilot-Frontend` 最新 release 的 `dist.zip` @@ -24,7 +24,8 @@ curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v2/scripts/bootst 说明: -- 如果系统里没有可用的 `Python 3.12+`,脚本会优先尝试自动补齐运行环境,再继续安装 +- 如果系统里已经有可用的 `Python 3.11+`,脚本会优先直接复用本地解释器 +- 如果系统里没有可用的 `Python 3.11+`,脚本会再尝试自动补齐运行环境 - Linux 下安装系统依赖时通常需要 `sudo` - 复用已有仓库时,脚本现在只会因为已跟踪源码改动而阻止自动更新,不会再被 `.DS_Store` 之类未跟踪文件卡住 @@ -141,12 +142,16 @@ moviepilot commands ```shell moviepilot install deps -moviepilot install deps --python python3.12 +moviepilot install deps --python python3.11 moviepilot install deps --venv /path/to/venv moviepilot install deps --recreate moviepilot install deps --config-dir /path/to/moviepilot-config ``` +说明: + +- 默认会自动选择本地已安装的 `Python 3.11+` 解释器 + 安装前端 release: ```shell diff --git a/docs/development-setup.md b/docs/development-setup.md index 39ae73c3..dd35552b 100644 --- a/docs/development-setup.md +++ b/docs/development-setup.md @@ -6,7 +6,7 @@ 在开始之前,请确保您的系统已安装以下软件: -- **Python 3.12 或更高版本** (暂时兼容 3.11 ,推荐使用 3.12+) +- **Python 3.11 或更高版本** - **pip** (Python 包管理器) - **Git** (用于版本控制) @@ -119,4 +119,4 @@ safety check -r requirements.txt --policy-file=safety.policy.yml > safety_report ### 5. 参考资源 - [pip-tools 官方文档](https://github.com/jazzband/pip-tools) -- [safety 官方文档](https://pyup.io/safety/) \ No newline at end of file +- [safety 官方文档](https://pyup.io/safety/) diff --git a/moviepilot b/moviepilot index b61bfd10..4a0c0b09 100755 --- a/moviepilot +++ b/moviepilot @@ -91,7 +91,7 @@ Usage: Options: deps: - --python PYTHON 用于创建虚拟环境的 Python 解释器 + --python PYTHON 用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.11+ 版本 --venv PATH 虚拟环境目录,默认 ./venv --recreate 删除并重建虚拟环境 --config-dir PATH 指定配置目录 @@ -128,7 +128,7 @@ show_setup_help() { Usage: moviepilot setup [OPTIONS] Options: - --python PYTHON 用于创建虚拟环境的 Python 解释器 + --python PYTHON 用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.11+ 版本 --venv PATH 虚拟环境目录,默认 ./venv --recreate 删除并重建虚拟环境 --frontend-version TAG 前端版本,默认 latest @@ -152,7 +152,7 @@ Options: --ref REF 后端 Git 版本,默认 latest --frontend-version TAG 前端版本,默认 latest --node-version VER 本地 Node 运行时版本,默认 20.12.1 - --python PYTHON 用于安装后端依赖的 Python 解释器 + --python PYTHON 用于安装后端依赖的 Python 解释器,默认自动选择本地 3.11+ 版本 --venv PATH 虚拟环境目录,默认 ./venv --recreate 删除并重建虚拟环境 --skip-resources 更新 all 时跳过资源同步 @@ -179,39 +179,60 @@ python_version_ok() { local python_bin="$1" "$python_bin" - <<'PY' >/dev/null 2>&1 import sys -raise SystemExit(0 if sys.version_info >= (3, 12) else 1) +raise SystemExit(0 if sys.version_info >= (3, 11) else 1) PY } +try_python_candidate() { + local candidate="$1" + local python_path="" + + python_path="$(command -v "$candidate" 2>/dev/null || true)" + if [ -n "$python_path" ] && python_version_ok "$python_path"; then + printf '%s\n' "$python_path" + return 0 + fi + return 1 +} + find_system_python() { - if command -v python3 >/dev/null 2>&1; then - local python3_bin - python3_bin="$(command -v python3)" - if python_version_ok "$python3_bin"; then - printf '%s\n' "$python3_bin" - return 0 - fi - fi - if command -v python >/dev/null 2>&1; then - local python_bin - python_bin="$(command -v python)" - if python_version_ok "$python_bin"; then - printf '%s\n' "$python_bin" - return 0 - fi - fi + local minor local uv_bin + local uv_python + + for minor in 20 19 18 17 16 15 14 13 12 11; do + if try_python_candidate "python3.$minor"; then + return 0 + fi + done + if try_python_candidate python3; then + return 0 + fi + if try_python_candidate python; then + return 0 + fi for uv_bin in "$(command -v uv 2>/dev/null || true)" "$HOME/.local/bin/uv"; do if [ -n "$uv_bin" ] && [ -x "$uv_bin" ]; then - if "$uv_bin" python find 3.12 >/dev/null 2>&1; then - "$uv_bin" python find 3.12 - return 0 - fi + for minor in 20 19 18 17 16 15 14 13 12 11; do + uv_python="$("$uv_bin" python find "3.$minor" 2>/dev/null || true)" + if [ -n "$uv_python" ] && python_version_ok "$uv_python"; then + printf '%s\n' "$uv_python" + return 0 + fi + done fi done return 1 } +require_bootstrap_python() { + if [ -n "$BOOTSTRAP_PYTHON" ]; then + return 0 + fi + echo "未找到可用的 Python 3.11+ 解释器,请先安装 Python 3.11 或更高版本" >&2 + exit 1 +} + default_config_dir() { case "$(uname -s)" in Darwin) @@ -342,10 +363,7 @@ case "${1:-}" in ;; install) shift - if [ -z "$BOOTSTRAP_PYTHON" ]; then - echo "未找到可用的 Python 解释器,请先安装 Python 3" >&2 - exit 1 - fi + require_bootstrap_python case "${1:-}" in deps) shift @@ -367,34 +385,22 @@ case "${1:-}" in ;; init) shift - if [ -z "$BOOTSTRAP_PYTHON" ]; then - echo "未找到可用的 Python 解释器,请先安装 Python 3" >&2 - exit 1 - fi + require_bootstrap_python exec "$BOOTSTRAP_PYTHON" "$SETUP_SCRIPT" init "$@" ;; setup) shift - if [ -z "$BOOTSTRAP_PYTHON" ]; then - echo "未找到可用的 Python 解释器,请先安装 Python 3" >&2 - exit 1 - fi + require_bootstrap_python exec "$BOOTSTRAP_PYTHON" "$SETUP_SCRIPT" setup "$@" ;; update) shift - if [ -z "$BOOTSTRAP_PYTHON" ]; then - echo "未找到可用的 Python 解释器,请先安装 Python 3" >&2 - exit 1 - fi + require_bootstrap_python exec "$BOOTSTRAP_PYTHON" "$SETUP_SCRIPT" update "$@" ;; agent) shift - if [ -z "$BOOTSTRAP_PYTHON" ]; then - echo "未找到可用的 Python 解释器,请先安装 Python 3" >&2 - exit 1 - fi + require_bootstrap_python exec "$BOOTSTRAP_PYTHON" "$SETUP_SCRIPT" agent "$@" ;; esac diff --git a/scripts/bootstrap-local.sh b/scripts/bootstrap-local.sh index 91de0659..9ec9e876 100755 --- a/scripts/bootstrap-local.sh +++ b/scripts/bootstrap-local.sh @@ -168,43 +168,74 @@ detect_package_manager() { esac } -find_python() { - if command -v python3 >/dev/null 2>&1; then - command -v python3 - return 0 - fi - if command -v python >/dev/null 2>&1; then - command -v python +python_version_ok() { + local python_bin="$1" + "$python_bin" - <<'PY' >/dev/null 2>&1 +import sys +raise SystemExit(0 if sys.version_info >= (3, 11) else 1) +PY +} + +try_python_candidate() { + local candidate="$1" + local python_path="" + + python_path="$(command -v "$candidate" 2>/dev/null || true)" + if [[ -n "$python_path" ]] && python_version_ok "$python_path"; then + printf '%s\n' "$python_path" return 0 fi return 1 } -python_version_ok() { - local python_bin="$1" - "$python_bin" - <<'PY' >/dev/null 2>&1 -import sys -raise SystemExit(0 if sys.version_info >= (3, 12) else 1) -PY +find_python() { + local minor="" + for minor in 20 19 18 17 16 15 14 13 12 11; do + if try_python_candidate "python3.$minor"; then + return 0 + fi + done + if try_python_candidate python3; then + return 0 + fi + if try_python_candidate python; then + return 0 + fi + return 1 +} + +find_uv_python() { + local uv_bin="$1" + local minor="" + local python_path="" + + for minor in 20 19 18 17 16 15 14 13 12 11; do + python_path="$("$uv_bin" python find "3.$minor" 2>/dev/null || true)" + if [[ -n "$python_path" ]] && python_version_ok "$python_path"; then + printf '%s\n' "$python_path" + return 0 + fi + done + return 1 } python_install_hint() { case "$OS_NAME" in macOS) - echo "脚本已尝试自动安装 Git、curl 和 Python 3.12+。" >&2 - echo "如果自动安装失败,请先安装 Homebrew,或手动执行:brew install git curl python@3.12" >&2 + echo "脚本已尝试自动安装 Git、curl 和 Python 3.11+。" >&2 + echo "如果自动安装失败,请先安装 Homebrew,或手动执行:brew install git curl python@3.11" >&2 ;; Linux*) - echo "脚本已尝试自动安装 Git、curl 和 Python 3.12+。" >&2 - echo "如果自动安装失败,请先安装 Git、curl 和 Python 3.12,并确保包含 venv 模块。" >&2 - echo "例如 Debian/Ubuntu: sudo apt install git curl python3.12 python3.12-venv" >&2 - echo "例如 Fedora/RHEL: sudo dnf install git curl python3.12" >&2 + echo "脚本已尝试自动安装 Git、curl 和 Python 3.11+。" >&2 + echo "如果自动安装失败,请先安装 Git、curl 和 Python 3.11+,并确保包含 venv 模块。" >&2 + echo "例如 Debian/Ubuntu: sudo apt install git curl python3.11 python3.11-venv" >&2 + echo "例如 Fedora/RHEL: sudo dnf install git curl python3.11" >&2 ;; Windows) echo "推荐在 WSL、Linux 或 macOS 终端中运行此脚本。" >&2 ;; *) - echo "请先安装 Git、curl 和 Python 3.12。" >&2 + echo "请先安装 Git、curl 和 Python 3.11 或更高版本。" >&2 ;; esac } @@ -352,7 +383,7 @@ ensure_uv() { return 0 fi - echo "==> 自动安装 uv,用于拉取 Python 3.12+" + echo "==> 自动安装 uv,用于补齐 Python 3.11+ 运行时" env UV_INSTALL_DIR="$HOME/.local/bin" sh -c "$(curl -LsSf https://astral.sh/uv/install.sh)" export PATH="$HOME/.local/bin:$PATH" hash -r @@ -369,12 +400,18 @@ ensure_python() { return 0 fi - echo "==> 未找到可用的 Python 3.12+,开始自动安装独立 Python 运行时" ensure_uv - uv python install 3.12 - PYTHON_BIN="$(uv python find 3.12 || true)" + + PYTHON_BIN="$(find_uv_python "$(command -v uv)" || true)" + if [[ -n "$PYTHON_BIN" ]] && python_version_ok "$PYTHON_BIN"; then + return 0 + fi + + echo "==> 未找到可用的 Python 3.11+,开始自动安装独立 Python 运行时" + uv python install 3.11 + PYTHON_BIN="$(find_uv_python "$(command -v uv)" || true)" if [[ -z "$PYTHON_BIN" ]] || ! python_version_ok "$PYTHON_BIN"; then - echo "自动安装 Python 3.12+ 失败。" >&2 + echo "自动安装 Python 3.11+ 失败。" >&2 return 1 fi } diff --git a/scripts/local_setup.py b/scripts/local_setup.py index 9e0e0f93..6cfb419b 100644 --- a/scripts/local_setup.py +++ b/scripts/local_setup.py @@ -28,6 +28,8 @@ PUBLIC_DIR = ROOT / "public" RUNTIME_DIR = ROOT / ".runtime" NODE_DIR = RUNTIME_DIR / "node" INSTALL_ENV_FILE = ROOT / ".moviepilot.env" +MIN_PYTHON_VERSION = (3, 11) +SUPPORTED_PYTHON_TEXT = f"Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]} 或更高版本" CONFIG_DIR = LEGACY_CONFIG_DIR LOG_DIR = CONFIG_DIR / "logs" @@ -204,6 +206,42 @@ def command_exists(name: str) -> bool: return shutil.which(name) is not None +def get_python_version(python_bin: str) -> tuple[int, int, int]: + version_json = capture([python_bin, "-c", "import json, sys; print(json.dumps(list(sys.version_info[:3])))"]) + version_info = json.loads(version_json) + if not isinstance(version_info, list) or len(version_info) < 3: + raise RuntimeError(f"无法识别 Python 版本信息:{python_bin}") + return int(version_info[0]), int(version_info[1]), int(version_info[2]) + + +def discover_supported_python() -> Optional[str]: + candidates = [f"python3.{minor}" for minor in range(20, MIN_PYTHON_VERSION[1] - 1, -1)] + if sys.executable: + candidates.append(sys.executable) + candidates.extend(["python3", "python"]) + + seen: set[str] = set() + for candidate in candidates: + if not candidate or candidate in seen: + continue + seen.add(candidate) + + python_path = candidate if os.sep in candidate else (shutil.which(candidate) or "") + if not python_path: + continue + + try: + version = get_python_version(python_path) + except (OSError, subprocess.CalledProcessError, json.JSONDecodeError): + continue + if version >= MIN_PYTHON_VERSION: + return python_path + return None + + +DEFAULT_BOOTSTRAP_PYTHON = discover_supported_python() or sys.executable + + def get_venv_python(venv_dir: Path) -> Path: if os.name == "nt": return venv_dir / "Scripts" / "python.exe" @@ -211,11 +249,10 @@ def get_venv_python(venv_dir: Path) -> Path: def ensure_supported_python(python_bin: str) -> None: - version_json = capture([python_bin, "-c", "import json, sys; print(json.dumps(list(sys.version_info[:3])))"]) - version = tuple(json.loads(version_json)) - if version < (3, 12, 0): + version = get_python_version(python_bin) + if version < MIN_PYTHON_VERSION: raise RuntimeError( - f"MoviePilot 本地安装需要 Python 3.12 或更高版本,当前解释器为 {python_bin} " + f"MoviePilot 本地安装需要 {SUPPORTED_PYTHON_TEXT},当前解释器为 {python_bin} " f"({version[0]}.{version[1]}.{version[2]})" ) @@ -1359,6 +1396,7 @@ def install_deps(*, python_bin: str, venv_dir: Path, recreate: bool) -> Path: ensure_supported_python(python_bin) venv_dir = venv_dir.expanduser().resolve() venv_python = get_venv_python(venv_dir) + print_step(f"使用 Python 解释器:{python_bin}") if recreate and venv_dir.exists(): print_step(f"删除已有虚拟环境:{venv_dir}") @@ -1512,7 +1550,7 @@ def build_parser() -> argparse.ArgumentParser: subparsers = parser.add_subparsers(dest="command", required=True) install_parser = subparsers.add_parser("install-deps", help="创建虚拟环境并安装后端依赖") - install_parser.add_argument("--python", default=sys.executable, help="用于创建虚拟环境的 Python 解释器") + install_parser.add_argument("--python", default=DEFAULT_BOOTSTRAP_PYTHON, help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.11+ 版本") install_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录") install_parser.add_argument("--recreate", action="store_true", help="删除并重建虚拟环境") install_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录") @@ -1536,7 +1574,7 @@ def build_parser() -> argparse.ArgumentParser: init_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录") setup_parser = subparsers.add_parser("setup", help="执行 install-deps、install-frontend、install-resources 和 init") - setup_parser.add_argument("--python", default=sys.executable, help="用于创建虚拟环境的 Python 解释器") + setup_parser.add_argument("--python", default=DEFAULT_BOOTSTRAP_PYTHON, help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.11+ 版本") setup_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录") setup_parser.add_argument("--recreate", action="store_true", help="删除并重建虚拟环境") setup_parser.add_argument("--frontend-version", default="latest", help="前端版本,默认 latest") @@ -1560,7 +1598,7 @@ def build_parser() -> argparse.ArgumentParser: update_parser.add_argument("--ref", default="latest", help="后端 Git 版本,默认 latest") update_parser.add_argument("--frontend-version", default="latest", help="前端版本,默认 latest") update_parser.add_argument("--node-version", default=DEFAULT_NODE_VERSION, help="本地 Node 运行时版本") - update_parser.add_argument("--python", default=sys.executable, help="用于安装后端依赖的 Python 解释器") + update_parser.add_argument("--python", default=DEFAULT_BOOTSTRAP_PYTHON, help="用于安装后端依赖的 Python 解释器,默认自动选择本地 3.11+ 版本") update_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录") update_parser.add_argument("--recreate", action="store_true", help="删除并重建虚拟环境") update_parser.add_argument("--skip-resources", action="store_true", help="更新 all 时跳过资源同步")