Etapa E1: imagen de disco auto-booteable (GRUB BIOS), sin -kernel
Primer entregable de release engineering (SDD 13 nuevo): scripts/install-image.sh produce una imagen GPT que se BOOTEA SOLA en QEMU (`-drive file=img`, sin -kernel ni firmware extra) — SeaBIOS → MBR → GRUB → kernel del propio disco → arje-zero PID 1. Capaz de arrancar en hardware real. Layout: vda1=BIOS boot (ef02), vda2=/ (con /boot/bzImage + /boot/grub), vda3=/store, vda4=/var/lib/hammer. Reusa el particionado sin-root de B3 (sfdisk + mke2fs -d bajo unshare -r + dd conv=sparse) y el wrapper /sbin/init. GRUB instalado SIN root ni loop: grub-bios-setup sondea el disco físico del host (/dev/nvme…, 660 root:disk) para adivinar el root device del dir -d y falla sin privilegios. Lo reemplaza un patch binario determinista sobre la ABI estable de GRUB i386-pc: grub-mkimage arma core.img; un script Python escribe core.img en la BIOS boot partition y parchea los punteros (boot.img off 0x5c kernel_sector → LBA de core.img; core.img off 0x1F4 blocklist.start → resto de core.img), con asserts de los valores por defecto (1 y 2) para fallar ruidoso si la ABI cambia. boot.img va al MBR sin pisar la GPT protective (sólo 440 B). Verificado in-VM (KVM, sin -kernel): SeaBIOS → GRUB 2.14 → Linux 6.16.12 → vda1..4 detectadas, vda2 root + vda3/vda4 montadas por el wrapper (0 errores) → arje-zero PID 1 + hammerd (store=/store journal=/var/lib/hammer/journal). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# SDD 13 — Release engineering (Etapa E): imagen auto-booteable, instalador, mirror, upgrades
|
||||
|
||||
> Estado: **EN CURSO**. Etapa E del roadmap (`docs/10-roadmap.md`, memoria distro-completion-roadmap).
|
||||
> Es trabajo nuevo: las Etapas A–D dejaron un sistema que (A) se orquesta, (B) bootea de disco
|
||||
> particionado real, (C) tiene userland Rust-nativo y (D) atesta confianza. Falta convertir eso en algo
|
||||
> que un tercero **instale y arranque sin el host de dev**: una imagen que bootea sola (sin
|
||||
> `qemu -kernel`), un instalador a disco físico, un mirror del store y un camino de upgrade.
|
||||
|
||||
## 1. El gap concreto que abre la Etapa E
|
||||
|
||||
Hasta B3 la imagen de disco (`scripts/disk-image.sh`, GPT con `/`, `/store`, `/var/lib/hammer`
|
||||
dedicados) **no se auto-bootea**: el `drive-rebuild.py` la arranca con `qemu -kernel <bzImage>
|
||||
-append root=/dev/vdaN` — el kernel lo carga QEMU, no la imagen. Eso sirve para verificar el
|
||||
auto-alojamiento, pero un disco así no arranca en hardware real ni en una VM "normal": le falta un
|
||||
**bootloader** en el propio disco.
|
||||
|
||||
## 2. Decisión de bootloader: GRUB BIOS (i386-pc) ahora, UEFI después
|
||||
|
||||
- **GRUB BIOS (i386-pc) — ELEGIDO para el primer corte.** QEMU trae SeaBIOS built-in ⇒ un disco con
|
||||
GRUB en el MBR + una *BIOS boot partition* (GPT type `ef02`) bootea con `qemu-system-x86_64 -drive
|
||||
file=img` **sin firmware extra ni `-kernel`**. Se instala **en userspace, sin root ni loop**:
|
||||
`grub-mkimage` arma `core.img` y `grub-bios-setup` escribe `boot.img`→MBR + `core.img`→partición
|
||||
BIOS sobre el *fichero* imagen (es I/O de bloques, no necesita montar). Los módulos de GRUB y el
|
||||
kernel van en `/boot` de la partición root, que `mke2fs -d` puebla desde un directorio.
|
||||
- **UEFI (EFI-stub + ESP FAT) — diferido.** El kernel ya trae `CONFIG_EFI_STUB=y`, pero (a) el host de
|
||||
dev no tiene firmware OVMF usable para verificar el arranque, y (b) poblar un ESP FAT sin root exige
|
||||
`mtools` (ausente). Camino futuro cuando haya OVMF + mtools (o se construyan): ESP con
|
||||
`EFI/BOOT/BOOTX64.EFI` = el bzImage (EFI-stub), cmdline por `loader.conf`/UKI.
|
||||
|
||||
## 3. Layout de la imagen instalable (GPT, BIOS)
|
||||
|
||||
```
|
||||
/dev/vda1 BIOS boot (ef02, ~2 MiB, sin fs) core.img de GRUB
|
||||
/dev/vda2 / ext4 [hammer-root] + /boot/bzImage + /boot/grub
|
||||
/dev/vda3 /store ext4 [hammer-store] artefactos CAS (inmutable)
|
||||
/dev/vda4 /var/lib/hammer ext4 [hammer-state] estado mutable (journal, overlays)
|
||||
```
|
||||
|
||||
Cadena de arranque: SeaBIOS → MBR (boot.img) → core.img (BIOS boot part) → lee `(hd0,gpt2)/boot/grub/
|
||||
grub.cfg` → `linux /boot/bzImage root=/dev/vda2 rdinit=/sbin/init` → wrapper `/sbin/init` monta
|
||||
vda3/vda4 → `exec /usr/bin/arje-zero` (PID 1). El wrapper es el mismo puente de B3 (arje todavía no lee
|
||||
fstab/cards de montaje por sí mismo).
|
||||
|
||||
`scripts/install-image.sh` produce esta imagen. Reusa la técnica de particionado sin-root de B3
|
||||
(sfdisk + `mke2fs -d` bajo `unshare -r` + `dd conv=sparse,notrunc`) y añade la BIOS boot partition, el
|
||||
kernel en `/boot`, los módulos GRUB en `/boot/grub/i386-pc` y los pasos `grub-mkimage`/`grub-bios-setup`.
|
||||
|
||||
## 4. Piezas pendientes de la Etapa E (orden tentativo)
|
||||
|
||||
- **E1 — imagen auto-booteable (GRUB BIOS): ✅ primer corte** (`scripts/install-image.sh`). Bootea en
|
||||
QEMU sin `-kernel` hasta la shell de arje-zero. Hoy empaqueta el *builder rootfs* (con toolchain);
|
||||
un *product rootfs* lean es refinamiento.
|
||||
- **E2 — instalador a disco físico:** un `hammer install <device>` que particiona un disco real y
|
||||
vuelca las tres particiones + GRUB (la misma lógica, apuntando a `/dev/sdX` en vez de un fichero).
|
||||
- **E3 — mirror del store:** servir/replicar `/store` content-addressed (BLAKE3) entre máquinas; el
|
||||
hash ES la dirección, así que un mirror es un CAS replicado + resolución por hash.
|
||||
- **E4 — upgrades:** aplicar un nuevo árbol Stage 1 sin reinstalar — atómico vía el modelo overlay +
|
||||
el journal (D), con rollback al árbol anterior.
|
||||
- **E5 — ISO/medio de arranque:** medio live para correr el instalador (xorriso/grub-mkrescue;
|
||||
herramental a construir).
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/bin/sh
|
||||
# install-image.sh — Etapa E1 (SDD 13): produce una imagen de disco AUTO-BOOTEABLE. A diferencia de
|
||||
# scripts/disk-image.sh (B3, que se arranca con `qemu -kernel` para verificar el auto-alojamiento),
|
||||
# ésta lleva GRUB en el propio disco ⇒ bootea con `qemu-system-x86_64 -drive file=img` SIN `-kernel` ni
|
||||
# firmware extra (SeaBIOS de QEMU → MBR → core.img → kernel), como en hardware real.
|
||||
#
|
||||
# Layout (GPT, BIOS):
|
||||
# /dev/vda1 BIOS boot (ef02, ~2 MiB, sin fs) core.img de GRUB
|
||||
# /dev/vda2 / ext4 [hammer-root] + /boot/bzImage + /boot/grub
|
||||
# /dev/vda3 /store ext4 [hammer-store]
|
||||
# /dev/vda4 /var/lib/hammer ext4 [hammer-state]
|
||||
#
|
||||
# Instalación de GRUB en USERSPACE (sin root ni loop): grub-mkimage arma core.img; grub-bios-setup
|
||||
# escribe boot.img→MBR + core.img→partición BIOS sobre el fichero imagen (I/O de bloques). Los módulos
|
||||
# de GRUB + el kernel van en /boot de la root, que mke2fs -d puebla desde un directorio.
|
||||
#
|
||||
# Uso:
|
||||
# ./scripts/install-image.sh # crea work/hammer-install.img
|
||||
# BOOT=1 KVM=1 ./scripts/install-image.sh # además lo arranca (auto-boot, sin -kernel)
|
||||
#
|
||||
# Variables:
|
||||
# ROOTFS dir rootfs a empaquetar (default work/builder-rootfs)
|
||||
# KERNEL bzImage a embeber en /boot (default el del store: store/*-linux/boot/bzImage)
|
||||
# IMG imagen de salida (default work/hammer-install.img)
|
||||
# ROOT_SIZE MiB de / (default 4096)
|
||||
# STORE_SIZE MiB de /store (default 2048)
|
||||
# STATE_SIZE MiB de /var/lib/hammer (default 2048)
|
||||
# CMDLINE línea de comando del kernel (default "console=ttyS0 root=/dev/vda2 rw rdinit=/sbin/init")
|
||||
# BOOT 1 ⇒ arranca la imagen con QEMU tras crearla (auto-boot, sin -kernel)
|
||||
# KVM/MEM passthrough al arranque de prueba
|
||||
set -eu
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
ROOTFS="${ROOTFS:-work/builder-rootfs}"
|
||||
IMG="${IMG:-work/hammer-install.img}"
|
||||
ROOT_SIZE="${ROOT_SIZE:-4096}"
|
||||
STORE_SIZE="${STORE_SIZE:-2048}"
|
||||
STATE_SIZE="${STATE_SIZE:-2048}"
|
||||
CMDLINE="${CMDLINE:-console=ttyS0 root=/dev/vda2 rw rdinit=/sbin/init}"
|
||||
KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}"
|
||||
GRUB_LIB="${GRUB_LIB:-/usr/lib/grub/i386-pc}"
|
||||
|
||||
[ -d "$ROOTFS" ] || { echo "no existe ROOTFS: $ROOTFS (corré selfhost-verify para armar el builder)" >&2; exit 1; }
|
||||
[ -e "$ROOTFS/sbin/init" ] || { echo "ROOTFS sin /sbin/init (arje-zero)" >&2; exit 1; }
|
||||
[ -n "$KERNEL" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel a embeber (set KERNEL=<bzImage>)" >&2; exit 1; }
|
||||
[ -d "$GRUB_LIB" ] && [ -r "$GRUB_LIB/boot.img" ] || { echo "faltan los módulos GRUB i386-pc en $GRUB_LIB" >&2; exit 1; }
|
||||
for t in sfdisk mke2fs grub-mkimage grub-bios-setup; do
|
||||
command -v "$t" >/dev/null 2>&1 || { echo "falta $t" >&2; exit 1; }
|
||||
done
|
||||
unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; }
|
||||
|
||||
STAGE="work/.install-stage"
|
||||
rm -rf "$STAGE"
|
||||
mkdir -p "$STAGE"
|
||||
ROOT_TREE="$STAGE/root"
|
||||
ROOT_IMG="$STAGE/root.img"
|
||||
STORE_IMG="$STAGE/store.img"
|
||||
STATE_IMG="$STAGE/state.img"
|
||||
GRUB_STAGE="$STAGE/grub"
|
||||
|
||||
# --- árbol root: hardlinks del rootfs SIN store/estado + /boot (kernel + GRUB) + wrapper /sbin/init ---
|
||||
echo "==> staging root: rootfs (hardlinks) + /boot/bzImage + /boot/grub + wrapper /sbin/init"
|
||||
cp -al "$ROOTFS" "$ROOT_TREE"
|
||||
rm -rf "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
|
||||
mkdir -p "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer" "$ROOT_TREE/boot/grub/i386-pc"
|
||||
cp "$KERNEL" "$ROOT_TREE/boot/bzImage"; chmod 0644 "$ROOT_TREE/boot/bzImage"
|
||||
# Módulos GRUB que core.img cargará en tiempo de boot desde /boot/grub/i386-pc.
|
||||
cp "$GRUB_LIB"/*.mod "$GRUB_LIB"/*.lst "$ROOT_TREE/boot/grub/i386-pc/" 2>/dev/null || true
|
||||
# grub.cfg con consola serie (para -nographic) y el menuentry del kernel embebido.
|
||||
cat > "$ROOT_TREE/boot/grub/grub.cfg" <<CFG
|
||||
set timeout=2
|
||||
set default=0
|
||||
serial --unit=0 --speed=115200
|
||||
terminal_input serial console
|
||||
terminal_output serial console
|
||||
insmod part_gpt
|
||||
insmod ext2
|
||||
menuentry "hammer" {
|
||||
linux /boot/bzImage $CMDLINE
|
||||
}
|
||||
CFG
|
||||
# Wrapper /sbin/init: monta las particiones dedicadas y hace exec del init real (igual que B3).
|
||||
rm -f "$ROOT_TREE/sbin/init"
|
||||
cat > "$ROOT_TREE/sbin/init" <<'INIT'
|
||||
#!/bin/sh
|
||||
# Wrapper PID1 (Etapa E/B3): el kernel monta vda2 como /; montamos las particiones dedicadas.
|
||||
/bin/busybox mount -t ext4 /dev/vda3 /store || echo "init: no pude montar /store (/dev/vda3)"
|
||||
/bin/busybox mount -t ext4 /dev/vda4 /var/lib/hammer || echo "init: no pude montar /var/lib/hammer (/dev/vda4)"
|
||||
exec /usr/bin/arje-zero
|
||||
INIT
|
||||
chmod +x "$ROOT_TREE/sbin/init"
|
||||
|
||||
# --- GPT: vda1=BIOS boot (ef02), vda2=/, vda3=/store, vda4=/var/lib/hammer ---
|
||||
SECT_MIB=2048
|
||||
BIOS_SECT=4096 # 2 MiB para core.img
|
||||
ROOT_SECT=$(( ROOT_SIZE * SECT_MIB ))
|
||||
STORE_SECT=$(( STORE_SIZE * SECT_MIB ))
|
||||
STATE_SECT=$(( STATE_SIZE * SECT_MIB ))
|
||||
START1=2048
|
||||
START2=$(( START1 + BIOS_SECT ))
|
||||
START3=$(( START2 + ROOT_SECT ))
|
||||
START4=$(( START3 + STORE_SECT ))
|
||||
TOTAL_SECT=$(( START4 + STATE_SECT + 2048 ))
|
||||
BIOS_GUID="21686148-6449-6E6F-744E-656564454649" # GPT "BIOS boot partition"
|
||||
|
||||
echo "==> imagen : $IMG (GPT BIOS-boot, root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)"
|
||||
rm -f "$IMG"
|
||||
truncate -s $(( TOTAL_SECT * 512 )) "$IMG"
|
||||
sfdisk --quiet "$IMG" >/dev/null <<EOF
|
||||
label: gpt
|
||||
start=$START1, size=$BIOS_SECT, type=$BIOS_GUID, name="bios-boot"
|
||||
start=$START2, size=$ROOT_SECT, type=linux, name="hammer-root"
|
||||
start=$START3, size=$STORE_SECT, type=linux, name="hammer-store"
|
||||
start=$START4, size=$STATE_SECT, type=linux, name="hammer-state"
|
||||
EOF
|
||||
|
||||
# --- poblar las 3 particiones ext4 (root-owned vía unshare -r) y empalmar en su offset ---
|
||||
echo "==> mke2fs + empalme de las particiones ext4 (unshare -r, sin sudo)"
|
||||
unshare -r sh -eu <<EOF
|
||||
mke2fs -q -t ext4 -L hammer-root -E root_owner=0:0 -d "$ROOT_TREE" "$ROOT_IMG" ${ROOT_SIZE}M
|
||||
mke2fs -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store" "$STORE_IMG" ${STORE_SIZE}M
|
||||
mke2fs -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer" "$STATE_IMG" ${STATE_SIZE}M
|
||||
dd if="$ROOT_IMG" of="$IMG" bs=512 seek=$START2 conv=sparse,notrunc status=none
|
||||
dd if="$STORE_IMG" of="$IMG" bs=512 seek=$START3 conv=sparse,notrunc status=none
|
||||
dd if="$STATE_IMG" of="$IMG" bs=512 seek=$START4 conv=sparse,notrunc status=none
|
||||
EOF
|
||||
|
||||
# --- instalar GRUB en userspace SIN root/loop: grub-mkimage arma core.img, y un patch binario
|
||||
# determinista coloca boot.img→MBR + core.img→BIOS boot partition (vda1) ajustando los punteros.
|
||||
# (`grub-bios-setup` se descarta: sondea el disco FÍSICO del host —/dev/nvme…, 660 root:disk— para
|
||||
# "adivinar el root device" del dir -d, y falla sin root; el patch manual evita ese sondeo.)
|
||||
echo "==> instalar GRUB (grub-mkimage + patch binario boot/core, sin root/loop)"
|
||||
mkdir -p "$GRUB_STAGE"
|
||||
# core.img: prefix a (hd0,gpt2)/boot/grub (root = 2ª partición GPT); embebemos lo justo para leer
|
||||
# GPT+ext2, cargar grub.cfg (normal/configfile) y el kernel (linux).
|
||||
grub-mkimage -O i386-pc -p '(hd0,gpt2)/boot/grub' -o "$GRUB_STAGE/core.img" \
|
||||
biosdisk part_gpt ext2 normal configfile linux echo ls boot
|
||||
# Punteros (ABI estable de GRUB i386-pc):
|
||||
# boot.img off 0x5c kernel_sector (u64 LE): LBA del 1er sector de core.img → START1
|
||||
# core.img off 0x1F4 blocklist.start (u64 LE): LBA del RESTO de core.img → START1+1
|
||||
# Verificamos los valores por defecto (1 y 2: core.img asumido contiguo tras el MBR) antes de tocar.
|
||||
START1="$START1" GRUB_LIB="$GRUB_LIB" GRUB_STAGE="$GRUB_STAGE" IMG="$IMG" python3 - <<'PY'
|
||||
import os, struct
|
||||
s1 = int(os.environ["START1"])
|
||||
img = os.environ["IMG"]
|
||||
boot = open(os.path.join(os.environ["GRUB_LIB"], "boot.img"), "rb").read()
|
||||
core = bytearray(open(os.path.join(os.environ["GRUB_STAGE"], "core.img"), "rb").read())
|
||||
boot = bytearray(boot)
|
||||
assert struct.unpack_from("<Q", boot, 0x5c)[0] == 1, "boot.img kernel_sector != 1 (ABI GRUB cambió)"
|
||||
assert struct.unpack_from("<Q", core, 0x1f4)[0] == 2, "core.img blocklist.start != 2 (ABI GRUB cambió)"
|
||||
struct.pack_into("<Q", boot, 0x5c, s1) # core.img empieza en START1
|
||||
struct.pack_into("<Q", core, 0x1f4, s1 + 1) # el resto de core.img, justo después
|
||||
with open(img, "r+b") as f:
|
||||
f.seek(s1 * 512); f.write(core) # core.img → BIOS boot partition (vda1)
|
||||
f.seek(0); f.write(boot[:440]) # boot.img → MBR, sin pisar la GPT protective (446+)
|
||||
print(f" boot.img→MBR (kernel_sector={s1}), core.img→sector {s1} ({len(core)} B), blocklist.start={s1+1}")
|
||||
PY
|
||||
|
||||
rm -rf "$STAGE"
|
||||
echo "==> imagen instalable creada: $(du -h "$IMG" | cut -f1) en disco (sparse), $(( TOTAL_SECT * 512 / 1024 / 1024 ))M virtuales"
|
||||
echo " auto-boot: qemu-system-x86_64 -drive file=$IMG,format=raw -nographic (sin -kernel)"
|
||||
|
||||
if [ "${BOOT:-0}" = 1 ]; then
|
||||
echo "==> arranque de prueba (auto-boot, sin -kernel)"
|
||||
accel=""
|
||||
[ "${KVM:-0}" = 1 ] && [ -w /dev/kvm ] && accel="-enable-kvm -cpu host"
|
||||
# shellcheck disable=SC2086
|
||||
exec qemu-system-x86_64 -m "${MEM:-4096}" -no-reboot -nographic $accel \
|
||||
-drive file="$IMG",format=raw,if=virtio
|
||||
fi
|
||||
Reference in New Issue
Block a user