From 89f6164eba03b0f93d37eda54980a38984506d82 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 16 Apr 2026 19:47:56 +0800 Subject: [PATCH] automate local bootstrap prerequisites --- docs/cli.md | 8 +- moviepilot | 33 +++++- scripts/bootstrap-local.sh | 236 +++++++++++++++++++++++++++++++++---- 3 files changed, 249 insertions(+), 28 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 73401951..87523844 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.12+` - 克隆 `MoviePilot` - 安装后端依赖 - 下载 `MoviePilot-Frontend` 最新 release 的 `dist.zip` @@ -22,6 +22,12 @@ curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v2/scripts/bootst - 创建全局 `moviepilot` 命令 - 默认启动前后端服务 +说明: + +- 如果系统里没有可用的 `Python 3.12+`,脚本会优先尝试自动补齐运行环境,再继续安装 +- Linux 下安装系统依赖时通常需要 `sudo` +- 复用已有仓库时,脚本现在只会因为已跟踪源码改动而阻止自动更新,不会再被 `.DS_Store` 之类未跟踪文件卡住 + 如果安装完成后当前终端仍提示找不到 `moviepilot`: - 重新打开终端 diff --git a/moviepilot b/moviepilot index df577558..b61bfd10 100755 --- a/moviepilot +++ b/moviepilot @@ -175,15 +175,40 @@ Options: EOF } +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_system_python() { if command -v python3 >/dev/null 2>&1; then - command -v python3 - return 0 + 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 - command -v python - return 0 + 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 uv_bin + 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 + fi + done return 1 } diff --git a/scripts/bootstrap-local.sh b/scripts/bootstrap-local.sh index 347936f0..91de0659 100755 --- a/scripts/bootstrap-local.sh +++ b/scripts/bootstrap-local.sh @@ -14,6 +14,9 @@ START_AFTER_INSTALL="true" NON_INTERACTIVE="false" OS_NAME="Unknown" PYTHON_BIN="" +BREW_BIN="" +PACKAGE_MANAGER="" +PACKAGE_INDEX_UPDATED="false" PROMPT_INPUT="/dev/stdin" PROMPT_OUTPUT="/dev/stdout" HAS_TTY="false" @@ -47,7 +50,7 @@ EOF repo_dirty() { ( cd "$1" - git status --porcelain 2>/dev/null | grep -q . + git status --porcelain --untracked-files=no 2>/dev/null | grep -q . ) } @@ -137,6 +140,34 @@ detect_os() { fi } +detect_package_manager() { + case "$OS_NAME" in + macOS) + PACKAGE_MANAGER="brew" + ;; + Linux*) + if command -v apt-get >/dev/null 2>&1; then + PACKAGE_MANAGER="apt-get" + elif command -v dnf >/dev/null 2>&1; then + PACKAGE_MANAGER="dnf" + elif command -v yum >/dev/null 2>&1; then + PACKAGE_MANAGER="yum" + elif command -v zypper >/dev/null 2>&1; then + PACKAGE_MANAGER="zypper" + elif command -v pacman >/dev/null 2>&1; then + PACKAGE_MANAGER="pacman" + elif command -v apk >/dev/null 2>&1; then + PACKAGE_MANAGER="apk" + else + PACKAGE_MANAGER="" + fi + ;; + *) + PACKAGE_MANAGER="" + ;; + esac +} + find_python() { if command -v python3 >/dev/null 2>&1; then command -v python3 @@ -160,10 +191,12 @@ PY python_install_hint() { case "$OS_NAME" in macOS) - echo "请先安装 Git、curl 和 Python 3.12,例如:brew install git curl python@3.12" >&2 + echo "脚本已尝试自动安装 Git、curl 和 Python 3.12+。" >&2 + echo "如果自动安装失败,请先安装 Homebrew,或手动执行:brew install git curl python@3.12" >&2 ;; Linux*) - echo "请先安装 Git、curl 和 Python 3.12,并确保包含 venv 模块。" >&2 + 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 ;; @@ -176,27 +209,183 @@ python_install_hint() { esac } -require_prereqs() { +setup_brew_env() { + local candidate="" + for candidate in "$BREW_BIN" "$(command -v brew 2>/dev/null || true)" /opt/homebrew/bin/brew /usr/local/bin/brew /home/linuxbrew/.linuxbrew/bin/brew "$HOME/.linuxbrew/bin/brew"; do + if [[ -n "$candidate" && -x "$candidate" ]]; then + BREW_BIN="$candidate" + eval "$("$BREW_BIN" shellenv)" + return 0 + fi + done + return 1 +} + +ensure_brew() { + if setup_brew_env; then + return 0 + fi + + echo "==> 未找到 Homebrew,开始自动安装" + NONINTERACTIVE=1 CI=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + if ! setup_brew_env; then + echo "自动安装 Homebrew 失败。" >&2 + return 1 + fi +} + +run_privileged() { + if [[ "$(id -u)" -eq 0 ]]; then + "$@" + return + fi + + if ! command -v sudo >/dev/null 2>&1; then + echo "当前步骤需要 sudo 权限,但系统中未找到 sudo。" >&2 + return 1 + fi + + if [[ "$HAS_TTY" == "true" ]]; then + sudo "$@" + return + fi + + if sudo -n true >/dev/null 2>&1; then + sudo -n "$@" + return + fi + + echo "当前步骤需要 sudo 权限,请在可交互终端中重新运行脚本。" >&2 + return 1 +} + +refresh_package_index() { + if [[ "$PACKAGE_INDEX_UPDATED" == "true" ]]; then + return + fi + + case "$PACKAGE_MANAGER" in + apt-get) + run_privileged apt-get update + ;; + pacman) + run_privileged pacman -Sy --noconfirm + ;; + zypper) + run_privileged zypper --gpg-auto-import-keys refresh + ;; + apk) + run_privileged apk update + ;; + esac + + PACKAGE_INDEX_UPDATED="true" +} + +install_system_packages() { + local packages=("$@") + if [[ "${#packages[@]}" -eq 0 ]]; then + return 0 + fi + + case "$PACKAGE_MANAGER" in + brew) + ensure_brew + "$BREW_BIN" install "${packages[@]}" + ;; + apt-get) + refresh_package_index + run_privileged apt-get install -y "${packages[@]}" + ;; + dnf) + run_privileged dnf install -y "${packages[@]}" + ;; + yum) + run_privileged yum install -y "${packages[@]}" + ;; + zypper) + refresh_package_index + run_privileged zypper install -y "${packages[@]}" + ;; + pacman) + refresh_package_index + run_privileged pacman -S --noconfirm --needed "${packages[@]}" + ;; + apk) + refresh_package_index + run_privileged apk add --no-cache "${packages[@]}" + ;; + *) + echo "当前系统暂不支持自动安装依赖,请手动安装:${packages[*]}" >&2 + return 1 + ;; + esac +} + +ensure_base_tools() { + local missing=() + + if ! command -v git >/dev/null 2>&1; then + missing+=("git") + fi + + if ! command -v curl >/dev/null 2>&1; then + missing+=("curl") + fi + + if [[ "${#missing[@]}" -eq 0 ]]; then + return 0 + fi + + echo "==> 自动安装基础依赖: ${missing[*]}" + install_system_packages "${missing[@]}" + hash -r + + if ! command -v git >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then + echo "基础依赖安装失败,请确认 git 和 curl 可用后重试。" >&2 + return 1 + fi +} + +ensure_uv() { + if command -v uv >/dev/null 2>&1; then + return 0 + fi + + echo "==> 自动安装 uv,用于拉取 Python 3.12+" + 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 + + if ! command -v uv >/dev/null 2>&1; then + echo "uv 安装失败,无法继续自动安装 Python。" >&2 + return 1 + fi +} + +ensure_python() { + PYTHON_BIN="$(find_python || true)" + if [[ -n "$PYTHON_BIN" ]] && python_version_ok "$PYTHON_BIN"; then + return 0 + fi + + echo "==> 未找到可用的 Python 3.12+,开始自动安装独立 Python 运行时" + ensure_uv + uv python install 3.12 + PYTHON_BIN="$(uv python find 3.12 || true)" + if [[ -z "$PYTHON_BIN" ]] || ! python_version_ok "$PYTHON_BIN"; then + echo "自动安装 Python 3.12+ 失败。" >&2 + return 1 + fi +} + +ensure_prereqs() { if [[ "$OS_NAME" == "Windows" ]]; then echo "检测到当前环境为 Windows shell,建议改用 WSL、Linux 或 macOS 终端运行。" >&2 exit 1 fi - if ! command -v git >/dev/null 2>&1; then - echo "未找到 git。" >&2 - python_install_hint - exit 1 - fi - - if ! command -v curl >/dev/null 2>&1; then - echo "未找到 curl。" >&2 - python_install_hint - exit 1 - fi - - PYTHON_BIN="$(find_python || true)" - if [[ -z "$PYTHON_BIN" ]] || ! python_version_ok "$PYTHON_BIN"; then - echo "未找到可用的 Python 3.12+ 解释器。" >&2 + if ! ensure_base_tools || ! ensure_python; then python_install_hint exit 1 fi @@ -379,8 +568,9 @@ while [[ $# -gt 0 ]]; do done detect_os +detect_package_manager setup_prompt_io -require_prereqs +ensure_prereqs ensure_link_path if [[ "$NON_INTERACTIVE" != "true" && "$HAS_TTY" == "true" ]]; then @@ -398,14 +588,14 @@ sync_repo cd "$APP_DIR" echo "==> 执行本地环境安装与初始化" -SETUP_ARGS=(setup --config-dir "$CONFIG_DIR") +SETUP_ARGS=(setup --python "$PYTHON_BIN" --config-dir "$CONFIG_DIR") if [[ "$RUN_WIZARD" == "true" ]]; then SETUP_ARGS+=(--wizard) fi if [[ "$HAS_TTY" == "true" ]]; then - ./moviepilot "${SETUP_ARGS[@]}" <"$PROMPT_INPUT" + "$PYTHON_BIN" ./scripts/local_setup.py "${SETUP_ARGS[@]}" <"$PROMPT_INPUT" else - ./moviepilot "${SETUP_ARGS[@]}" + "$PYTHON_BIN" ./scripts/local_setup.py "${SETUP_ARGS[@]}" fi if [[ "$LINK_CLI" == "true" ]]; then