Files
takana/scripts/drive-rebuild.py
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.

El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.

Y el hallazgo caro:  casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a  y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.

Además 14 rutas de módulo  en docs, que el barrido anterior no tocó
porque  no es frontera de palabra.
2026-09-09 19:28:48 +00:00

152 lines
6.8 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)
# DISK imagen de disco PARTICIONADA (GPT) a bootear como ROOT real (Etapa B3). Si está
# seteada, se ignora el initramfs: QEMU monta la imagen como disco virtio y el kernel
# arranca con `root=/dev/vda1 rw rdinit=/sbin/init` — sin initramfs ni switch_root (/ ya
# es un mount ext4 real, pivotable). La partición 1 es /, y el wrapper /sbin/init monta
# las particiones dedicadas /store (vda2) y /var/lib/hammer (vda3) antes del init real.
# La arma scripts/disk-image.sh. Requiere kernel con VIRTIO_BLK+EXT4 (recipes/linux.toml).
# 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")
DISK = os.environ.get("DISK", "") # Etapa B: si está, booteamos de disco real en vez de initramfs.
if DISK and not os.path.isabs(DISK):
DISK = os.path.join(ROOT, DISK)
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"
_needed = [(DISK, "imagen de disco")] if DISK else [(CPIO, "initramfs del builder")]
for p, what in _needed + [(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 []
# APPEND override: por defecto consola serie + initramfs. Útil para experimentar con params del kernel
# (p.ej. el frente kernel-from-source: el bzImage takana bootea pero bwrap falla en pivot_root porque /
# es el rootfs absoluto del namespace; `rootfstype=tmpfs` NO lo arregla — la causa es que / no tiene
# mount padre, no el tipo de fs). Override con APPEND="…".
# Boot de disco (Etapa B3) vs initramfs. En disco / ya es un mount ext4 real ⇒ rdinit=/sbin/init directo,
# sin el wrapper switch_root del path initramfs. La GPT tiene 3 particiones: vda1=/ , vda2=/store ,
# vda3=/var/lib/hammer; el kernel monta vda1 y el wrapper /sbin/init monta las otras dos.
if DISK:
APPEND = os.environ.get("APPEND", "console=ttyS0 root=/dev/vda1 rw rdinit=/sbin/init")
media = ["-drive", f"file={DISK},if=virtio,format=raw", "-append", APPEND]
else:
APPEND = os.environ.get("APPEND", "console=ttyS0 rdinit=/sbin/init")
media = ["-initrd", CPIO, "-append", APPEND]
QEMU = (["qemu-system-x86_64", "-m", MEM, "-no-reboot", "-nographic"] + accel + net +
["-kernel", KERNEL] + media)
print(f"==> kernel : {KERNEL}")
print(f"==> {'disco ' if DISK else 'initramfs '}: {DISK or 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)