#!/usr/bin/env bash # freeze_python_runtime.sh — genera /runtime/python/ embebido # para distribuir graph_explorer (u otra app) sin dependencia de WSL # ni del .venv del registry. # # Issue 0033 fase B. # # Uso: # tools/freeze_python_runtime.sh # = linux | windows # # Lee `python_runtime_deps` del frontmatter de /app.md # (lista YAML inline). Tambien acepta override via env var: # PY_DEPS="requests certifi urllib3" tools/freeze_python_runtime.sh ... # # Idempotente — calcula un hash de (PY_VERSION + deps + platform) y # lo guarda en runtime/.lock. Si coincide con el actual, no rehace. # # Salida (Windows): # /runtime/python/python.exe # /runtime/python/Lib/site-packages//... # # Salida (Linux): # /runtime/python/bin/python3 # /runtime/python/lib/... set -euo pipefail PY_VERSION="${PY_VERSION:-3.12.7}" APP_DIR="${1:?app_dir requerido}" PLATFORM="${2:?platform requerido (linux|windows)}" if [[ ! -d "$APP_DIR" ]]; then echo "ERROR: $APP_DIR no es un directorio" >&2 exit 1 fi RUNTIME_DIR="$APP_DIR/runtime/python" APP_MD="$APP_DIR/app.md" # ---------------------------------------------------------------------------- # Resolver dependencias: PY_DEPS (env) > frontmatter de app.md > vacio. # ---------------------------------------------------------------------------- deps_from_env="${PY_DEPS:-}" deps_from_md="" if [[ -z "$deps_from_env" && -f "$APP_MD" ]]; then # Parser ad-hoc del frontmatter (entre el primer y segundo `---`). # Busca lineas que empiezan por ` - ` despues de una linea # `python_runtime_deps:`. Suficiente para el formato YAML simple # que usamos en los app.md del registry. Si necesitamos algo mas # complejo (anidados, comentarios), portarlo a Python o usar yq. deps_from_md=$(awk ' /^---$/ { fm = !fm; next } !fm { next } /^python_runtime_deps:[[:space:]]*$/ { collecting = 1; next } collecting && /^[[:space:]]*-[[:space:]]+/ { sub(/^[[:space:]]*-[[:space:]]+/, "") sub(/[[:space:]]*#.*$/, "") gsub(/[\047"]/, "") print next } collecting && /^[^[:space:]-]/ { collecting = 0 } ' "$APP_MD" | xargs) fi DEPS="${deps_from_env:-$deps_from_md}" # ---------------------------------------------------------------------------- # Hash del estado: si coincide con runtime/.lock no rehacemos nada. # ---------------------------------------------------------------------------- state_hash=$(echo -n "$PY_VERSION|$PLATFORM|$DEPS" | sha256sum | cut -d' ' -f1) lock_file="$APP_DIR/runtime/.lock" if [[ -f "$lock_file" ]]; then cur=$(cat "$lock_file" 2>/dev/null || echo "") if [[ "$cur" == "$state_hash" ]]; then echo "freeze: sin cambios (.lock = $state_hash)" exit 0 fi fi # ---------------------------------------------------------------------------- # Limpieza y creacion. # ---------------------------------------------------------------------------- echo "freeze: $PLATFORM, deps=[$DEPS], py=$PY_VERSION" rm -rf "$RUNTIME_DIR" mkdir -p "$RUNTIME_DIR" if [[ "$PLATFORM" == "windows" ]]; then # Windows embedded distribution (zip oficial python.org). zip="python-${PY_VERSION}-embed-amd64.zip" cache="${TMPDIR:-/tmp}/$zip" if [[ ! -f "$cache" ]]; then echo "freeze: descargando $zip..." curl -sSL --fail \ "https://www.python.org/ftp/python/$PY_VERSION/$zip" \ -o "$cache.tmp" mv "$cache.tmp" "$cache" fi unzip -oq "$cache" -d "$RUNTIME_DIR" # Habilitar site-packages (el embedded viene con ._pth restrictivo). short=$(echo "$PY_VERSION" | awk -F. '{print $1$2}') pth="$RUNTIME_DIR/python${short}._pth" if [[ -f "$pth" ]]; then sed -i 's|^#import site|import site|' "$pth" fi # Instalar deps con pip "host" usando el embedded como target. if [[ -n "$DEPS" ]]; then echo "freeze: pip install $DEPS (target=$RUNTIME_DIR/Lib/site-packages)" # `--platform win_amd64 --only-binary=:all:` fuerza wheels # binarios para Windows aunque pip corra en Linux. python3 -m pip install --quiet \ --target "$RUNTIME_DIR/Lib/site-packages" \ --platform win_amd64 --only-binary=:all: \ --python-version "$PY_VERSION" \ $DEPS fi elif [[ "$PLATFORM" == "linux" ]]; then # En Linux preferimos `uv` si esta disponible — descarga un # Python standalone (de python-build-standalone) que no depende # del Python del sistema y empaqueta todo. Si no, fallback a # `python3 -m venv` (requiere python3-venv del sistema). if command -v uv >/dev/null 2>&1; then # uv mantiene un cache global de Pythons standalone (de # python-build-standalone) en ~/.local/share/uv/python/. Para # un runtime distribuible copiamos ese arbol completo (~82 MB) # y luego instalamos deps con su pip propio. Sin esto el venv # quedaria con symlinks al cache de uv que se rompen al # mover la carpeta a otra maquina. uv python install "$PY_VERSION" >/dev/null 2>&1 || true py_root=$(uv python find "$PY_VERSION" 2>/dev/null | xargs -I{} dirname {} | xargs dirname) if [[ -z "$py_root" || ! -d "$py_root" ]]; then echo "ERROR: no se localizo Python $PY_VERSION via uv" >&2 exit 4 fi cp -r "$py_root/." "$RUNTIME_DIR/" # python-build-standalone deja un marker EXTERNALLY-MANAGED # (PEP 668) que bloquea pip install. Es nuestro runtime, no # gestion del sistema — lo eliminamos. find "$RUNTIME_DIR" -name "EXTERNALLY-MANAGED" -delete 2>/dev/null || true py_bin="$RUNTIME_DIR/bin/python3" if [[ -n "$DEPS" ]]; then echo "freeze: pip install $DEPS" "$py_bin" -m pip install --quiet --no-warn-script-location $DEPS fi else python3 -m venv "$RUNTIME_DIR" --copies >/dev/null if [[ ! -x "$RUNTIME_DIR/bin/pip" ]]; then echo "ERROR: pip no disponible. Instala uv o python3-venv del sistema." >&2 exit 3 fi if [[ -n "$DEPS" ]]; then echo "freeze: pip install $DEPS" "$RUNTIME_DIR/bin/pip" install --quiet $DEPS fi fi else echo "ERROR: platform desconocida: $PLATFORM (esperado linux|windows)" >&2 exit 2 fi echo "$state_hash" > "$lock_file" echo "freeze: OK ($RUNTIME_DIR)"