Files
sergioandClaude Opus 4.8 4dfb0ff127 Etapa B3: /store y /var/lib/hammer en particiones dedicadas (GPT)
scripts/disk-image.sh ahora arma una imagen GPT de 3 particiones ext4 en vez
de una sola: vda1=/ , vda2=/store (CAS inmutable), vda3=/var/lib/hammer (estado
mutable). Construcción sin root ni loopback: cp -al stagea el rootfs por
hardlinks (vacía store/ y var/lib/hammer/, instala el wrapper /sbin/init),
mke2fs -d puebla cada ext4 bajo unshare -r (root-owned), sfdisk escribe la GPT
y dd conv=sparse,notrunc empalma cada fs en su offset (imagen sparse, ~2G
reales). El wrapper /sbin/init monta vda2/vda3 y hace exec de arje-zero (el
kernel sólo monta vda1). drive-rebuild.py: root=/dev/vda1 en modo DISK.

Consecuencia resuelta: con /store en su propia partición el sellado cruza
filesystems y rename(2) da EXDEV. Store::seal cae a copia recursiva a un
staging dentro del store (preserva symlinks+modos) + rename store-interno
(atómico, mismo FS) + borrado del origen. Test copy_tree añadido.

Verificado in-VM (kernel hammer, KVM): vda{1,2,3} montados dedicados, 0
errores Cross-device, stage1' == stage1 ✓ REPRODUCIBLE. Cierra el ☐ de
SDD 11 §6 (particionado/montaje en la imagen destino).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:05:35 -04:00

121 lines
6.9 KiB
Bash
Executable File

#!/bin/sh
# disk-image.sh — Etapa B3 (mounts dedicados): empaqueta el rootfs como una imagen de disco virtio
# PARTICIONADA (GPT) con tres filesystems ext4 dedicados, en vez de una única partición que mete todo
# junto (B2). Es el paso hacia la imagen instalable real: el store y el estado mutable dejan de ser meros
# directorios del rootfs y pasan a ser particiones propias, como en un sistema instalado de verdad.
#
# /dev/vda1 → / rootfs (runtime + toolchain + /work del rebuild) [hammer-root]
# /dev/vda2 → /store artefactos CAS BLAKE3 (inmutables) [hammer-store]
# /dev/vda3 → /var/lib/hammer estado mutable: journal + overlays (upper/work) [hammer-state]
#
# El kernel (recipes/linux.toml) trae VIRTIO_BLK+EXT4+EFI_PARTITION(=y por defconfig) ⇒ lee la GPT y
# monta /dev/vda1 como root sin initramfs. Las otras dos particiones las monta un wrapper `/sbin/init`
# (montar /store + /var/lib/hammer, luego `exec /usr/bin/arje-zero`) — el init real (arje-zero) sigue
# en su sitio; el wrapper es el puente mientras arje no lea fstab/cards de montaje por sí mismo.
#
# Construcción sin root ni loopback:
# - `cp -al` arma un árbol del rootfs por HARDLINKS (sin copiar datos), le vacía store/ y
# var/lib/hammer/ (deja mountpoints vacíos) y le instala el wrapper /sbin/init. NUNCA muta $ROOTFS.
# - `mke2fs -d <dir>` puebla cada ext4 desde un directorio sin montar nada (e2fsprogs >= 1.43).
# - `unshare -r` mapea nuestro uid→0 ⇒ los ficheros uid-host se sellan como uid 0 en la imagen (igual
# que el `--owner=root:root` del cpio; el copy-up de overlay del rebuild falla EACCES si van uid 1000).
# - `sfdisk` escribe la GPT sobre el fichero-imagen (no necesita root) y `dd conv=notrunc` empalma cada
# ext4 en el offset de su partición. Tamaños exactos: la partición mide lo mismo que el ext4 (M==MiB).
#
# Uso:
# ./scripts/disk-image.sh # crea work/hammer-disk.img
# BOOT=1 KERNEL=<bzImage> KVM=1 ./scripts/disk-image.sh # además bootea y corre rebuild-stage1
#
# Variables:
# ROOTFS dir rootfs a empaquetar (default work/builder-rootfs, lo arma selfhost-verify)
# IMG imagen de salida (default work/hammer-disk.img)
# ROOT_SIZE MiB de la partición / (default 6144; incluye /work del rebuild)
# STORE_SIZE MiB de la partición /store (default 2048; artefactos CAS ~0.4G + reseal del rebuild)
# STATE_SIZE MiB de /var/lib/hammer (default 2048; journal + overlays)
# BOOT 1 ⇒ bootea con drive-rebuild.py tras crear la imagen
# KERNEL bzImage con VIRTIO_BLK+EXT4 (requerido si BOOT=1)
# KVM/MEM/NET passthrough a drive-rebuild.py
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
ROOTFS="${ROOTFS:-work/builder-rootfs}"
IMG="${IMG:-work/hammer-disk.img}"
ROOT_SIZE="${ROOT_SIZE:-6144}"
STORE_SIZE="${STORE_SIZE:-2048}"
STATE_SIZE="${STATE_SIZE:-2048}"
[ -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; }
command -v sfdisk >/dev/null 2>&1 || { echo "falta sfdisk (util-linux) para la tabla GPT" >&2; exit 1; }
command -v mke2fs >/dev/null 2>&1 || { echo "falta mke2fs (e2fsprogs)" >&2; exit 1; }
unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; }
STAGE="work/.disk-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"
# --- árbol del rootfs SIN store/estado, con el wrapper /sbin/init (hardlinks, no copia datos) ---
echo "==> staging root (hardlinks) sin /store ni /var/lib/hammer + 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" # mountpoints vacíos
rm -f "$ROOT_TREE/sbin/init" # era symlink → /usr/bin/arje-zero
cat > "$ROOT_TREE/sbin/init" <<'INIT'
#!/bin/sh
# Wrapper PID1 (Etapa B3): monta los filesystems dedicados antes del init real. El kernel arranca con
# root=/dev/vda1; /store (CAS inmutable) y /var/lib/hammer (estado mutable) son particiones propias que
# el kernel NO monta solo. DEVTMPFS_MOUNT=y deja /dev poblado, así que /dev/vda{2,3} ya existen aquí.
/bin/busybox mount -t ext4 /dev/vda2 /store || echo "init: no pude montar /store (/dev/vda2)"
/bin/busybox mount -t ext4 /dev/vda3 /var/lib/hammer || echo "init: no pude montar /var/lib/hammer (/dev/vda3)"
exec /usr/bin/arje-zero
INIT
chmod +x "$ROOT_TREE/sbin/init"
# --- tabla de particiones GPT (sectores de 512 B; 1 MiB = 2048 sectores, alineación por defecto) ---
SECT_MIB=2048
ROOT_SECT=$(( ROOT_SIZE * SECT_MIB ))
STORE_SECT=$(( STORE_SIZE * SECT_MIB ))
STATE_SECT=$(( STATE_SIZE * SECT_MIB ))
START1=2048
START2=$(( START1 + ROOT_SECT ))
START3=$(( START2 + STORE_SECT ))
TOTAL_SECT=$(( START3 + STATE_SECT + 2048 )) # + 1 MiB de slack para la GPT de respaldo
echo "==> imagen : $IMG (GPT, 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=$ROOT_SECT, type=linux, name="hammer-root"
start=$START2, size=$STORE_SECT, type=linux, name="hammer-store"
start=$START3, size=$STATE_SECT, type=linux, name="hammer-state"
EOF
# --- poblar cada ext4 (root-owned vía unshare -r) y empalmar en su offset ---
echo "==> mke2fs + empalme de las 3 particiones (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
# conv=sparse,notrunc: empalma sin destruir la GPT y deja los bloques cero del ext4 como huecos (si no,
# dd rellena la imagen entera y la sparse del truncate se pierde → 11G reales en vez de ~2G).
dd if="$ROOT_IMG" of="$IMG" bs=512 seek=$START1 conv=sparse,notrunc status=none
dd if="$STORE_IMG" of="$IMG" bs=512 seek=$START2 conv=sparse,notrunc status=none
dd if="$STATE_IMG" of="$IMG" bs=512 seek=$START3 conv=sparse,notrunc status=none
EOF
rm -rf "$STAGE"
echo "==> imagen creada: $(du -h "$IMG" | cut -f1) en disco (sparse), $(( TOTAL_SECT * 512 / 1024 / 1024 ))M virtuales"
if [ "${BOOT:-0}" = 1 ]; then
[ -n "${KERNEL:-}" ] || { echo "BOOT=1 requiere KERNEL=<bzImage con VIRTIO_BLK+EXT4>" >&2; exit 1; }
echo "==> booteando de disco particionado (root=/dev/vda1, /store+/var/lib/hammer montados por /sbin/init)"
DISK="$IMG" KERNEL="$KERNEL" KVM="${KVM:-0}" MEM="${MEM:-6144}" NET="${NET:-1}" \
python3 scripts/drive-rebuild.py
fi