Bootstrap 2.14.0 roto — PyInstaller descartó app_main.py
Resumen
El bootstrap 2.14.0 de app_main.py fue descartado por PyInstaller durante el build cuando se pasaba como --add-data .py suelto en agent_modules/. Causa: PyInstaller analiza módulos y descarta los que colisionan con su análisis interno → con app_main.py suelto, el fallback embebido se rompía.
Síntoma: 2 builds fallidos (07-07) con error "app_main.py no encontrado en agent_modules" tras un swap limpio.
Duración: ~3 horas (desde build hasta root-cause).
Timeline
| Hora | Evento |
|---|---|
| 07-07 13:00 | Build 2.14.0 falló |
| 07-07 14:00 | Segundo intento falló con mismo error |
| 07-07 15:00 | Root-cause: PyInstaller descartó app_main.py |
| 07-07 16:30 | Fix: app_main.py embebido como ZIP, no suelto |
| 07-07 17:00 | 2.14.1 build OK, paquete validado |
| 07-07 18:00 | E2E en máquina de Edu: update automático exitoso |
Causa raíz
Antes (2.14.0, bug)
scripts/agent/build_app_package.pygenerabaapp_main.pysuelto enagent_modules/.PyInstaller --add-data agent_modules/app_main.py:agent_modules→ ingresaba como instrucción “añade este .py”.- Durante el análisis de modelos, PyInstaller detectaba que
app_maincolisionaba con su propio análisis y descartaba silenciosamente el archivo. - Bootstrap en runtime:
_import_app_main()→ModuleNotFoundError.
Ahora (2.14.1, fix)
build_agent.batgeneraapp-VER.zipANTES de PyInstaller, conteniendo todo el paquete.PyInstaller --add-data app-VER.zip:root→ incrusta el ZIP como dato binario (no código Python).- PyInstaller no lo analiza como módulo → no lo descarta.
- Bootstrap:
_embedded_source()descomprime el ZIP, valida hash, ejecuta.
Impacto
- Desarrollo: El Lanzador Estable F1 (bootstrap + paquete) no fue entregable hasta resolver esto.
- Producción: Ninguno (2.14.0 nunca se distribuyó, cancelado tras 2 fallos).
- Flota legacy (2.13.6): Sin impacto, seguía con auto-update por
.exelegacy (que aún está).
Fix e implementación
build_agent.bat (orquestación)
REM 1. Generar ZIP embebido
python scripts/agent/build_app_package.py --output app-%VERSION%.zip
REM 2. Compilar bootstrap con ZIP embebido
pyinstaller --add-data "app-%VERSION%.zip:root" ...
scripts/agent/build_app_package.py (generador ZIP)
- Genera ZIP con código completo + hash SHA-256.
__init__.pycarga comoagent_bootstrap(módulo).
bootstrap._embedded_source() (extractor)
def _embedded_source() -> bytes:
"""Extrae y valida ZIP embebido en fallback."""
if not _EMBEDDED_ZIP:
raise RuntimeError("Código embebido no disponible")
# Validar SHA-256 del ZIP
actual_hash = hashlib.sha256(_EMBEDDED_ZIP).hexdigest()
expected_hash = _EMBEDDED_HASH # Generado en build
if actual_hash != expected_hash:
raise RuntimeError(f"Hash mismatch: {actual_hash} != {expected_hash}")
# Descomprimir
return zlib.decompress(_EMBEDDED_ZIP)
Testing
-
3 tests nuevos en
tests/agent/test_agent_launcher.py:test_embedded_source_integrity: ZIP válido y válida el hash.test_embedded_source_corrupted: ZIP corrupto → error.test_selftest_with_embedded: self-test fuerzaCREARACK_FORCE_EMBEDDED=1→ prueba el camino embebido real (nocurrent.txt).
-
Suite: 26/26 tests passing.
Lecciones aprendidas
- PyInstaller + análisis de módulos: Ten cuidado con archivos
.pyque PyInstaller pudiera confundir. Prueba con--analyzesi dudas. - Embeber binarios vs. código: ZIP/datos binarios son más seguros que
.pysueltos dentro del análisis de PyInstaller. - Self-test en CI: El antiguo self-test pasaba porque usaba
current.txt(versión anterior intacta) → enmascara el bug. Ahora fuerza embebido con env var.
Resoluciones
| Aspecto | Resolución |
|---|---|
| Código embebido | ZIP con hash validado |
| Self-test | Fuerza embebido con CREARACK_FORCE_EMBEDDED=1 |
| Orquestación build | build_agent.bat + build_app_package.py |
| Validación en runtime | _embedded_source() con SHA-256 + descomprensión |
Véase también
- [[feature—agent-lanzador—bootstrap-zip]]
- [[feature—agent-lanzador—paquete-firmado-ed25519]]
- [[concept—agent-lanzador—cadena-fallback]]
- [[concept—agent-lanzador—plan-f3]]