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
+12 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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))