Files
takana/scripts/drive-rebuild.py
T
SergioandClaude Opus 4.8 7025f93bcd scripts: tarea selfhost-verify end-to-end (correr el 4/4 in-VM en KVM)
El veredicto pleno 4/4 in-VM pide KVM + RAM holgada; el host de dev (7.6 GB, sin KVM)
colgó por presión de RAM bajo TCG a los ~17 min del make de musl. Esta tarea traslada
la verificación a una máquina capaz (laptop) en un solo comando.

- scripts/selfhost-verify.sh: pipeline completo — stage0 → stage1 baseline → stage2
  (ancla la ref of_tree) → inyecta overlay.ko/e1000.ko del kernel local al toolchain →
  ensambla el builder con la ref embebida → empaqueta (cpio --owner=root:root) →
  bootea + driver no-interactivo → veredicto. KVM auto-detect; PRESEED=hammerd para el
  camino barato (preseed C+arje, sólo hammerd in-VM). Cross-check entre máquinas:
  compara su of_tree(stage1) contra EXPECT_REF (198f209f… conocida-buena del dev).
- scripts/drive-rebuild.py: driver no-interactivo parametrizado (env: BUILDER_CPIO,
  KERNEL, MEM, CPU, KVM, NET, DEADLINE, LOG). Bootea, espera la shell de arje-zero,
  manda rebuild-stage1 y sale 0 si REPRODUCIBLE / 1 si DIVERGENTE. (Versión de repo del
  driver ad-hoc que vivía en work/.)
- scripts/boot-builder-vm.sh: añade la NIC e1000 (NET=1 por defecto) — el vendoring Rust
  necesita red.
- runbook §8c: documenta la tarea y el muro de RAM del host de dev.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:57:07 +00:00

129 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
# drive-rebuild.py — Driver NO-interactivo del rebuild in-rootfs (auto-alojamiento, SDD 11 §7;
# runbook docs/runbooks/stage1-vm-boot.md §8c).
#
# Bootea el builder en QEMU, espera la shell de arje-zero (PID 1), manda `rebuild-stage1` y captura
# todo hasta el veredicto (✓ REPRODUCIBLE / ✗ DIVERGENTE), el RC o el timeout. Imprime un resumen y
# sale con 0 si REPRODUCIBLE, 1 si DIVERGENTE/otro.
#
# Variables de entorno (todas opcionales):
# BUILDER_CPIO initramfs del builder (default work/builder.cpio.gz)
# KERNEL bzImage/vmlinuz a bootear (default /boot/vmlinuz-linux)
# MEM RAM de la VM en MiB (default 6144)
# CPU modelo de CPU TCG (default Broadwell; AVX2 sin KVM)
# KVM 1 ⇒ -enable-kvm -cpu host si hay /dev/kvm (mucho más rápido; recomendado)
# DEADLINE techo en segundos (default 14400 = 4h)
# LOG fichero de captura (default work/rebuild-run.log)
# NET 1 ⇒ NIC e1000 user-mode para el vendoring Rust (default 1)
#
# El rebuild Rust (hammerd/arje-zero) hace `cargo vendor` desde crates.io ⇒ necesita red: por eso la
# NIC e1000 (qemu user-mode 10.0.2.0/24). Sin red, sólo sellan los componentes C.
import os, pty, select, subprocess, sys, time, re
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
def envpath(name, default):
v = os.environ.get(name, default)
return v if os.path.isabs(v) else os.path.join(ROOT, v)
CPIO = envpath("BUILDER_CPIO", "work/builder.cpio.gz")
KERNEL = os.environ.get("KERNEL", "/boot/vmlinuz-linux")
LOG = envpath("LOG", "work/rebuild-run.log")
MEM = os.environ.get("MEM", "6144")
CPU = os.environ.get("CPU", "Broadwell")
DEADLINE = int(os.environ.get("DEADLINE", "14400"))
WANT_KVM = os.environ.get("KVM", "0") == "1"
WANT_NET = os.environ.get("NET", "1") == "1"
for p, what in [(CPIO, "initramfs del builder"), (KERNEL, "kernel")]:
if not os.path.exists(p):
sys.exit(f"no existe {what}: {p}")
accel = ["-cpu", CPU]
if WANT_KVM and os.access("/dev/kvm", os.W_OK):
accel = ["-enable-kvm", "-cpu", "host"]
print("==> KVM activo (-cpu host)")
else:
print(f"==> TCG (sin KVM); -cpu {CPU}. Lento — el rebuild Rust puede tardar.")
net = ["-netdev", "user,id=n0", "-device", "e1000,netdev=n0"] if WANT_NET else []
QEMU = (["qemu-system-x86_64", "-m", MEM, "-no-reboot", "-nographic"] + accel + net +
["-kernel", KERNEL, "-initrd", CPIO, "-append", "console=ttyS0 rdinit=/sbin/init"])
print(f"==> kernel : {KERNEL}")
print(f"==> initramfs : {CPIO}")
print(f"==> mem : {MEM} MiB deadline: {DEADLINE}s net: {'sí' if WANT_NET else 'no'}")
print(f"==> log : {LOG}")
ansi = re.compile(rb'\x1b\[[0-9;?]*[a-zA-Z]')
strip = lambda b: ansi.sub(b'', b)
master, slave = pty.openpty()
p = subprocess.Popen(QEMU, stdin=slave, stdout=slave, stderr=slave, close_fds=True)
os.close(slave)
log = open(LOG, "wb")
buf = b""
sent = False
start = time.time()
send = lambda s: os.write(master, s.encode())
while True:
if time.time() - start > DEADLINE:
log.write(b"\n[DRIVER] DEADLINE\n")
break
r, _, _ = select.select([master], [], [], 10)
if r:
try:
data = os.read(master, 8192)
except OSError:
break
if not data:
break
log.write(data)
log.flush()
buf = strip(buf + data)[-20000:]
# Boot listo: arje-zero levantó hammerd (fanotify) o apareció la shell.
if not sent and (b"watcher fanotify activo" in buf or b"\n~ #" in buf or b"\r~ #" in buf):
time.sleep(3)
send("echo DRIVER_BEGIN; rebuild-stage1; echo DRIVER_RC=$?\n")
sent = True
log.write(b"\n[DRIVER] enviado rebuild-stage1\n")
log.flush()
# OJO: el comando tecleado ECHOA "DRIVER_RC=$?"; el marcador real es la SALIDA
# "DRIVER_RC=<digito>". Matchear sólo eso (o el veredicto) evita salir antes de tiempo.
if sent and (re.search(rb'DRIVER_RC=[0-9]', buf) or b"REPRODUCIBLE" in buf or b"DIVERGENTE" in buf):
time.sleep(2)
try:
log.write(os.read(master, 8192))
except Exception:
pass
log.write(b"\n[DRIVER] FIN (marcador)\n")
break
else:
if p.poll() is not None:
log.write(b"\n[DRIVER] qemu salio\n")
break
try:
p.terminate()
time.sleep(2)
p.kill()
except Exception:
pass
log.close()
# Veredicto a partir de lo capturado.
blob = strip(open(LOG, "rb").read())
ok = (b"REPRODUCIBLE" in blob) and (b"DIVERGENTE" not in blob)
print(f"\nDRIVER done sent={sent} elapsed={int(time.time() - start)}s")
if ok:
print("RESULTADO: ✓ REPRODUCIBLE — auto-alojamiento bit a bit verificado")
elif b"DIVERGENTE" in blob:
print("RESULTADO: ✗ DIVERGENTE — hay no-determinismo que cazar (SDD 09 §2)")
else:
print("RESULTADO: ? incompleto (timeout/boot/OOM) — revisá el log")
sys.exit(0 if ok else 1)