Files
Mikhail ChusavitinandClaude Sonnet 5 b8c45d54c1 feat(iso): split the live medium into semantic SquashFS layers
Booting via BMC virtual CD reads the ~2.8 GB filesystem squashfs
sequentially during the live-boot toram copy; a mid-read drop of the
redirected medium loses the whole copy and fails the boot (v14). Split
the rootfs into self-contained semantic layers so a retry re-reads at
most one ~500-700 MiB layer, not everything. This is a resilience /
reduced-re-read mechanism, not a fix for the virtual-media instability.

NVIDIA variants now ship 7 layers (00-base, 05-firmware, 08-desktop,
10-nvidia-driver, 20-nvidia-platform, 30-nvidia-cuda-libs,
40-nvidia-dcgm-cuda) plus an explicit live/filesystem.module that fixes
their OverlayFS order; amd/nogpu keep a single squashfs.

- lib/squashfs-layers.sh: deterministic classifier (dpkg file ownership
  plus explicit rules for build.sh-injected files, never a path
  substring), per-layer mksquashfs, 800 MiB hard ceiling, unsquashfs -s
  plus strict extraction of every layer, merged-rootfs bootability check.
- build.sh: split the monolith after the full lb build, verify and merge,
  write the module file, delete the monolith only then; abort before ISO
  assembly on any failure. Runs the builder test suites up front.
- fast-path: force a full build for a multi-layer medium;
  fast_path_repack_squashfs hard-refuses (it would drop layers).
- iso-validation.sh: validate_iso_squashfs_layers (module vs layer set
  match, size ceiling, no lone giant squashfs) and
  validate_iso_media_integrity (xorriso -check_media).
- bee-install: honour filesystem.module order, abort on any layer failure.
- 9013-toram-retry: record the real rsync exit code (it printed a false
  rc=0) and correct the "resumes the tail" comment (rsync without
  --partial keeps only fully-copied layers). No unsafe partial resume.
- tests: test-squashfs-layers.sh plus a multi-layer guard in
  test-build-libs.sh; both run at the top of every build.
- docs: bible-local architecture and decision, iso/README, iso-build-rules.

Verified by a full nvidia build: 7 layers 622/199/256/466/37/567/562 MiB,
every validator passes, xorriso -check_media good, merged rootfs bootable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:38:14 +03:00

367 lines
13 KiB
Bash
Executable File

#!/bin/bash
# bee-install — install the live system to a local disk.
#
# Usage: bee-install <device> [logfile]
# device — target block device, e.g. /dev/sda (will be WIPED)
# logfile — optional path to write progress log (default: /tmp/bee-install.log)
#
# Layout (UEFI): GPT, /dev/sdX1=EFI 512MB vfat, /dev/sdX2=root ext4
# Layout (BIOS): MBR, /dev/sdX1=root ext4
#
# Squashfs sources: /run/live/medium/live/*.squashfs
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage: bee-install <device> [logfile]
Installs the live system to a local disk (WIPES the target).
device Target block device, e.g. /dev/sda or /dev/nvme0n1
Must be a hard disk or NVMe — NOT a CD-ROM (/dev/sr*)
logfile Optional path for progress log (default: /tmp/bee-install.log)
Examples:
bee-install /dev/sda
bee-install /dev/nvme0n1
bee-install /dev/sdb /tmp/my-install.log
WARNING: ALL DATA ON <device> WILL BE ERASED.
Layout (UEFI): GPT — partition 1: EFI 512MB vfat, partition 2: root ext4
Layout (BIOS): MBR — partition 1: root ext4
EOF
exit 1
}
DEVICE="${1:-}"
LOGFILE="${2:-/tmp/bee-install.log}"
if [ -z "$DEVICE" ] || [ "$DEVICE" = "--help" ] || [ "$DEVICE" = "-h" ]; then
usage
fi
if [ ! -b "$DEVICE" ]; then
echo "ERROR: $DEVICE is not a block device" >&2
echo "Run 'lsblk' to list available disks." >&2
exit 1
fi
# Block CD-ROM devices
case "$DEVICE" in
/dev/sr*|/dev/scd*)
echo "ERROR: $DEVICE is a CD-ROM/optical device — cannot install to it." >&2
echo "Run 'lsblk' to find the target disk (e.g. /dev/sda, /dev/nvme0n1)." >&2
exit 1
;;
esac
# Check required tools
for tool in parted mkfs.vfat mkfs.ext4 unsquashfs grub-install update-grub; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "ERROR: required tool not found: $tool" >&2
exit 1
fi
done
# Order the squashfs layers exactly as live-boot stacks them: if the medium
# carries live/filesystem.module, honour that list verbatim (it is the same
# order OverlayFS uses, last entry = highest priority); otherwise fall back to a
# lexical sort, which the NN- numeric prefixes make equivalent.
LIVE_DIR=/run/live/medium/live
MODULE_FILE="${LIVE_DIR}/filesystem.module"
read_squashfs_layers() {
SQUASHFS_FILES=()
if [ -f "$MODULE_FILE" ]; then
while IFS= read -r name; do
[ -n "$name" ] || continue
case "$name" in \#*) continue ;; esac
if [ -f "${LIVE_DIR}/${name}" ]; then
SQUASHFS_FILES+=("${LIVE_DIR}/${name}")
else
echo "ERROR: filesystem.module lists ${name} but it is missing from the medium" >&2
exit 1
fi
done < "$MODULE_FILE"
else
mapfile -t SQUASHFS_FILES < <(find "$LIVE_DIR" -maxdepth 1 -type f -name '*.squashfs' | sort)
fi
}
read_squashfs_layers
if [ "${#SQUASHFS_FILES[@]}" -eq 0 ]; then
echo "ERROR: no squashfs files found under /run/live/medium/live" >&2
echo " The live medium may have been disconnected." >&2
echo " Reconnect the disc and run: bee-remount-medium --wait" >&2
echo " Then re-run bee-install." >&2
exit 1
fi
MOUNT_ROOT="/mnt/bee-install-root"
# ------------------------------------------------------------------
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOGFILE"; }
die() { log "ERROR: $*"; exit 1; }
# ------------------------------------------------------------------
# Detect UEFI
if [ -d /sys/firmware/efi ]; then
UEFI=1
log "Boot mode: UEFI"
else
UEFI=0
log "Boot mode: BIOS/legacy"
fi
# Determine partition names (nvme uses p-suffix: nvme0n1p1)
if echo "$DEVICE" | grep -qE 'nvme|mmcblk'; then
PART_PREFIX="${DEVICE}p"
else
PART_PREFIX="${DEVICE}"
fi
if [ "$UEFI" = "1" ]; then
PART_EFI="${PART_PREFIX}1"
PART_ROOT="${PART_PREFIX}2"
else
PART_ROOT="${PART_PREFIX}1"
fi
# ------------------------------------------------------------------
log "=== BEE DISK INSTALLER ==="
log "Target device : $DEVICE"
log "Root partition: $PART_ROOT"
[ "$UEFI" = "1" ] && log "EFI partition : $PART_EFI"
log "Squashfs : ${#SQUASHFS_FILES[@]} layer(s)"
for sf in "${SQUASHFS_FILES[@]}"; do
log " - $sf ($(du -sh "$sf" | cut -f1))"
done
log "Log : $LOGFILE"
log ""
# ------------------------------------------------------------------
log "--- Step 1/7: Unmounting target device ---"
# Unmount any partitions on target device
for part in "${DEVICE}"* ; do
if [ "$part" = "$DEVICE" ]; then continue; fi
if mountpoint -q "$part" 2>/dev/null; then
log " umount $part"
umount "$part" || true
fi
done
# Also unmount our mount point if leftover
umount "${MOUNT_ROOT}" 2>/dev/null || true
umount "${MOUNT_ROOT}/boot/efi" 2>/dev/null || true
# ------------------------------------------------------------------
log "--- Step 2/7: Partitioning $DEVICE ---"
if [ "$UEFI" = "1" ]; then
parted -s "$DEVICE" mklabel gpt
parted -s "$DEVICE" mkpart EFI fat32 1MiB 513MiB
parted -s "$DEVICE" set 1 esp on
parted -s "$DEVICE" mkpart root ext4 513MiB 100%
else
parted -s "$DEVICE" mklabel msdos
parted -s "$DEVICE" mkpart primary ext4 1MiB 100%
parted -s "$DEVICE" set 1 boot on
fi
# Wait for kernel to see new partitions
sleep 1
partprobe "$DEVICE" 2>/dev/null || true
sleep 1
log " Partitioning done."
# ------------------------------------------------------------------
log "--- Step 3/7: Formatting ---"
if [ "$UEFI" = "1" ]; then
mkfs.vfat -F32 -n EFI "$PART_EFI"
log " $PART_EFI formatted as vfat (EFI)"
fi
mkfs.ext4 -F -L bee-root "$PART_ROOT"
log " $PART_ROOT formatted as ext4"
# ------------------------------------------------------------------
log "--- Step 4/7: Mounting target ---"
mkdir -p "$MOUNT_ROOT"
mount "$PART_ROOT" "$MOUNT_ROOT"
if [ "$UEFI" = "1" ]; then
mkdir -p "${MOUNT_ROOT}/boot/efi"
mount "$PART_EFI" "${MOUNT_ROOT}/boot/efi"
fi
log " Mounted."
# ------------------------------------------------------------------
log "--- Step 5/7: Unpacking filesystem (this takes 10-20 minutes) ---"
for sf in "${SQUASHFS_FILES[@]}"; do
log " Source: $sf"
done
log " Target: $MOUNT_ROOT"
# unsquashfs does not support resume, so retry the entire unpack step if the
# source medium disappears mid-copy (e.g. CD physically disconnected).
UNPACK_ATTEMPTS=0
UNPACK_MAX=5
while true; do
UNPACK_ATTEMPTS=$(( UNPACK_ATTEMPTS + 1 ))
if [ "$UNPACK_ATTEMPTS" -gt "$UNPACK_MAX" ]; then
die "Unpack failed $UNPACK_MAX times — giving up. Check the disc and logs."
fi
[ "$UNPACK_ATTEMPTS" -gt 1 ] && log " Retry attempt $UNPACK_ATTEMPTS / $UNPACK_MAX ..."
read_squashfs_layers
if [ "${#SQUASHFS_FILES[@]}" -eq 0 ]; then
log " SOURCE LOST: no squashfs files found under /run/live/medium/live."
log " Reconnect the disc and run 'bee-remount-medium --wait' in another terminal,"
log " then press Enter here to retry."
read -r _
continue
fi
# wipe partial unpack so unsquashfs starts clean
if [ "$UNPACK_ATTEMPTS" -gt 1 ]; then
log " Cleaning partial unpack from $MOUNT_ROOT ..."
# keep the mount point itself but remove its contents
find "$MOUNT_ROOT" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
fi
# Unpack every layer in module order. unsquashfs -f makes a later layer
# overwrite an earlier one, matching live-boot's OverlayFS precedence. A
# failed layer aborts this attempt: a partial multi-layer unpack must never
# be treated as a usable install.
UNPACK_OK=0
UNPACK_TMP=$(mktemp)
for sf in "${SQUASHFS_FILES[@]}"; do
log " Unpacking $(basename "$sf") ..."
rc=0
unsquashfs -f -d "$MOUNT_ROOT" "$sf" > "$UNPACK_TMP" 2>&1 || rc=$?
grep -E '^\[|^inod|^created|^extract|^ERROR|failed' "$UNPACK_TMP" \
| while IFS= read -r line; do log " $line"; done || true
if [ "$rc" -ne 0 ]; then
log " ERROR: unsquashfs failed for $(basename "$sf") (rc=$rc)"
UNPACK_OK=$rc
break
fi
done
rm -f "$UNPACK_TMP"
# Check squashfs is still reachable (gone = disc pulled during copy)
read_squashfs_layers
if [ "${#SQUASHFS_FILES[@]}" -eq 0 ]; then
log " WARNING: source medium lost during unpack — will retry after remount."
log " Run 'bee-remount-medium --wait' in another terminal, then press Enter."
read -r _
continue
fi
if [ "$UNPACK_OK" -ne 0 ]; then
log " A layer failed to unpack — retrying the whole unpack."
[ "$UNPACK_ATTEMPTS" -lt "$UNPACK_MAX" ] && sleep 5
continue
fi
# Verify the unpack produced a usable root: /etc from the base layer and
# /usr/sbin/init must both be present after all layers are applied.
if [ -d "${MOUNT_ROOT}/etc" ] && [ -e "${MOUNT_ROOT}/usr/sbin/init" ]; then
log " Unpack complete (${#SQUASHFS_FILES[@]} layer(s))."
break
else
log " WARNING: unpack produced no /etc — squashfs may be corrupt or incomplete."
if [ "$UNPACK_ATTEMPTS" -lt "$UNPACK_MAX" ]; then
log " Retrying in 5 s ..."
sleep 5
fi
fi
done
# ------------------------------------------------------------------
log "--- Step 6/7: Configuring installed system ---"
# Write /etc/fstab
ROOT_UUID=$(blkid -s UUID -o value "$PART_ROOT")
log " Root UUID: $ROOT_UUID"
cat > "${MOUNT_ROOT}/etc/fstab" <<FSTAB
# Generated by bee-install
UUID=${ROOT_UUID} / ext4 defaults,errors=remount-ro 0 1
tmpfs /tmp tmpfs defaults,size=512m 0 0
FSTAB
if [ "$UEFI" = "1" ]; then
EFI_UUID=$(blkid -s UUID -o value "$PART_EFI")
echo "UUID=${EFI_UUID} /boot/efi vfat umask=0077 0 1" >> "${MOUNT_ROOT}/etc/fstab"
fi
log " fstab written."
# Remove live-boot persistence markers so installed system boots normally
rm -f "${MOUNT_ROOT}/etc/live/boot.conf" 2>/dev/null || true
rm -f "${MOUNT_ROOT}/etc/live/live.conf" 2>/dev/null || true
# Bind mount virtual filesystems for chroot
mount --bind /dev "${MOUNT_ROOT}/dev"
mount --bind /proc "${MOUNT_ROOT}/proc"
mount --bind /sys "${MOUNT_ROOT}/sys"
[ "$UEFI" = "1" ] && mount --bind /sys/firmware/efi/efivars "${MOUNT_ROOT}/sys/firmware/efi/efivars" 2>/dev/null || true
# ------------------------------------------------------------------
log "--- Step 7/7: Installing GRUB bootloader ---"
# Helper: run a chroot command, log all output, return its exit code.
# Needed because "cmd | while" pipelines hide the exit code of cmd.
chroot_log() {
local rc=0
local out
out=$(chroot "$MOUNT_ROOT" "$@" 2>&1) || rc=$?
echo "$out" | while IFS= read -r line; do log " $line"; done
return $rc
}
if [ "$UEFI" = "1" ]; then
# Primary attempt: write EFI NVRAM entry (requires writable efivars)
if ! chroot_log grub-install \
--target=x86_64-efi \
--efi-directory=/boot/efi \
--bootloader-id=bee \
--recheck; then
log " WARNING: grub-install (with NVRAM) failed — retrying with --no-nvram"
# --no-nvram: write grubx64.efi but skip EFI variable update.
# Needed on headless servers where efivars is read-only or unavailable.
chroot_log grub-install \
--target=x86_64-efi \
--efi-directory=/boot/efi \
--bootloader-id=bee \
--no-nvram \
--recheck || log " WARNING: grub-install --no-nvram also failed — check logs"
fi
# Always install the UEFI fallback path EFI/BOOT/BOOTX64.EFI.
# Many UEFI implementations (especially server BMCs and some firmware)
# ignore the NVRAM boot entry and only look for this path.
GRUB_EFI="${MOUNT_ROOT}/boot/efi/EFI/bee/grubx64.efi"
FALLBACK_DIR="${MOUNT_ROOT}/boot/efi/EFI/BOOT"
if [ -f "$GRUB_EFI" ]; then
mkdir -p "$FALLBACK_DIR"
cp "$GRUB_EFI" "${FALLBACK_DIR}/BOOTX64.EFI"
log " Fallback EFI binary installed: EFI/BOOT/BOOTX64.EFI"
else
log " WARNING: grubx64.efi not found at $GRUB_EFI — UEFI fallback path not set"
fi
else
chroot_log grub-install \
--target=i386-pc \
--recheck \
"$DEVICE" || log " WARNING: grub-install (BIOS) failed — check logs"
fi
chroot_log update-grub || log " WARNING: update-grub failed — check logs"
log " GRUB step complete."
# ------------------------------------------------------------------
# Cleanup
log "--- Cleanup ---"
umount "${MOUNT_ROOT}/sys/firmware/efi/efivars" 2>/dev/null || true
umount "${MOUNT_ROOT}/sys" 2>/dev/null || true
umount "${MOUNT_ROOT}/proc" 2>/dev/null || true
umount "${MOUNT_ROOT}/dev" 2>/dev/null || true
[ "$UEFI" = "1" ] && umount "${MOUNT_ROOT}/boot/efi" 2>/dev/null || true
umount "$MOUNT_ROOT" 2>/dev/null || true
rmdir "$MOUNT_ROOT" 2>/dev/null || true
log ""
log "=== INSTALLATION COMPLETE ==="
log "Remove the ISO and reboot to start the installed system."