feat: 使用 uv 锁定主程序依赖并强化插件恢复边界 (#6364)

This commit is contained in:
InfinityPacer
2026-08-20 12:17:19 +08:00
committed by GitHub
parent 27ae1b5290
commit 23f5d59c74
59 changed files with 6804 additions and 1797 deletions
+20 -17
View File
@@ -16,6 +16,7 @@ SUPERUSER=""
SUPERUSER_PASSWORD=""
OS_NAME="Unknown"
PYTHON_BIN=""
UV_VERSION="0.12.5"
BREW_BIN=""
PACKAGE_MANAGER=""
PACKAGE_INDEX_UPDATED="false"
@@ -177,7 +178,7 @@ 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)
raise SystemExit(0 if sys.version_info >= (3, 12) else 1)
PY
}
@@ -195,7 +196,7 @@ try_python_candidate() {
find_python() {
local minor=""
for minor in 20 19 18 17 16 15 14 13 12 11; do
for minor in 20 19 18 17 16 15 14 13 12; do
if try_python_candidate "python3.$minor"; then
return 0
fi
@@ -227,20 +228,20 @@ find_uv_python() {
python_install_hint() {
case "$OS_NAME" in
macOS)
echo "脚本已尝试自动安装 Git、curl 和 Python 3.11+。" >&2
echo "如果自动安装失败,请先安装 Homebrew,或手动执行:brew install git curl python@3.11" >&2
echo "脚本已尝试自动安装 Git、curl 和 Python 3.12+。" >&2
echo "如果自动安装失败,请先安装 Homebrew,或手动执行:brew install git curl python@3.12" >&2
;;
Linux*)
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
echo "脚本已尝试自动安装 Git、curl 和 Python 3.12+。" >&2
echo "如果自动安装失败,请先安装 Git、curl、Python 3.12+。" >&2
echo "例如 Debian/Ubuntu: sudo apt install git curl python3.12" >&2
echo "例如 Fedora/RHEL: sudo dnf install git curl python3.12" >&2
;;
Windows)
echo "推荐在 WSL、Linux 或 macOS 终端中运行此脚本。" >&2
;;
*)
echo "请先安装 Git、curl、Python 3.11 或更高版本。" >&2
echo "请先安装 Git、curl、Python 3.12+。" >&2
;;
esac
}
@@ -384,17 +385,19 @@ ensure_base_tools() {
}
ensure_uv() {
if command -v uv >/dev/null 2>&1; then
if command -v uv >/dev/null 2>&1 \
&& [[ "$(uv --version 2>/dev/null | awk '{print $2}')" == "${UV_VERSION}" ]]; then
return 0
fi
echo "==> 自动安装 uv,用于补齐 Python 3.11+ 运行时"
env UV_INSTALL_DIR="$HOME/.local/bin" sh -c "$(curl -LsSf https://astral.sh/uv/install.sh)"
echo "==> 自动安装 uv ${UV_VERSION}"
env UV_INSTALL_DIR="$HOME/.local/bin" sh -c "$(curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh")"
export PATH="$HOME/.local/bin:$PATH"
hash -r
if ! command -v uv >/dev/null 2>&1; then
echo "uv 安装失败,无法继续自动安装 Python。" >&2
if ! command -v uv >/dev/null 2>&1 \
|| [[ "$(uv --version 2>/dev/null | awk '{print $2}')" != "${UV_VERSION}" ]]; then
echo "uv ${UV_VERSION} 安装失败,无法继续自动安装 Python。" >&2
return 1
fi
}
@@ -412,11 +415,11 @@ ensure_python() {
return 0
fi
echo "==> 未找到可用的 Python 3.11+,开始自动安装独立 Python 运行时"
uv python install 3.11
echo "==> 未找到可用的 Python 3.12+,开始自动安装 Python 3.12"
uv python install 3.12
PYTHON_BIN="$(find_uv_python "$(command -v uv)" || true)"
if [[ -z "$PYTHON_BIN" ]] || ! python_version_ok "$PYTHON_BIN"; then
echo "自动安装 Python 3.11+ 失败。" >&2
echo "自动安装 Python 3.12 失败。" >&2
return 1
fi
}
+76 -95
View File
@@ -5,22 +5,22 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT
mkdir -p "${TMP_DIR}/venv/bin" "${TMP_DIR}/config"
mkdir -p "${TMP_DIR}/bin" "${TMP_DIR}/venv/bin" "${TMP_DIR}/config"
cat > "${TMP_DIR}/venv/bin/pip" <<'SH'
cat > "${TMP_DIR}/bin/uv" <<'SH'
#!/usr/bin/env bash
printf 'argv=%s\n' "$*" >> "${MP_FAKE_PIP_LOG}"
printf 'HTTP_PROXY=%s\n' "${HTTP_PROXY:-}" >> "${MP_FAKE_PIP_LOG}"
printf 'HTTPS_PROXY=%s\n' "${HTTPS_PROXY:-}" >> "${MP_FAKE_PIP_LOG}"
printf 'PACKAGE_CACHE_ROOT=%s\n' "${PACKAGE_CACHE_ROOT:-}" >> "${MP_FAKE_PIP_LOG}"
printf 'PIP_CACHE_DIR=%s\n' "${PIP_CACHE_DIR:-}" >> "${MP_FAKE_PIP_LOG}"
printf 'UV_CACHE_DIR=%s\n' "${UV_CACHE_DIR:-}" >> "${MP_FAKE_PIP_LOG}"
if [ "${MP_FAKE_PIP_FAIL:-}" = "1" ]; then
printf 'argv=%s\n' "$*" >> "${MP_FAKE_UV_LOG}"
printf 'HTTP_PROXY=%s\n' "${HTTP_PROXY:-}" >> "${MP_FAKE_UV_LOG}"
printf 'HTTPS_PROXY=%s\n' "${HTTPS_PROXY:-}" >> "${MP_FAKE_UV_LOG}"
printf 'PACKAGE_CACHE_ROOT=%s\n' "${PACKAGE_CACHE_ROOT:-}" >> "${MP_FAKE_UV_LOG}"
printf 'UV_CACHE_DIR=%s\n' "${UV_CACHE_DIR:-}" >> "${MP_FAKE_UV_LOG}"
printf 'UV_PROJECT_ENVIRONMENT=%s\n' "${UV_PROJECT_ENVIRONMENT:-}" >> "${MP_FAKE_UV_LOG}"
if [ "${MP_FAKE_UV_FAIL:-}" = "1" ]; then
exit 1
fi
exit 0
SH
chmod +x "${TMP_DIR}/venv/bin/pip"
chmod +x "${TMP_DIR}/bin/uv"
assert_contains() {
local needle="$1"
@@ -42,52 +42,50 @@ assert_not_contains() {
fi
}
UPDATE_FUNCS="${TMP_DIR}/update-functions.sh"
awk '
BEGIN {capture=1}
/^if \[\[ "\$\{MOVIEPILOT_AUTO_UPDATE\}"/ {capture=0}
capture {print}
' "${ROOT}/docker/update.sh" > "${UPDATE_FUNCS}"
# macOS 默认不提供 GNU timeout;模拟器只需保留被执行命令的参数和环境。
timeout() {
if [[ "${1:-}" == --kill-after=* ]]; then
shift
fi
shift
"$@"
}
MP_FAKE_PIP_LOG="${TMP_DIR}/update.log"
export MP_FAKE_PIP_LOG
MP_FAKE_UV_LOG="${TMP_DIR}/update.log"
export MP_FAKE_UV_LOG
export UV_BIN="${TMP_DIR}/bin/uv"
export VENV_PATH="${TMP_DIR}/venv"
export CONFIG_DIR="${TMP_DIR}/config"
export MOVIEPILOT_AUTO_UPDATE=false
export PIP_PROXY="https://mirror.example/simple"
export PROXY_HOST="http://proxy.example:7890"
unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
source "${UPDATE_FUNCS}" >/dev/null
unset PACKAGE_CACHE_ROOT UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
source "${ROOT}/docker/update.sh" >/dev/null
: > "${MP_FAKE_PIP_LOG}"
test_connectivity_pip 0
assert_contains "argv=install -i https://mirror.example/simple pip-hello-world" "${MP_FAKE_PIP_LOG}"
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}"
assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/config/.cache" "${MP_FAKE_PIP_LOG}"
assert_contains "PIP_CACHE_DIR=${TMP_DIR}/config/.cache/pip" "${MP_FAKE_PIP_LOG}"
assert_contains "UV_CACHE_DIR=${TMP_DIR}/config/.cache/uv" "${MP_FAKE_PIP_LOG}"
if [[ "${PIP_OPTIONS}" != "-i ${PIP_PROXY}" ]]; then
echo "mirror branch must preserve index option: ${PIP_OPTIONS}" >&2
exit 1
fi
if [[ "${PIP_OPTIONS}" == *"--proxy"* ]]; then
echo "PIP_OPTIONS must not contain --proxy: ${PIP_OPTIONS}" >&2
: > "${MP_FAKE_UV_LOG}"
test_connectivity_package 0
assert_contains "argv=pip install --target " "${MP_FAKE_UV_LOG}"
assert_contains "--no-deps --default-index https://mirror.example/simple pip-hello-world" "${MP_FAKE_UV_LOG}"
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_UV_LOG}"
assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/config/.cache" "${MP_FAKE_UV_LOG}"
assert_contains "UV_CACHE_DIR=${TMP_DIR}/config/.cache/uv" "${MP_FAKE_UV_LOG}"
if [[ "${UV_OPTIONS[*]}" != "--default-index ${PIP_PROXY}" ]]; then
echo "mirror branch must preserve uv index option: ${UV_OPTIONS[*]}" >&2
exit 1
fi
if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" || -n "${http_proxy:-}" || -n "${https_proxy:-}" ]]; then
echo "pip connectivity must not leak PROXY_HOST into parent proxy env" >&2
echo "package connectivity must not leak PROXY_HOST into parent proxy env" >&2
env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true
exit 1
fi
assert_not_contains "user:pass" "${MP_FAKE_PIP_LOG}"
: > "${MP_FAKE_PIP_LOG}"
: > "${MP_FAKE_UV_LOG}"
PIP_PROXY=""
test_connectivity_pip 1
assert_contains "argv=install pip-hello-world" "${MP_FAKE_PIP_LOG}"
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}"
if [[ -n "${PIP_OPTIONS}" ]]; then
echo "proxy branch must keep PIP_OPTIONS empty: ${PIP_OPTIONS}" >&2
test_connectivity_package 1
assert_contains "argv=pip install --target " "${MP_FAKE_UV_LOG}"
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_UV_LOG}"
if [[ ${#UV_OPTIONS[@]} -ne 0 ]]; then
echo "proxy branch must keep UV_OPTIONS empty: ${UV_OPTIONS[*]}" >&2
exit 1
fi
if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" || -n "${http_proxy:-}" || -n "${https_proxy:-}" ]]; then
@@ -96,66 +94,54 @@ if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" || -n "${http_proxy:-}" || -
exit 1
fi
MP_FAKE_PIP_LOG="${TMP_DIR}/update-explicit-standard-proxy.log"
export MP_FAKE_PIP_LOG
MP_FAKE_UV_LOG="${TMP_DIR}/update-explicit-standard-proxy.log"
export MP_FAKE_UV_LOG
(
export VENV_PATH="${TMP_DIR}/venv"
export CONFIG_DIR="${TMP_DIR}/config"
export MOVIEPILOT_AUTO_UPDATE=false
export PIP_PROXY=""
export PROXY_HOST="http://proxy.example:7890"
export HTTP_PROXY="http://explicit.example:8080"
export HTTPS_PROXY="http://explicit.example:8080"
export http_proxy="http://explicit.example:8080"
export https_proxy="http://explicit.example:8080"
source "${UPDATE_FUNCS}" >/dev/null
test_connectivity_pip 1
source "${ROOT}/docker/update.sh" >/dev/null
test_connectivity_package 1
if [[ "${HTTP_PROXY}" != "http://explicit.example:8080" || "${HTTPS_PROXY}" != "http://explicit.example:8080" ]]; then
echo "explicit standard proxy env must be preserved" >&2
env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true
exit 1
fi
)
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}"
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_UV_LOG}"
MP_FAKE_PIP_LOG="${TMP_DIR}/update-explicit-cache.log"
export MP_FAKE_PIP_LOG
MP_FAKE_UV_LOG="${TMP_DIR}/update-explicit-cache.log"
export MP_FAKE_UV_LOG
(
export VENV_PATH="${TMP_DIR}/venv"
export CONFIG_DIR="${TMP_DIR}/config"
export MOVIEPILOT_AUTO_UPDATE=false
export PACKAGE_CACHE_ROOT="${TMP_DIR}/update-custom-package-cache"
export PIP_CACHE_DIR="${TMP_DIR}/explicit-pip-cache"
export UV_CACHE_DIR="${TMP_DIR}/explicit-uv-cache"
export PIP_PROXY="https://mirror.example/simple"
export PROXY_HOST="http://proxy.example:7890"
source "${UPDATE_FUNCS}" >/dev/null
test_connectivity_pip 0
source "${ROOT}/docker/update.sh" >/dev/null
test_connectivity_package 0
)
assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/update-custom-package-cache" "${MP_FAKE_UV_LOG}"
assert_contains "UV_CACHE_DIR=${TMP_DIR}/explicit-uv-cache" "${MP_FAKE_UV_LOG}"
assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/update-custom-package-cache" "${MP_FAKE_PIP_LOG}"
assert_contains "PIP_CACHE_DIR=${TMP_DIR}/explicit-pip-cache" "${MP_FAKE_PIP_LOG}"
assert_contains "UV_CACHE_DIR=${TMP_DIR}/explicit-uv-cache" "${MP_FAKE_PIP_LOG}"
MP_FAKE_PIP_LOG="${TMP_DIR}/update-fallback-no-proxy.log"
export MP_FAKE_PIP_LOG
MP_FAKE_UV_LOG="${TMP_DIR}/update-fallback-no-proxy.log"
export MP_FAKE_UV_LOG
(
export VENV_PATH="${TMP_DIR}/venv"
export CONFIG_DIR="${TMP_DIR}/config"
export MOVIEPILOT_AUTO_UPDATE=false
export PIP_PROXY="https://mirror.example/simple"
export PROXY_HOST="http://proxy.example:7890"
unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
source "${UPDATE_FUNCS}" >/dev/null
MP_FAKE_PIP_FAIL=1 test_connectivity_pip 0 && exit 1
if [[ -n "${HTTPS_PROXY:-}" || -n "${https_proxy:-}" ]]; then
echo "mirror failure must not leak proxy env" >&2
env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true
exit 1
fi
test_connectivity_pip 2
if [[ "${PIP_LOG}" != "不使用代理" ]]; then
echo "fallback branch must report direct mode: ${PIP_LOG}" >&2
unset PACKAGE_CACHE_ROOT UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
source "${ROOT}/docker/update.sh" >/dev/null
MP_FAKE_UV_FAIL=1 test_connectivity_package 0 && exit 1
test_connectivity_package 2
if [[ "${PACKAGE_LOG}" != "不使用代理" ]]; then
echo "fallback branch must report direct mode: ${PACKAGE_LOG}" >&2
exit 1
fi
)
@@ -183,13 +169,13 @@ exit 0
SH
chmod +x "${TMP_DIR}/venv/bin/python3"
MP_FAKE_PIP_LOG="${TMP_DIR}/entrypoint.log"
MP_FAKE_UV_LOG="${TMP_DIR}/entrypoint.log"
MP_FAKE_PYTHON_COUNT="${TMP_DIR}/python-count"
export MP_FAKE_PIP_LOG MP_FAKE_PYTHON_COUNT
export MP_FAKE_UV_LOG MP_FAKE_PYTHON_COUNT
(
export VENV_PATH="${TMP_DIR}/venv"
export CONFIG_DIR="${TMP_DIR}/config"
unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
unset PACKAGE_CACHE_ROOT UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
export PIP_PROXY=""
export PROXY_HOST="http://proxy.example:7890"
source "${ENTRYPOINT_FUNCS}"
@@ -197,25 +183,24 @@ export MP_FAKE_PIP_LOG MP_FAKE_PYTHON_COUNT
ensure_backend_runtime_dependencies
if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" || -n "${http_proxy:-}" || -n "${https_proxy:-}" ]]; then
echo "dependency recovery must not leak PROXY_HOST into parent proxy env" >&2
env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true
exit 1
fi
) >/dev/null
assert_contains "argv=install -r /app/requirements.txt" "${MP_FAKE_PIP_LOG}"
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}"
assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/config/.cache" "${MP_FAKE_PIP_LOG}"
assert_contains "PIP_CACHE_DIR=${TMP_DIR}/config/.cache/pip" "${MP_FAKE_PIP_LOG}"
assert_contains "UV_CACHE_DIR=${TMP_DIR}/config/.cache/uv" "${MP_FAKE_PIP_LOG}"
assert_not_contains "--proxy" "${MP_FAKE_PIP_LOG}"
assert_contains "argv=sync --project /app --locked --no-dev --no-install-project --inexact" "${MP_FAKE_UV_LOG}"
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_UV_LOG}"
assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/config/.cache" "${MP_FAKE_UV_LOG}"
assert_contains "UV_CACHE_DIR=${TMP_DIR}/config/.cache/uv" "${MP_FAKE_UV_LOG}"
assert_contains "UV_PROJECT_ENVIRONMENT=${TMP_DIR}/venv" "${MP_FAKE_UV_LOG}"
assert_not_contains "requirements" "${MP_FAKE_UV_LOG}"
MP_FAKE_PIP_LOG="${TMP_DIR}/entrypoint-explicit-standard-proxy.log"
MP_FAKE_UV_LOG="${TMP_DIR}/entrypoint-explicit-standard-proxy.log"
MP_FAKE_PYTHON_COUNT="${TMP_DIR}/python-count-explicit-standard-proxy"
export MP_FAKE_PIP_LOG MP_FAKE_PYTHON_COUNT
export MP_FAKE_UV_LOG MP_FAKE_PYTHON_COUNT
(
export VENV_PATH="${TMP_DIR}/venv"
export CONFIG_DIR="${TMP_DIR}/config"
unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR
unset PACKAGE_CACHE_ROOT UV_CACHE_DIR
export PIP_PROXY=""
export PROXY_HOST="http://proxy.example:7890"
export HTTP_PROXY="http://explicit.example:8080"
@@ -227,32 +212,28 @@ export MP_FAKE_PIP_LOG MP_FAKE_PYTHON_COUNT
ensure_backend_runtime_dependencies
if [[ "${HTTP_PROXY}" != "http://explicit.example:8080" || "${HTTPS_PROXY}" != "http://explicit.example:8080" ]]; then
echo "dependency recovery must preserve explicit standard proxy env" >&2
env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true
exit 1
fi
) >/dev/null
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_UV_LOG}"
assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}"
MP_FAKE_PIP_LOG="${TMP_DIR}/entrypoint-app-env.log"
MP_FAKE_UV_LOG="${TMP_DIR}/entrypoint-app-env.log"
MP_FAKE_PYTHON_COUNT="${TMP_DIR}/python-count-app-env"
cat > "${TMP_DIR}/config/app.env" <<EOF
PACKAGE_CACHE_ROOT='${TMP_DIR}/app-env-custom-package-cache'
PROXY_HOST='http://proxy.example:7890'
EOF
export MP_FAKE_PIP_LOG MP_FAKE_PYTHON_COUNT
export MP_FAKE_UV_LOG MP_FAKE_PYTHON_COUNT
(
export VENV_PATH="${TMP_DIR}/venv"
export CONFIG_DIR="${TMP_DIR}/config"
unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR PIP_PROXY PROXY_HOST
unset PACKAGE_CACHE_ROOT UV_CACHE_DIR PIP_PROXY PROXY_HOST
source "${ENTRYPOINT_FUNCS}"
load_config_from_app_env
apply_package_cache_env
ensure_backend_runtime_dependencies
) >/dev/null
assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/app-env-custom-package-cache" "${MP_FAKE_PIP_LOG}"
assert_contains "PIP_CACHE_DIR=${TMP_DIR}/app-env-custom-package-cache/pip" "${MP_FAKE_PIP_LOG}"
assert_contains "UV_CACHE_DIR=${TMP_DIR}/app-env-custom-package-cache/uv" "${MP_FAKE_PIP_LOG}"
assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/app-env-custom-package-cache" "${MP_FAKE_UV_LOG}"
assert_contains "UV_CACHE_DIR=${TMP_DIR}/app-env-custom-package-cache/uv" "${MP_FAKE_UV_LOG}"
echo "Docker package env simulation passed"
+6 -7
View File
@@ -21,7 +21,6 @@ def sample(name: str, request: PackageInstallRequest) -> None:
print(rendered)
assert all("--proxy" not in arg for arg in strategy.command)
assert "user:pass" not in rendered
assert strategy.env["PIP_CACHE_DIR"].endswith("/.cache/pip")
assert strategy.env["UV_CACHE_DIR"].endswith("/.cache/uv")
if strategy.strategy_name.endswith("代理") or strategy.strategy_name.endswith("镜像+代理"):
assert strategy.env["HTTPS_PROXY"] == "http://proxy.example:7890"
@@ -35,31 +34,31 @@ def main() -> None:
samples = {
"plain": PackageInstallRequest(
requirements_file=requirements,
dependency_file=requirements,
python_bin=python_bin,
config_dir=config_dir,
),
"mirror": PackageInstallRequest(
requirements_file=requirements,
dependency_file=requirements,
python_bin=python_bin,
config_dir=config_dir,
pip_index_url="https://user:pass@mirror.example/simple",
package_index_url="https://user:pass@mirror.example/simple",
),
"proxy": PackageInstallRequest(
requirements_file=requirements,
dependency_file=requirements,
python_bin=python_bin,
config_dir=config_dir,
proxy_url="http://proxy.example:7890",
),
"mirror_proxy_wheels": PackageInstallRequest(
requirements_file=requirements,
dependency_file=requirements,
python_bin=python_bin,
config_dir=config_dir,
find_links_dirs=[
root / "plugins.v2" / "demo" / "wheels",
root / "plugins.v2" / "other" / "wheels",
],
pip_index_url="https://user:pass@mirror.example/simple",
package_index_url="https://user:pass@mirror.example/simple",
proxy_url="http://proxy.example:7890",
),
}
+90 -89
View File
@@ -33,10 +33,9 @@ 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]} 或更高版本"
)
MIN_PYTHON_VERSION = (3, 12)
SUPPORTED_PYTHON_TEXT = "Python 3.12+"
UV_VERSION = "0.12.5"
CONFIG_DIR = LEGACY_CONFIG_DIR
LOG_DIR = CONFIG_DIR / "logs"
@@ -525,12 +524,10 @@ def build_package_install_env() -> dict[str, str]:
env = os.environ.copy()
package_cache_root = env.get("PACKAGE_CACHE_ROOT", "").strip() or str(CONFIG_DIR / ".cache")
env.setdefault("PACKAGE_CACHE_ROOT", package_cache_root)
env.setdefault("PIP_CACHE_DIR", os.path.join(package_cache_root, "pip"))
env.setdefault("UV_CACHE_DIR", os.path.join(package_cache_root, "uv"))
index_url = env.get("PIP_PROXY", "").strip()
if index_url:
env["PIP_INDEX_URL"] = index_url
env["UV_DEFAULT_INDEX"] = index_url
proxy = env.get("PROXY_HOST", "").strip()
@@ -620,67 +617,36 @@ def get_venv_bin_dir(venv_dir: Path) -> Path:
return venv_dir / "bin"
def get_venv_pip(venv_dir: Path) -> Path:
if os.name == "nt":
return get_venv_bin_dir(venv_dir) / "pip.exe"
return get_venv_bin_dir(venv_dir) / "pip"
def require_uv() -> Path:
"""返回仓库要求版本的 uv,避免不同安装入口使用不同解析器。"""
uv_command = shutil.which("uv")
if not uv_command:
raise RuntimeError(
f"未找到 uv {UV_VERSION},请先安装后重新执行。"
)
uv_bin = Path(uv_command).expanduser().resolve()
version = capture([str(uv_bin), "--version"])
if version.split()[:2] != ["uv", UV_VERSION]:
raise RuntimeError(
f"MoviePilot 需要 uv {UV_VERSION},当前为 {version or '未知版本'}"
)
return uv_bin
def _ensure_uv_available_for_venv(venv_dir: Path, venv_python: Path) -> Optional[Path]:
if os.name == "nt":
return None
def expose_uv_to_venv(uv_bin: Path, venv_dir: Path) -> Path:
"""让运行时能从虚拟环境旁定位同一 uv 二进制。"""
venv_bin = get_venv_bin_dir(venv_dir)
uv_bin = venv_bin / "uv"
if uv_bin.exists():
return uv_bin
system_uv = shutil.which("uv")
if system_uv:
uv_target = Path(system_uv).expanduser().resolve()
print_step(f"复用系统 uv{uv_target}")
if uv_bin.exists() or uv_bin.is_symlink():
uv_bin.unlink()
uv_bin.symlink_to(uv_target)
return uv_bin
print_step("当前未检测到 uv,先在虚拟环境内安装 uv")
command = [str(venv_python), "-m", "pip", "install", "--upgrade", "pip", "uv"]
run(command, env=build_package_install_env(), safe_command=redact_command(command))
if uv_bin.exists():
return uv_bin
raise RuntimeError("uv 安装完成,但虚拟环境中未找到 uv 可执行文件")
def configure_venv_pip_compat(venv_dir: Path, venv_python: Path) -> Path:
"""
在虚拟环境中安装 uv 并保持 pip 命令兼容,供现有安装流程复用。
"""
runtime_uv = venv_bin / ("uv.exe" if os.name == "nt" else "uv")
venv_bin.mkdir(parents=True, exist_ok=True)
if runtime_uv.resolve() == uv_bin.resolve():
return runtime_uv
if runtime_uv.exists() or runtime_uv.is_symlink():
runtime_uv.unlink()
if os.name == "nt":
return get_venv_pip(venv_dir)
_ensure_uv_available_for_venv(venv_dir, venv_python)
venv_bin = get_venv_bin_dir(venv_dir)
wrapper_src = ROOT / "scripts" / "uv-pip-compat.sh"
wrapper_dst = venv_bin / "uv-pip-compat"
shutil.copy2(wrapper_src, wrapper_dst)
wrapper_dst.chmod(0o755)
python_version = get_python_version(str(venv_python))
compat_links = {
"pip",
"pip3",
f"pip{python_version[0]}",
f"pip{python_version[0]}.{python_version[1]}",
"pip-compile",
"pip-sync",
}
for link_name in compat_links:
link_path = venv_bin / link_name
if link_path.exists() or link_path.is_symlink():
link_path.unlink()
link_path.symlink_to(wrapper_dst.name)
return get_venv_pip(venv_dir)
shutil.copy2(uv_bin, runtime_uv)
else:
runtime_uv.symlink_to(uv_bin)
return runtime_uv
def ensure_supported_python(python_bin: str) -> None:
@@ -692,6 +658,12 @@ def ensure_supported_python(python_bin: str) -> None:
)
def resolve_python_path(python_bin: str) -> Path:
"""解析解释器命令,优先按 PATH 找到实际执行文件。"""
resolved = shutil.which(python_bin)
return Path(resolved or python_bin).expanduser().resolve()
def ensure_local_dirs() -> None:
for path in (CONFIG_DIR, LOG_DIR, CACHE_DIR, TEMP_DIR, COOKIE_DIR, RUNTIME_DIR):
path.mkdir(parents=True, exist_ok=True)
@@ -2743,33 +2715,62 @@ def install_deps(*, python_bin: str, venv_dir: Path, recreate: bool) -> Path:
"""
ensure_supported_python(python_bin)
venv_dir = venv_dir.expanduser().resolve()
if recreate:
requested_python = resolve_python_path(python_bin)
executing_python = Path(sys.executable).expanduser().resolve()
if requested_python.is_relative_to(venv_dir) or executing_python.is_relative_to(
venv_dir
):
raise RuntimeError(
"重建虚拟环境需要使用 venv 外部的 Python 3.12+ 解释器。"
)
uv_bin = require_uv()
temporary_uv_dir: Optional[TemporaryDirectory[str]] = None
if recreate and venv_dir.exists() and uv_bin.is_relative_to(venv_dir):
temporary_uv_dir = TemporaryDirectory(prefix="moviepilot-uv-")
temporary_uv = Path(temporary_uv_dir.name) / uv_bin.name
shutil.copy2(uv_bin, temporary_uv)
uv_bin = temporary_uv
venv_python = get_venv_python(venv_dir)
venv_pip = get_venv_pip(venv_dir)
print_step(f"使用 Python 解释器:{python_bin}")
try:
if recreate and venv_dir.exists():
print_step(f"删除已有虚拟环境:{venv_dir}")
shutil.rmtree(venv_dir)
if recreate and venv_dir.exists():
print_step(f"删除已有虚拟环境:{venv_dir}")
shutil.rmtree(venv_dir)
if venv_python.exists():
print_step(f"复用已有虚拟环境:{venv_dir}")
else:
print_step(f"创建虚拟环境:{venv_dir}")
if not venv_python.exists():
print_step(f"创建虚拟环境:{venv_dir}")
run([python_bin, "-m", "venv", str(venv_dir)])
else:
print_step(f"复用已有虚拟环境:{venv_dir}")
if os.name == "nt":
print_step("升级 pip")
command = [str(venv_python), "-m", "pip", "install", "--upgrade", "pip"]
run(command, env=build_package_install_env(), safe_command=redact_command(command))
else:
print_step("为虚拟环境配置 uv 兼容 pip 命令")
venv_pip = configure_venv_pip_compat(venv_dir, venv_python)
print_step("安装项目依赖")
command = [str(venv_pip), "install", "-r", str(ROOT / "requirements.txt")]
run(command, env=build_package_install_env(), safe_command=redact_command(command))
install_browser_runtime(venv_python)
return venv_python
print_step("同步项目锁定依赖")
command = [
str(uv_bin),
"sync",
"--project",
str(ROOT),
"--locked",
"--no-dev",
"--no-install-project",
"--python",
python_bin,
]
env = build_package_install_env()
env["UV_PROJECT_ENVIRONMENT"] = str(venv_dir)
run(command, env=env, safe_command=redact_command(command))
if temporary_uv_dir is not None:
runtime_uv = get_venv_bin_dir(venv_dir) / (
"uv.exe" if os.name == "nt" else "uv"
)
runtime_uv.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(uv_bin, runtime_uv)
else:
expose_uv_to_venv(uv_bin, venv_dir)
install_browser_runtime(venv_python)
return venv_python
finally:
if temporary_uv_dir is not None:
temporary_uv_dir.cleanup()
def install_browser_runtime(venv_python: Path) -> None:
@@ -3718,7 +3719,7 @@ def build_parser() -> argparse.ArgumentParser:
install_parser.add_argument(
"--python",
default=DEFAULT_BOOTSTRAP_PYTHON,
help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.11+ 版本",
help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.12+ 版本",
)
install_parser.add_argument(
"--venv", default=str(ROOT / "venv"), help="虚拟环境目录"
@@ -3780,7 +3781,7 @@ def build_parser() -> argparse.ArgumentParser:
setup_parser.add_argument(
"--python",
default=DEFAULT_BOOTSTRAP_PYTHON,
help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.11+ 版本",
help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.12+ 版本",
)
setup_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录")
setup_parser.add_argument(
@@ -3852,7 +3853,7 @@ def build_parser() -> argparse.ArgumentParser:
update_parser.add_argument(
"--python",
default=DEFAULT_BOOTSTRAP_PYTHON,
help="用于安装后端依赖的 Python 解释器,默认自动选择本地 3.11+ 版本",
help="用于安装后端依赖的 Python 解释器,默认自动选择本地 3.12+ 版本",
)
update_parser.add_argument(
"--venv", default=str(ROOT / "venv"), help="虚拟环境目录"
+2 -2
View File
@@ -40,9 +40,9 @@ ROLE_LABEL = "org.moviepilot.perf.role"
SOURCE_LABEL = "org.moviepilot.perf.source-commit"
SUBSTRATE_LABEL = "org.moviepilot.perf.substrate"
CRITICAL_SUBSTRATE_PATHS = (
"requirements.in",
"pyproject.toml",
"uv.lock",
"docker/Dockerfile",
"scripts/uv-pip-compat.sh",
)
SEED_COMPATIBILITY_PATHS = ("database/versions",)
AGENT_HEAVY_MODULE_PREFIXES = (
-141
View File
@@ -1,141 +0,0 @@
#!/bin/sh
set -eu
SCRIPT_PATH="$0"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)
COMMAND_NAME=$(basename -- "$SCRIPT_PATH")
if [ "${COMMAND_NAME}" = "uv-pip-compat" ] || [ "${COMMAND_NAME}" = "uv-pip-compat.sh" ]; then
if [ "$#" -eq 0 ]; then
echo "用法: uv-pip-compat <pip|pip-compile|pip-sync> [args...]" >&2
exit 2
fi
COMMAND_NAME="$1"
shift
fi
if [ -x "${SCRIPT_DIR}/uv" ]; then
UV_BIN="${SCRIPT_DIR}/uv"
elif command -v uv >/dev/null 2>&1; then
UV_BIN=$(command -v uv)
else
echo "未找到 uv,可执行 pip 兼容层无法继续运行。" >&2
exit 127
fi
has_environment_option() {
while [ "$#" -gt 0 ]; do
case "$1" in
-p|--python|--python=*|-p*|--system|--user|\
-t|--target|--target=*|-t*|--prefix|--prefix=*)
return 0
;;
--)
return 1
;;
esac
shift
done
return 1
}
normalize_pip_proxy_args() {
output_file="$1"
shift
original_args_file=$(mktemp)
: > "${output_file}"
trap 'rm -f "${original_args_file}"' EXIT HUP INT TERM
for arg in "$@"; do
printf '%s\n' "$arg" >> "${original_args_file}"
done
skip_next=0
while IFS= read -r arg; do
if [ "${skip_next}" -eq 1 ]; then
proxy_value="${arg}"
export HTTP_PROXY="${proxy_value}"
export HTTPS_PROXY="${proxy_value}"
export http_proxy="${proxy_value}"
export https_proxy="${proxy_value}"
skip_next=0
continue
fi
case "$arg" in
--proxy)
skip_next=1
;;
--proxy=*)
proxy_value="${arg#--proxy=}"
export HTTP_PROXY="${proxy_value}"
export HTTPS_PROXY="${proxy_value}"
export http_proxy="${proxy_value}"
export https_proxy="${proxy_value}"
;;
*)
printf '%s\n' "$arg" >> "${output_file}"
;;
esac
done < "${original_args_file}"
rm -f "${original_args_file}"
trap - EXIT HUP INT TERM
}
uv_pip_with_venv_python() {
command_name="$1"
shift
if [ -x "${SCRIPT_DIR}/python" ] && ! has_environment_option "$@"; then
# uv 不会仅凭 pip 软链接位置锁定 venv,本地安装也不会激活 venv。
# 因此需要在会读取或改写环境的 pip 子命令上显式绑定当前 venv 解释器。
exec "${UV_BIN}" pip "${command_name}" --python "${SCRIPT_DIR}/python" "$@"
fi
exec "${UV_BIN}" pip "${command_name}" "$@"
}
case "${COMMAND_NAME}" in
pip|pip3|pip3.*)
if [ "$#" -eq 0 ]; then
exec "${UV_BIN}" pip --help
fi
case "$1" in
-V|--version|version)
exec "${UV_BIN}" --version
;;
help)
shift
exec "${UV_BIN}" help pip "$@"
;;
check|freeze|install|list|show|sync|tree|uninstall)
pip_command="$1"
shift
if [ "${pip_command}" = "install" ]; then
normalized_file=$(mktemp)
normalize_pip_proxy_args "${normalized_file}" "$@"
set --
while IFS= read -r arg; do
set -- "$@" "$arg"
done < "${normalized_file}"
rm -f "${normalized_file}"
fi
uv_pip_with_venv_python "${pip_command}" "$@"
;;
*)
exec "${UV_BIN}" pip "$@"
;;
esac
;;
pip-compile)
exec "${UV_BIN}" pip compile "$@"
;;
pip-sync)
uv_pip_with_venv_python sync "$@"
;;
*)
echo "不支持的 pip 兼容命令入口:${COMMAND_NAME}" >&2
exit 2
;;
esac