feat: bundle Python 3.13 for Ubuntu 26.04

This commit is contained in:
Haitao Pan 2026-06-15 15:44:59 +08:00
parent 1a106d9f22
commit 73762d498f
3 changed files with 76 additions and 2 deletions

View File

@ -185,6 +185,9 @@ jobs:
grep -Eq 'packages/playwright-browsers/.*/chrome-linux(64)?/chrome$' contents.txt
grep -q 'metadata/apt/browser-deb-packages.txt' contents.txt
fi
if [[ "${{ matrix.distro }}:${{ matrix.version }}" == "ubuntu:26.04" ]]; then
grep -Eq 'packages/python/.*/bin/python3.13$' contents.txt
fi
publish-release:
needs: test-offline-package

View File

@ -10,6 +10,8 @@ NPM_RUNTIME_CACHE_DIR="${AI_WORKSPACE_NPM_CACHE_DIR:-/var/cache/ai-workspace/npm
PIP_WHEEL_DIR="${ROOT}/packages/pip"
PLAYWRIGHT_BROWSER_DIR="${ROOT}/packages/playwright-browsers"
PLAYWRIGHT_BROWSER_INSTALL_DIR="${AI_WORKSPACE_PLAYWRIGHT_BROWSER_DIR:-/opt/ai-workspace/playwright-browsers}"
PORTABLE_PYTHON_DIR="${ROOT}/packages/python"
PORTABLE_PYTHON_INSTALL_DIR="${AI_WORKSPACE_PORTABLE_PYTHON_DIR:-/opt/ai-workspace/python}"
STATE_DIR="${AI_WORKSPACE_OFFLINE_STATE_DIR:-/var/lib/ai-workspace/offline}"
AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT="${AI_WORKSPACE_DEPLOYMENT_LOCK_TIMEOUT:-1800}"
AI_WORKSPACE_APT_LOCK_TIMEOUT="${AI_WORKSPACE_APT_LOCK_TIMEOUT:-900}"
@ -256,6 +258,46 @@ install_bundled_playwright_browser() {
ln -sfn "${browser_binary}" /usr/local/bin/chromium
}
install_bundled_python_runtime() {
local packaged_python installed_python marker seed_checksum
packaged_python="$(
find -L "${PORTABLE_PYTHON_DIR}" -type f -path '*/bin/python3.13' -perm /111 -print -quit 2>/dev/null || true
)"
if [ -z "${packaged_python}" ]; then
return
fi
seed_checksum="$(sha256sum "${ROOT}/metadata/manifest.json" | awk '{print $1}')"
marker="${PORTABLE_PYTHON_INSTALL_DIR}/.ai-workspace-seed-sha256"
if [ "$(cat "${marker}" 2>/dev/null || true)" != "${seed_checksum}" ]; then
info "Installing bundled portable Python runtime"
rm -rf "${PORTABLE_PYTHON_INSTALL_DIR}"
mkdir -p "${PORTABLE_PYTHON_INSTALL_DIR}"
cp -a "${PORTABLE_PYTHON_DIR}/." "${PORTABLE_PYTHON_INSTALL_DIR}/"
printf '%s\n' "${seed_checksum}" > "${marker}"
else
info "Reusing bundled portable Python runtime"
fi
chown -R root:root "${PORTABLE_PYTHON_INSTALL_DIR}"
chmod -R a+rX "${PORTABLE_PYTHON_INSTALL_DIR}"
installed_python="$(
find -L "${PORTABLE_PYTHON_INSTALL_DIR}" -type f -path '*/bin/python3.13' -perm /111 -print -quit
)"
if [ -z "${installed_python}" ]; then
echo "Installed portable Python executable is missing." >&2
exit 1
fi
ln -sfn "${installed_python}" /usr/local/bin/ai-workspace-python
cat > /usr/local/bin/ai-workspace-pip <<'EOF'
#!/usr/bin/env bash
exec /usr/local/bin/ai-workspace-python -m pip "$@"
EOF
chmod 0755 /usr/local/bin/ai-workspace-pip
export LITELLM_PYTHON_EXECUTABLE=/usr/local/bin/ai-workspace-python
export LITELLM_PIP_EXECUTABLE=/usr/local/bin/ai-workspace-pip
}
load_container_images() {
if [ ! -d "${IMAGE_DIR}" ] || ! compgen -G "${IMAGE_DIR}/*.tar" >/dev/null; then
return
@ -372,6 +414,7 @@ main() {
configure_local_git_sources
install_bundled_binaries
install_bundled_playwright_browser
install_bundled_python_runtime
load_container_images
configure_language_package_caches
run_bootstrap

View File

@ -29,6 +29,8 @@ POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:17.7}"
OPENCLAW_VERSION="${OPENCLAW_VERSION:-2026.6.6}"
PLAYWRIGHT_VERSION="${PLAYWRIGHT_VERSION:-1.60.0}"
GOOGLE_CHROME_VERSION="${GOOGLE_CHROME_VERSION:-149.0.7827.114-1}"
UV_VERSION="${UV_VERSION:-0.11.21}"
PORTABLE_PYTHON_VERSION="${PORTABLE_PYTHON_VERSION:-3.13.14}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
@ -81,8 +83,9 @@ download_apt_packages() {
local apt_dir="${WORKDIR}/packages/apt"
local apt_lists="${WORKDIR}/metadata/apt"
local wheel_dir="${WORKDIR}/packages/pip"
local python_dir="${WORKDIR}/packages/python"
mkdir -p "${apt_dir}" "${apt_lists}" "${wheel_dir}"
mkdir -p "${apt_dir}" "${apt_lists}" "${wheel_dir}" "${python_dir}"
docker run --rm --platform "${platform}" \
-e DISTRO_ID="${DISTRO_ID}" \
@ -92,9 +95,12 @@ download_apt_packages() {
-e NODEJS_24_VERSION="${NODEJS_24_VERSION}" \
-e GOOGLE_CHROME_VERSION="${GOOGLE_CHROME_VERSION}" \
-e LITELLM_DEBIAN_11_VERSION="${LITELLM_DEBIAN_11_VERSION}" \
-e UV_VERSION="${UV_VERSION}" \
-e PORTABLE_PYTHON_VERSION="${PORTABLE_PYTHON_VERSION}" \
-v "${apt_dir}:/offline-apt" \
-v "${apt_lists}:/offline-meta" \
-v "${wheel_dir}:/offline-pip" \
-v "${python_dir}:/offline-python" \
-v "${WORKDIR}/repos/litellm:/litellm-src:ro" \
"${image}" \
bash -lc "$(cat <<'CONTAINER'
@ -229,7 +235,27 @@ find . -maxdepth 1 -name '*.deb' -printf '%f\n' | sort > /offline-meta/deb-files
apt-get install -y --no-install-recommends \
build-essential git libpq-dev python3 python3-dev python3-pip python3-venv
python3 -m venv /tmp/ai-workspace-wheel-builder
python_bin=python3
if [ "${DISTRO_ID}:${DISTRO_VERSION}" = "ubuntu:26.04" ]; then
case "$(dpkg --print-architecture)" in
amd64) uv_arch=x86_64 ;;
arm64) uv_arch=aarch64 ;;
*) echo "Unsupported uv architecture: $(dpkg --print-architecture)" >&2; exit 1 ;;
esac
curl -fsSL \
"https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${uv_arch}-unknown-linux-gnu.tar.gz" \
-o /tmp/uv.tar.gz
tar -xzf /tmp/uv.tar.gz -C /tmp
install -m 0755 "$(find /tmp -type f -path '*/uv-*/uv' -print -quit)" /usr/local/bin/uv
uv python install "${PORTABLE_PYTHON_VERSION}" --install-dir /offline-python --no-bin
python_bin="$(find -L /offline-python -type f -path '*/bin/python3.13' -perm /111 -print -quit)"
if [ -z "${python_bin}" ]; then
echo "Portable Python ${PORTABLE_PYTHON_VERSION} was not installed." >&2
exit 1
fi
"${python_bin}" -m ensurepip --upgrade
fi
"${python_bin}" -m venv /tmp/ai-workspace-wheel-builder
/tmp/ai-workspace-wheel-builder/bin/pip install --upgrade pip setuptools wheel
litellm_package_spec="/litellm-src[proxy]"
if [ "${DISTRO_ID}:${DISTRO_VERSION}" = "debian:11" ]; then
@ -383,6 +409,8 @@ EOF
"playwright": "${PLAYWRIGHT_VERSION}",
"googleChrome": "${GOOGLE_CHROME_VERSION}",
"litellmDebian11": "${LITELLM_DEBIAN_11_VERSION}",
"uv": "${UV_VERSION}",
"portablePython": "${PORTABLE_PYTHON_VERSION}",
"postgresImage": "${POSTGRES_IMAGE}"
}
}