Permite distribuir graph_explorer.exe Windows sin dependencia de WSL
ni del .venv del registry. Tambien funciona en Linux como bundle
autocontenido portable.
Cambios:
1. tools/freeze_python_runtime.sh
- Linux: copia python-build-standalone (uv) ~87 MB,
elimina marker EXTERNALLY-MANAGED, instala wheels.
- Windows: descarga python-3.12.7-embed-amd64.zip oficial
(~12 MB), habilita site-packages, instala wheels via
pip install --target --platform win_amd64.
- Idempotente via runtime/.lock con SHA256 del estado.
- Lee python_runtime_deps del frontmatter de app.md.
2. jobs.cpp::cached_python_runtime() — resolver con cadena:
1. <exe_dir>/runtime/python/{python.exe|bin/python3} (embedded)
2. $FN_PYTHON (env)
3. <registry_root>/python/.venv/bin/python3 (registry_venv)
4. python3 del PATH (system)
Loggea procedencia al iniciar jobs_init.
3. POSIX run_subprocess: usa el runtime resuelto en lugar del
path hardcodeado.
4. Windows run_subprocess: ramifica por needs_wsl. Si embedded
o env, lanza Python Windows nativo via CreateProcessW
directamente (run_path tambien Windows nativo). Solo el
legacy registry_venv sigue por wsl.exe.
5. app.md: nuevos campos python_runtime: true y
python_runtime_deps: [requests, certifi, urllib3].
6. .gitignore extendido con runtime/, projects/, _vendored/,
.vendor.lock, binarios Go de enrichers.
Tests: 26/26 verde — 16 originales + 6 dispatcher fase A + 4
nuevos del resolver fase B (con/sin embed, FN_PYTHON, idempotencia
del freeze script).
Smoke E2E manual: runtime/python/bin/python3 ejecuta web_search
con cwd /tmp y registry_root pasado en ctx, sin tocar el .venv del
registry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
id, title, status, priority, created, depends_on, blocks
| id | title | status | priority | created | depends_on | blocks | ||
|---|---|---|---|---|---|---|---|---|
| 0033 | Dispatcher multi-lenguaje + runtime Python embebido | pending | high | 2026-05-02 |
|
|
Contexto
graph_explorer hoy depende de WSL: el .exe Windows invoca
python/.venv/bin/python3 que vive dentro de WSL. Consecuencias:
- Cada job paga arranque de WSL (~100-300 ms warm, varios segundos cold) + cruce de paths Linux/Windows.
- Bugs recurrentes de path mixto (
projects\default\operations.db→/home/.../projects\default\...). - La app no es portable: requiere WSL configurado, distro presente y
el
.venvdel registry montado. - El test del path absoluto (issue 0029-fix de hoy) es un parche; el problema de fondo es el cruce de OS.
Objetivo: que graph_explorer.exe se distribuya autocontenido y
soporte enrichers escritos en cualquiera de los lenguajes del
registry, sin pedirle nada al sistema huésped.
Diseño
1. Manifest extendido
id: fetch_webpage
name: "Fetch web page"
applies_to: [Url, Webpage]
emits: [Domain]
relations: [BELONGS_TO]
lang: python # NUEVO — go|python|bash (default: python para retrocompat)
exec: run # NUEVO — basename del binario/script (sin extension)
params:
- { name: timeout_s, type: int, default: 15 }
Reglas:
lang: python→<dir>/<exec>.py(defaultrun.py).lang: go→<dir>/<exec>(Linux) o<dir>/<exec>.exe(Windows).lang: bash→<dir>/<exec>.sh.- Default si falta
lang:python(retrocompat con los 5 enrichers existentes).
2. EnricherSpec (C++) extendido
// enrichers.h
struct EnricherSpec {
std::string id;
std::string name;
std::string description;
std::vector<std::string> applies_to;
std::string run_path; // path absoluto al ejecutable/script
std::string lang; // "python", "go", "bash" (NUEVO)
std::string exec_basename; // "run" por defecto (NUEVO)
};
enrichers_load() resuelve run_path segun lang:
python: busca<dir>/<exec_basename>.py.go: busca primero<dir>/<exec_basename>.exeen Windows o<dir>/<exec_basename>en Linux. Si no existe, log de warning y el enricher queda deshabilitado (no aparece en el menu).bash:<dir>/<exec_basename>.sh.
3. Dispatcher en jobs.cpp
run_subprocess() elige el argv segun spec.lang:
std::vector<std::string> argv;
if (spec.lang == "go") {
argv = { spec.run_path }; // ejecutable directo
} else if (spec.lang == "bash") {
argv = { "/bin/bash", spec.run_path };
} else {
// python (default)
argv = { python_runtime_path(), spec.run_path };
}
python_runtime_path() busca en este orden:
<exe_dir>/runtime/python/python(Linux) opython.exe(Windows)$FN_PYTHONenv var<registry_root>/python/.venv/bin/python3(legacy)python3del PATH (fallback)
Retorna "" si no encuentra ninguno; los enrichers Python quedan
deshabilitados con un warning visible en el panel.
4. Runtime Python embebido
4.1 Layout
<app_dir>/runtime/ # ignorado por git, generado por freeze
python/
python.exe # Windows
python3 # Linux
pythonXY.zip # stdlib comprimida
DLLs/ Lib/ ...
site-packages/
requests/ certifi/ charset_normalizer/ ...
En Windows usamos el embedded zip oficial de python.org
(python-3.12.X-embed-amd64.zip, ~12 MB). En Linux empaquetamos
python3 del sistema build con un venv limpio (python -m venv)
copiado a runtime/python/.
4.2 Script de freeze
tools/freeze_python_runtime.sh:
#!/usr/bin/env bash
# Genera runtime/ embebido para una app.
#
# Uso: freeze_python_runtime.sh <app_dir> <platform>
# <platform> = linux | windows
#
# Lee deps de <app_dir>/app.md frontmatter:
# python_runtime_deps: [requests, beautifulsoup4, ...]
set -euo pipefail
APP_DIR="${1:?app_dir requerido}"
PLATFORM="${2:?platform requerido (linux|windows)}"
PY_VERSION="3.12.7"
RUNTIME_DIR="$APP_DIR/runtime/python"
deps=$(yq '.python_runtime_deps[]' "$APP_DIR/app.md" 2>/dev/null || echo "")
mkdir -p "$RUNTIME_DIR"
if [[ "$PLATFORM" == "windows" ]]; then
zip="python-${PY_VERSION}-embed-amd64.zip"
[[ -f "/tmp/$zip" ]] || curl -sSL -o "/tmp/$zip" \
"https://www.python.org/ftp/python/$PY_VERSION/$zip"
unzip -oq "/tmp/$zip" -d "$RUNTIME_DIR"
# Habilitar site-packages en el embedded (viene deshabilitado).
pth="$RUNTIME_DIR/python$(echo $PY_VERSION | tr -d . | cut -c1-3)._pth"
sed -i 's|^#import site|import site|' "$pth"
# Instalar deps con pip de un Python "normal".
[[ -n "$deps" ]] && python -m pip install --target "$RUNTIME_DIR/Lib/site-packages" \
--platform win_amd64 --only-binary=:all: --python-version "$PY_VERSION" $deps
else
python3 -m venv "$RUNTIME_DIR" --copies --without-pip
"$RUNTIME_DIR/bin/python3" -m ensurepip --upgrade
[[ -n "$deps" ]] && "$RUNTIME_DIR/bin/python3" -m pip install $deps
fi
echo "Frozen runtime: $RUNTIME_DIR ($PLATFORM)"
4.3 Hook en app.md
Frontmatter de la app declara las dependencias:
---
name: graph_explorer
...
python_runtime: true
python_runtime_deps:
- requests
- certifi
- urllib3
---
fn index recoge estos campos en apps.python_runtime y
apps.python_runtime_deps (JSON). El build de Windows
(compile.sh//compile) llama al freeze script si
python_runtime: true.
5. UI: badge de lenguaje en el panel
En el menu contextual de enrichers y en el panel Jobs, mostrar al
lado del nombre un badge [Go] / [Py] / [Sh] para que el
usuario sepa cual va a ejecutar. Coste: 3-4 lineas en views.cpp
y jobs.cpp.
Plan de implementacion
Fase A — dispatcher multi-lang sin runtime embebido (COMPLETADA)
- ✅ Extender
EnricherSpecconlang,exec_basename,disabled,disabled_reason. - ✅
parse_manifestleelang(default"python") yexec(default"run"). - ✅
enrichers_loadresuelverun_pathsegun lang + plataforma viaresolve_run_path. Enrichers Go sin binario quedandisabledcon razon visible y se ocultan del menu. - ✅
run_subprocess(POSIX y Windows) ramificaargvpor lang. - ✅ Tests pytest del dispatcher: 6 tests nuevos en
tests/test_dispatcher_lang.py(default lang, bash wire protocol, regresion de enricher real, etc.). 22/22 verde incluyendo los 16 originales. - ✅ Los 5 enrichers existentes funcionan sin cambios — heredan
lang: pythonpor default.
Implementado en commit fce3f97 (rama issue/0033a-multilang-dispatcher).
Fase B — runtime Python embebido (COMPLETADA)
- ✅
tools/freeze_python_runtime.shcon dos backends:- Linux: copia
python-build-standalone(uv) ~87 MB, autocontenido. - Windows: descarga
python-3.X.Y-embed-amd64.zipoficial, habilita site-packages, instala wheels viapip install --targetcross-platform. Idempotente viaruntime/.lockcon SHA256 de (PY_VERSION, deps, platform). Leepython_runtime_depsdel frontmatter deapp.md(override: envPY_DEPS=...).
- Linux: copia
- ⏳ Hook al
/compileskill — pendiente, parte del issue 0033e. - ✅
cached_python_runtime()enjobs.cppcon cadena de fallback:<exe_dir>/runtime/python/{python.exe|bin/python3}→$FN_PYTHON→<registry_root>/python/.venv/bin/python3→python3del PATH. Loggea procedencia (kind=embedded|env| registry_venv|system) al iniciar jobs_init. - ✅ Tests:
tests/test_python_runtime_resolver.py— 4 tests verifican fallback con/sin embed, override via FN_PYTHON e idempotencia del freeze script.- Smoke E2E manual:
runtime/python/bin/python3ejecutaweb_searchen cwd arbitrario, sin pasar por el venv del registry.
- ✅
app.mdactualizado conpython_runtime: trueypython_runtime_deps: [requests, certifi, urllib3]. - ✅
.gitignorecreado: ignoraruntime/,_vendored/,.vendor.lock, binarios Go de enrichers.
Implementado en commits <hash-fase-B>. Para Windows nativo, el
spawn ahora bifurca: si el resolver es embedded/env/system,
lanza Python Windows nativo via CreateProcessW; si es registry_venv
(legacy), sigue el camino wsl.exe previo.
Fase C — UI hints (0.5 sesion)
- Badge
[Go]/[Py]/[Sh]en el menu de enrichers. - Panel Jobs muestra
langen la columna detalles. - Si el dispatcher detecta runtime Python ausente y hay enrichers Python registrados, banner persistente en el panel Echo: "runtime Python no encontrado — los enrichers Py estan deshabilitados".
Tests
tests/test_dispatcher.py— manifest dummy de cada lang lanzando un script trivial que devuelve{"ok": 1}. Verifica wire protocol.tests/test_python_runtime_path.py— fakea estructuras de carpeta y verifica que la cadena de fallback resuelve en el orden correcto.- Los 16 tests existentes deben seguir pasando (regresion zero).
Definicion de hecho
graph_explorer.exelanza enrichers Python sin WSL, usandoruntime/python/python.exeempaquetado junto al exe.- Los manifests aceptan
lang: goy un binario en el dir del enricher se ejecuta directamente. freeze_python_runtime.shes idempotente y se llama desde/compile.- 0 fallos por path mixto Linux/Windows en jobs (test que arranca el exe desde un cwd arbitrario en cada SO).
Riesgos y mitigaciones
| Riesgo | Mitigacion |
|---|---|
| Runtime Python suma 50-100 MB | Lazy/condicional via python_runtime: true. Apps 100% Go no pagan. |
| Embedded Python en Windows no soporta C extensions complejas | Limitar deps a wheels puras o con binarios precompilados (requests, urllib3, beautifulsoup4 OK; lxml/numpy quedan fuera del embedded — usarlas requiere venv full). Documentar lista soportada. |
| Mantener freeze script roto | Test en CI que reconstruye el runtime y lanza un enricher dummy. |
| Versionar runtime entre PCs | runtime/ en .gitignore; cada PC lo regenera con freeze_python_runtime.sh. Hash SHA256 del zip embed se cachea en runtime/.lock. |
| Devs sin paciencia para esperar el freeze | Cachear /tmp/python-embed-*.zip. Build incremental: skip freeze si runtime/.lock matches deps de app.md. |
Fuera de alcance
- Ports de los 5 enrichers a Go (eso es el issue 0034).
- Runtime Node.js / Bun para enrichers TypeScript (futuro, baja prioridad).
- Runtime Go para enrichers JIT (no aplica, Go se compila AOT).
- Distribucion del
.exepor instalador (msi/setup) — fuera. La app se sigue distribuyendo como carpeta zippeada al Desktop.