#!/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 [takana-root] + /boot/bzImage + /boot/grub # /dev/vda3 /store ext4 [takana-store] # /dev/vda4 /var/lib/hammer ext4 [takana-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) # # Target IN-PLACE (Etapa E2): si IMG es un block device (o PREALLOC=1 sobre un fichero pre-dimensionado), # se instala EN SITIO — no se trunca ni se borra el nodo, sólo se valida capacidad. Esa es la base de # `takana install /dev/sdX` (ver scripts/hammer-install.sh): misma lógica, apuntando a un disco real. # # Variables: # ROOTFS dir rootfs a empaquetar (default work/builder-rootfs) # PREALLOC 1 ⇒ trata IMG como target pre-dimensionado in-place (no trunca; prueba el camino device) # 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=)" >&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" < "$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" # Target = block device (instalación in-place, Etapa E2 `takana install /dev/sdX`) o fichero imagen. # Un device tiene tamaño fijo: NO se trunca ni se borra el nodo; se valida capacidad. PREALLOC=1 fuerza # este modo sobre un fichero pre-dimensionado (para probar el camino de device sin hardware real). TOTAL_BYTES=$(( TOTAL_SECT * 512 )) if [ -b "$IMG" ] || [ "${PREALLOC:-0}" = 1 ]; then cur=$(blockdev --getsize64 "$IMG" 2>/dev/null || stat -c%s "$IMG" 2>/dev/null || echo 0) [ "$cur" -ge "$TOTAL_BYTES" ] || { echo "target $IMG es ${cur}B < ${TOTAL_BYTES}B requeridos" >&2; exit 1; } echo "==> target IN-PLACE: $IMG ($(( cur/1024/1024 ))M disponibles; root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)" else echo "==> imagen : $IMG (GPT BIOS-boot, root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)" rm -f "$IMG" truncate -s "$TOTAL_BYTES" "$IMG" fi sfdisk --quiet "$IMG" >/dev/null < mke2fs + empalme de las particiones ext4 (unshare -r, sin sudo)" unshare -r sh -eu < 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(" 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