#!/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 ` 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= 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 < mke2fs + empalme de las 3 particiones (unshare -r, sin sudo)" unshare -r sh -eu < 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=" >&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