sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
Etapa B (B1+B2): boot desde disco real ext4, sin switch_root
1#!/usr/bin/env python3
2# drive-rebuild.py — Driver NO-interactivo del rebuild in-rootfs (auto-alojamiento, SDD 11 §7;
3# runbook docs/runbooks/stage1-vm-boot.md §8c).
4#
5# Bootea el builder en QEMU, espera la shell de arje-zero (PID 1), manda `rebuild-stage1` y captura
6# todo hasta el veredicto (✓ REPRODUCIBLE / ✗ DIVERGENTE), el RC o el timeout. Imprime un resumen y
7# sale con 0 si REPRODUCIBLE, 1 si DIVERGENTE/otro.
8#
9# Variables de entorno (todas opcionales):
10# BUILDER_CPIO initramfs del builder (default work/builder.cpio.gz)
11# DISK imagen ext4 a bootear como ROOT real (Etapa B). Si está seteada, se ignora el
12# initramfs: QEMU monta la imagen como disco virtio (/dev/vda) y el kernel arranca con
13# `root=/dev/vda rw rdinit=/sbin/init` — sin initramfs ni switch_root (/ ya es un mount
14# ext4 real, pivotable). Requiere un kernel con VIRTIO_BLK+EXT4 (recipes/linux.toml).
15# KERNEL bzImage/vmlinuz a bootear (default /boot/vmlinuz-linux)
16# MEM RAM de la VM en MiB (default 6144)
17# CPU modelo de CPU TCG (default Broadwell; AVX2 sin KVM)
18# KVM 1 ⇒ -enable-kvm -cpu host si hay /dev/kvm (mucho más rápido; recomendado)
19# DEADLINE techo en segundos (default 14400 = 4h)
20# LOG fichero de captura (default work/rebuild-run.log)
21# NET 1 ⇒ NIC e1000 user-mode para el vendoring Rust (default 1)
22#
23# El rebuild Rust (hammerd/arje-zero) hace `cargo vendor` desde crates.io ⇒ necesita red: por eso la
24# NIC e1000 (qemu user-mode 10.0.2.0/24). Sin red, sólo sellan los componentes C.
25import os, pty, select, subprocess, sys, time, re
26
27ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
28
29
30def envpath(name, default):
31 v = os.environ.get(name, default)
32 return v if os.path.isabs(v) else os.path.join(ROOT, v)
33
34
35CPIO = envpath("BUILDER_CPIO", "work/builder.cpio.gz")
36DISK = os.environ.get("DISK", "") # Etapa B: si está, booteamos de disco real en vez de initramfs.
37if DISK and not os.path.isabs(DISK):
38 DISK = os.path.join(ROOT, DISK)
39KERNEL = os.environ.get("KERNEL", "/boot/vmlinuz-linux")
40LOG = envpath("LOG", "work/rebuild-run.log")
41MEM = os.environ.get("MEM", "6144")
42CPU = os.environ.get("CPU", "Broadwell")
43DEADLINE = int(os.environ.get("DEADLINE", "14400"))
44WANT_KVM = os.environ.get("KVM", "0") == "1"
45WANT_NET = os.environ.get("NET", "1") == "1"
46
47_needed = [(DISK, "imagen de disco")] if DISK else [(CPIO, "initramfs del builder")]
48for p, what in _needed + [(KERNEL, "kernel")]:
49 if not os.path.exists(p):
50 sys.exit(f"no existe {what}: {p}")
51
52accel = ["-cpu", CPU]
53if WANT_KVM and os.access("/dev/kvm", os.W_OK):
54 accel = ["-enable-kvm", "-cpu", "host"]
55 print("==> KVM activo (-cpu host)")
56else:
57 print(f"==> TCG (sin KVM); -cpu {CPU}. Lento — el rebuild Rust puede tardar.")
58
59net = ["-netdev", "user,id=n0", "-device", "e1000,netdev=n0"] if WANT_NET else []
60
61# APPEND override: por defecto consola serie + initramfs. Útil para experimentar con params del kernel
62# (p.ej. el frente kernel-from-source: el bzImage hammer bootea pero bwrap falla en pivot_root porque /
63# es el rootfs absoluto del namespace; `rootfstype=tmpfs` NO lo arregla — la causa es que / no tiene
64# mount padre, no el tipo de fs). Override con APPEND="…".
65# Boot de disco (Etapa B) vs initramfs. En disco / ya es un mount ext4 real ⇒ rdinit=/sbin/init directo
66# (arje-zero PID1), sin el wrapper switch_root del path initramfs.
67if DISK:
68 APPEND = os.environ.get("APPEND", "console=ttyS0 root=/dev/vda rw rdinit=/sbin/init")
69 media = ["-drive", f"file={DISK},if=virtio,format=raw", "-append", APPEND]
70else:
71 APPEND = os.environ.get("APPEND", "console=ttyS0 rdinit=/sbin/init")
72 media = ["-initrd", CPIO, "-append", APPEND]
73QEMU = (["qemu-system-x86_64", "-m", MEM, "-no-reboot", "-nographic"] + accel + net +
74 ["-kernel", KERNEL] + media)
75
76print(f"==> kernel : {KERNEL}")
77print(f"==> {'disco ' if DISK else 'initramfs '}: {DISK or CPIO}")
78print(f"==> mem : {MEM} MiB deadline: {DEADLINE}s net: {'sí' if WANT_NET else 'no'}")
79print(f"==> log : {LOG}")
80
81ansi = re.compile(rb'\x1b\[[0-9;?]*[a-zA-Z]')
82strip = lambda b: ansi.sub(b'', b)
83
84master, slave = pty.openpty()
85p = subprocess.Popen(QEMU, stdin=slave, stdout=slave, stderr=slave, close_fds=True)
86os.close(slave)
87log = open(LOG, "wb")
88buf = b""
89sent = False
90start = time.time()
91send = lambda s: os.write(master, s.encode())
92
93while True:
94 if time.time() - start > DEADLINE:
95 log.write(b"\n[DRIVER] DEADLINE\n")
96 break
97 r, _, _ = select.select([master], [], [], 10)
98 if r:
99 try:
100 data = os.read(master, 8192)
101 except OSError:
102 break
103 if not data:
104 break
105 log.write(data)
106 log.flush()
107 buf = strip(buf + data)[-20000:]
108 # Boot listo: arje-zero levantó hammerd (fanotify) o apareció la shell.
109 if not sent and (b"watcher fanotify activo" in buf or b"\n~ #" in buf or b"\r~ #" in buf):
110 time.sleep(3)
111 send("echo DRIVER_BEGIN; rebuild-stage1; echo DRIVER_RC=$?\n")
112 sent = True
113 log.write(b"\n[DRIVER] enviado rebuild-stage1\n")
114 log.flush()
115 # OJO: el comando tecleado ECHOA "DRIVER_RC=$?"; el marcador real es la SALIDA
116 # "DRIVER_RC=<digito>". Matchear sólo eso (o el veredicto) evita salir antes de tiempo.
117 if sent and (re.search(rb'DRIVER_RC=[0-9]', buf) or b"REPRODUCIBLE" in buf or b"DIVERGENTE" in buf):
118 time.sleep(2)
119 try:
120 log.write(os.read(master, 8192))
121 except Exception:
122 pass
123 log.write(b"\n[DRIVER] FIN (marcador)\n")
124 break
125 else:
126 if p.poll() is not None:
127 log.write(b"\n[DRIVER] qemu salio\n")
128 break
129
130try:
131 p.terminate()
132 time.sleep(2)
133 p.kill()
134except Exception:
135 pass
136log.close()
137
138# Veredicto a partir de lo capturado.
139blob = strip(open(LOG, "rb").read())
140ok = (b"REPRODUCIBLE" in blob) and (b"DIVERGENTE" not in blob)
141print(f"\nDRIVER done sent={sent} elapsed={int(time.time() - start)}s")
142if ok:
143 print("RESULTADO: ✓ REPRODUCIBLE — auto-alojamiento bit a bit verificado")
144elif b"DIVERGENTE" in blob:
145 print("RESULTADO: ✗ DIVERGENTE — hay no-determinismo que cazar (SDD 09 §2)")
146else:
147 print("RESULTADO: ? incompleto (timeout/boot/OOM) — revisá el log")
148sys.exit(0 if ok else 1)