mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
feat: 使用 uv 锁定主程序依赖并强化插件恢复边界 (#6364)
This commit is contained in:
+12
-16
@@ -1,3 +1,6 @@
|
||||
FROM ghcr.io/astral-sh/uv:0.12.5@sha256:e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1 AS uv
|
||||
|
||||
|
||||
FROM python:3.12.13-slim-bookworm AS base
|
||||
|
||||
|
||||
@@ -92,21 +95,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
jq \
|
||||
wget
|
||||
|
||||
# 安装 Python 构建依赖并创建虚拟环境
|
||||
# 按锁文件创建主程序虚拟环境
|
||||
WORKDIR /app
|
||||
COPY requirements.in requirements.in
|
||||
COPY scripts/uv-pip-compat.sh /usr/local/bin/uv-pip-compat
|
||||
RUN python3 -m venv ${VENV_PATH} \
|
||||
&& env UV_INSTALL_DIR=/usr/local/bin sh -c "$(curl -LsSf https://astral.sh/uv/install.sh)" \
|
||||
&& chmod +x /usr/local/bin/uv-pip-compat \
|
||||
&& ln -sf /usr/local/bin/uv ${VENV_PATH}/bin/uv \
|
||||
&& ln -sf /usr/local/bin/uv-pip-compat ${VENV_PATH}/bin/pip \
|
||||
&& ln -sf /usr/local/bin/uv-pip-compat ${VENV_PATH}/bin/pip3 \
|
||||
&& ln -sf /usr/local/bin/uv-pip-compat ${VENV_PATH}/bin/pip3.12 \
|
||||
&& ln -sf /usr/local/bin/uv-pip-compat ${VENV_PATH}/bin/pip-compile \
|
||||
&& ln -sf /usr/local/bin/uv-pip-compat ${VENV_PATH}/bin/pip-sync \
|
||||
&& pip-compile requirements.in -o requirements.txt \
|
||||
&& pip install -r requirements.txt
|
||||
COPY --from=uv /uv /usr/local/bin/uv
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN python3 -m venv --without-pip ${VENV_PATH} \
|
||||
&& UV_PROJECT_ENVIRONMENT=${VENV_PATH} uv sync \
|
||||
--locked \
|
||||
--no-dev \
|
||||
--no-install-project
|
||||
|
||||
# 下载准备代码
|
||||
FROM prepare_package AS prepare_code
|
||||
@@ -145,8 +142,7 @@ COPY --from=mwader/static-ffmpeg:8.1.1 /ffprobe /usr/local/bin/
|
||||
|
||||
# python 环境
|
||||
COPY --from=prepare_venv --chmod=777 ${VENV_PATH} ${VENV_PATH}
|
||||
COPY --from=prepare_venv /usr/local/bin/uv /usr/local/bin/uv
|
||||
COPY --from=prepare_venv /usr/local/bin/uv-pip-compat /usr/local/bin/uv-pip-compat
|
||||
COPY --from=uv /uv /usr/local/bin/uv
|
||||
|
||||
# 浏览器运行依赖
|
||||
RUN playwright install-deps chromium \
|
||||
|
||||
+51
-5
@@ -35,6 +35,7 @@ function is_truthy_value() {
|
||||
# 设置虚拟环境路径(兼容群晖等系统必须这样配置)
|
||||
VENV_PATH="${VENV_PATH:-/opt/venv}"
|
||||
export PATH="${VENV_PATH}/bin:$PATH"
|
||||
UV_BIN="${UV_BIN:-/usr/local/bin/uv}"
|
||||
|
||||
# 校正设置目录
|
||||
CONFIG_DIR="${CONFIG_DIR:-/config}"
|
||||
@@ -42,9 +43,8 @@ CONFIG_DIR="${CONFIG_DIR:-/config}"
|
||||
function apply_package_cache_env() {
|
||||
PACKAGE_CACHE_ROOT="${PACKAGE_CACHE_ROOT:-${CONFIG_DIR}/.cache}"
|
||||
export PACKAGE_CACHE_ROOT
|
||||
export PIP_CACHE_DIR="${PIP_CACHE_DIR:-${PACKAGE_CACHE_ROOT}/pip}"
|
||||
export UV_CACHE_DIR="${UV_CACHE_DIR:-${PACKAGE_CACHE_ROOT}/uv}"
|
||||
mkdir -p "${PIP_CACHE_DIR}" "${UV_CACHE_DIR}"
|
||||
mkdir -p "${UV_CACHE_DIR}"
|
||||
}
|
||||
|
||||
function run_package_command() {
|
||||
@@ -346,12 +346,20 @@ function ensure_backend_runtime_dependencies() {
|
||||
fi
|
||||
|
||||
WARN "→ 检测到后端核心依赖异常,开始尝试恢复主程序依赖..."
|
||||
local -a pip_cmd=("${VENV_PATH}/bin/pip" "install" "-r" "/app/requirements.txt")
|
||||
local -a uv_cmd=(
|
||||
"${UV_BIN}" sync
|
||||
--project /app
|
||||
--locked
|
||||
--no-dev
|
||||
--no-install-project
|
||||
--inexact
|
||||
)
|
||||
if [ -n "${PIP_PROXY}" ]; then
|
||||
pip_cmd+=("-i" "${PIP_PROXY}")
|
||||
uv_cmd+=(--default-index "${PIP_PROXY}")
|
||||
fi
|
||||
|
||||
if ! run_package_command "${pip_cmd[@]}" > /dev/stdout 2> /dev/stderr; then
|
||||
if ! run_package_command env "UV_PROJECT_ENVIRONMENT=${VENV_PATH}" \
|
||||
"${uv_cmd[@]}" > /dev/stdout 2> /dev/stderr; then
|
||||
ERROR "→ 自动恢复主程序依赖失败,后端无法启动。"
|
||||
diagnostic_keepalive 1
|
||||
fi
|
||||
@@ -463,6 +471,41 @@ function correct_config_permissions() {
|
||||
done < <(find "${CONFIG_DIR}" -mindepth 1 -maxdepth 1 -print0)
|
||||
}
|
||||
|
||||
function correct_package_cache_permissions() {
|
||||
local cache_dir="${UV_CACHE_DIR:-}"
|
||||
[ -n "${cache_dir}" ] || return 0
|
||||
if [[ "${cache_dir}" != /* ]]; then
|
||||
ERROR "→ UV_CACHE_DIR 必须是绝对目录:${cache_dir}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local resolved_cache
|
||||
local resolved_config
|
||||
resolved_cache="$(python3 -c 'import os, sys; print(os.path.normpath(sys.argv[1]))' "${cache_dir}")"
|
||||
resolved_config="$(python3 -c 'import os, sys; print(os.path.normpath(sys.argv[1]))' "${CONFIG_DIR}")"
|
||||
case "${resolved_cache}/" in
|
||||
"${resolved_config}/"*) return 0 ;;
|
||||
esac
|
||||
case "${resolved_cache}" in
|
||||
/|/app|/public|/opt|/usr|/etc|/var|/home|/root|"${VENV_PATH}")
|
||||
ERROR "→ UV_CACHE_DIR 不能使用受管根目录:${resolved_cache}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! mkdir -p -- "${resolved_cache}" \
|
||||
|| ! chown -R moviepilot:moviepilot "${resolved_cache}"; then
|
||||
ERROR "→ uv 缓存目录权限修复失败:${resolved_cache}"
|
||||
return 1
|
||||
fi
|
||||
if ! gosu moviepilot:moviepilot sh -c \
|
||||
'probe="$1/.moviepilot-write-test.$$"; : > "${probe}" && rm -f "${probe}"' \
|
||||
sh "${resolved_cache}"; then
|
||||
ERROR "→ uv 缓存目录不可写:${resolved_cache}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function chown_plugin_runtime_path() {
|
||||
local plugin_path="${1:-}"
|
||||
[ -n "${plugin_path}" ] || return 0
|
||||
@@ -492,6 +535,9 @@ function correct_file_permissions() {
|
||||
chown_plugin_runtime_path /app/app/plugins
|
||||
correct_home_permissions
|
||||
correct_config_permissions
|
||||
if ! correct_package_cache_permissions; then
|
||||
return 1
|
||||
fi
|
||||
chown -R moviepilot:moviepilot \
|
||||
/var/lib/nginx \
|
||||
/var/log/nginx
|
||||
|
||||
+56
-44
@@ -23,26 +23,27 @@ function WARN() {
|
||||
# 设置虚拟环境路径(兼容群晖等系统必须这样配置)
|
||||
VENV_PATH="${VENV_PATH:-/opt/venv}"
|
||||
export PATH="${VENV_PATH}/bin:$PATH"
|
||||
UV_BIN="${UV_BIN:-/usr/local/bin/uv}"
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-/config}"
|
||||
|
||||
function apply_package_cache_env() {
|
||||
PACKAGE_CACHE_ROOT="${PACKAGE_CACHE_ROOT:-${CONFIG_DIR}/.cache}"
|
||||
export PACKAGE_CACHE_ROOT
|
||||
export PIP_CACHE_DIR="${PIP_CACHE_DIR:-${PACKAGE_CACHE_ROOT}/pip}"
|
||||
export UV_CACHE_DIR="${UV_CACHE_DIR:-${PACKAGE_CACHE_ROOT}/uv}"
|
||||
mkdir -p "${PIP_CACHE_DIR}" "${UV_CACHE_DIR}"
|
||||
mkdir -p "${UV_CACHE_DIR}"
|
||||
}
|
||||
|
||||
apply_package_cache_env
|
||||
|
||||
PIP_ENV=()
|
||||
PACKAGE_ENV=()
|
||||
UV_OPTIONS=()
|
||||
MOVIEPILOT_UPDATE_RESULT="noop"
|
||||
|
||||
function set_package_proxy_env() {
|
||||
PIP_ENV=()
|
||||
PACKAGE_ENV=()
|
||||
if [[ -n "${PROXY_HOST}" ]]; then
|
||||
PIP_ENV=(
|
||||
PACKAGE_ENV=(
|
||||
"HTTP_PROXY=${PROXY_HOST}"
|
||||
"HTTPS_PROXY=${PROXY_HOST}"
|
||||
"http_proxy=${PROXY_HOST}"
|
||||
@@ -77,6 +78,34 @@ function download_and_unzip() {
|
||||
fi
|
||||
}
|
||||
|
||||
function sync_project_dependencies() {
|
||||
INFO "检测到依赖变化,正在更新虚拟环境..."
|
||||
configure_package_route || return 1
|
||||
INFO "依赖源:${PACKAGE_LOG}"
|
||||
local -a uv_cmd=(
|
||||
"${UV_BIN}" sync
|
||||
--project "${TMP_PATH}/App"
|
||||
--locked
|
||||
--inexact
|
||||
--no-dev
|
||||
--no-install-project
|
||||
--python "${VENV_PATH}/bin/python3"
|
||||
)
|
||||
uv_cmd+=("${UV_OPTIONS[@]}")
|
||||
if ! env "${PACKAGE_ENV[@]}" \
|
||||
"UV_PROJECT_ENVIRONMENT=${VENV_PATH}" \
|
||||
"UV_LINK_MODE=copy" "${uv_cmd[@]}"; then
|
||||
ERROR "依赖同步失败,当前程序依赖未完成更新"
|
||||
return 1
|
||||
fi
|
||||
INFO "依赖更新成功"
|
||||
}
|
||||
|
||||
function dependency_manifests_changed() {
|
||||
! cmp -s /app/pyproject.toml "${TMP_PATH}/App/pyproject.toml" \
|
||||
|| ! cmp -s /app/uv.lock "${TMP_PATH}/App/uv.lock"
|
||||
}
|
||||
|
||||
# 下载程序资源,$1: 后端版本路径
|
||||
function install_backend_and_download_resources() {
|
||||
# 更新后端程序
|
||||
@@ -88,28 +117,15 @@ function install_backend_and_download_resources() {
|
||||
|
||||
# 检查依赖是否有变化
|
||||
INFO "→ 检查依赖变化..."
|
||||
if [ -f "${TMP_PATH}/App/requirements.in" ]; then
|
||||
if ! cmp -s /app/requirements.in "${TMP_PATH}/App/requirements.in"; then
|
||||
INFO "检测到依赖变化,正在更新虚拟环境..."
|
||||
configure_pip_route
|
||||
INFO "PIP:${PIP_LOG}"
|
||||
local compiled_requirements="${TMP_PATH}/requirements.txt"
|
||||
if ! env "${PIP_ENV[@]}" ${VENV_PATH}/bin/pip-compile \
|
||||
"${TMP_PATH}/App/requirements.in" -o "${compiled_requirements}"; then
|
||||
ERROR "依赖编译失败,当前程序依赖未变更"
|
||||
return 1
|
||||
fi
|
||||
if ! env "${PIP_ENV[@]}" ${VENV_PATH}/bin/pip install ${PIP_OPTIONS} \
|
||||
-r "${compiled_requirements}"; then
|
||||
ERROR "依赖安装失败,当前程序依赖清单未变更"
|
||||
return 1
|
||||
fi
|
||||
INFO "依赖更新成功"
|
||||
if [ -f "${TMP_PATH}/App/pyproject.toml" ] && [ -f "${TMP_PATH}/App/uv.lock" ]; then
|
||||
if dependency_manifests_changed; then
|
||||
sync_project_dependencies || return 1
|
||||
else
|
||||
INFO "依赖无变化,跳过依赖更新"
|
||||
fi
|
||||
else
|
||||
WARN "未找到requirements.in文件,跳过依赖检查"
|
||||
ERROR "更新包缺少 pyproject.toml 或 uv.lock,拒绝替换当前程序"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 如果是"heads/v3.zip",则查找v3开头的最新版本号
|
||||
@@ -159,7 +175,6 @@ function install_backend_and_download_resources() {
|
||||
resource_source_dir=/app/app/application/site
|
||||
for legacy_resource_dir in /app/app/infrastructure /app/app/adapters/network /app/app/helper; do
|
||||
if [ ! -d "${resource_source_dir}" ] && [ -d "${legacy_resource_dir}" ]; then
|
||||
# 升级时允许读取历史目录,恢复目标始终使用 canonical 站点应用目录。
|
||||
resource_source_dir="${legacy_resource_dir}"
|
||||
fi
|
||||
done
|
||||
@@ -221,19 +236,16 @@ function install_backend_and_download_resources() {
|
||||
return 0
|
||||
}
|
||||
|
||||
function probe_pip_package() {
|
||||
function probe_package_index() {
|
||||
local probe_env=(
|
||||
"UV_NO_CACHE=1"
|
||||
"PIP_NO_CACHE_DIR=1"
|
||||
"UV_HTTP_TIMEOUT=5"
|
||||
"PIP_DEFAULT_TIMEOUT=5"
|
||||
"UV_HTTP_RETRIES=0"
|
||||
"PIP_RETRIES=0"
|
||||
)
|
||||
local package_index="${1:-}"
|
||||
local use_proxy="${2:-false}"
|
||||
local probe_dir
|
||||
local -a probe_args=(install)
|
||||
local -a probe_args=(pip install)
|
||||
|
||||
if [[ "${use_proxy}" = "true" ]]; then
|
||||
probe_env+=(
|
||||
@@ -247,7 +259,7 @@ function probe_pip_package() {
|
||||
# 包源探针必须使用独立目标目录,避免修改主程序与插件共享的虚拟环境。
|
||||
probe_args+=(--target "${probe_dir}" --no-deps)
|
||||
if [[ -n "${package_index}" ]]; then
|
||||
probe_args+=(-i "${package_index}")
|
||||
probe_args+=(--default-index "${package_index}")
|
||||
fi
|
||||
probe_args+=(pip-hello-world)
|
||||
|
||||
@@ -257,22 +269,22 @@ function probe_pip_package() {
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
timeout --kill-after=2s 10s env "${probe_env[@]}" \
|
||||
"${VENV_PATH}/bin/pip" "${probe_args[@]}" > /dev/null 2>&1
|
||||
"${UV_BIN}" "${probe_args[@]}" > /dev/null 2>&1
|
||||
)
|
||||
}
|
||||
|
||||
function test_connectivity_pip() {
|
||||
function test_connectivity_package() {
|
||||
case "$1" in
|
||||
0)
|
||||
if [[ -n "${PIP_PROXY}" ]]; then
|
||||
if [[ -n "${PROXY_HOST}" ]]; then
|
||||
probe_pip_package "${PIP_PROXY}" true
|
||||
probe_package_index "${PIP_PROXY}" true
|
||||
else
|
||||
probe_pip_package "${PIP_PROXY}" false
|
||||
probe_package_index "${PIP_PROXY}" false
|
||||
fi
|
||||
if [[ $? -eq 0 ]]; then
|
||||
PIP_OPTIONS="-i ${PIP_PROXY}"
|
||||
PIP_LOG="镜像代理模式"
|
||||
UV_OPTIONS=(--default-index "${PIP_PROXY}")
|
||||
PACKAGE_LOG="镜像代理模式"
|
||||
set_package_proxy_env
|
||||
return 0
|
||||
fi
|
||||
@@ -281,9 +293,9 @@ function test_connectivity_pip() {
|
||||
;;
|
||||
1)
|
||||
if [[ -n "${PROXY_HOST}" ]]; then
|
||||
if probe_pip_package "" true; then
|
||||
PIP_OPTIONS=""
|
||||
PIP_LOG="全局代理模式"
|
||||
if probe_package_index "" true; then
|
||||
UV_OPTIONS=()
|
||||
PACKAGE_LOG="全局代理模式"
|
||||
set_package_proxy_env
|
||||
return 0
|
||||
fi
|
||||
@@ -291,9 +303,9 @@ function test_connectivity_pip() {
|
||||
return 1
|
||||
;;
|
||||
2)
|
||||
PIP_ENV=()
|
||||
PIP_OPTIONS=""
|
||||
PIP_LOG="不使用代理"
|
||||
PACKAGE_ENV=()
|
||||
UV_OPTIONS=()
|
||||
PACKAGE_LOG="不使用代理"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
@@ -329,10 +341,10 @@ function test_connectivity_github() {
|
||||
esac
|
||||
}
|
||||
|
||||
function configure_pip_route() {
|
||||
function configure_package_route() {
|
||||
local retries=0
|
||||
while true; do
|
||||
if test_connectivity_pip "${retries}"; then
|
||||
if test_connectivity_package "${retries}"; then
|
||||
return 0
|
||||
fi
|
||||
retries=$((retries + 1))
|
||||
|
||||
Reference in New Issue
Block a user