From 6cfb8082c58bac38c2cd0281120af7fe930f5db6 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 27 Mar 2026 23:33:31 +0000 Subject: [PATCH] fix: xorriso append_partition for real USB boot + grub-mkstandalone Root cause of USB boot failure: our xorriso used -e boot/grub/efi.img to embed the EFI image inside the ISO. This works for CD-ROM and QEMU but NOT for USB on real UEFI hardware. Fix: use the Will Haley / Debian live-build approach: - -append_partition 2 (GPT type EFI) appends efi.img AFTER ISO data - -e --interval:appended_partition_2:all:: references the appended partition - --mbr-force-bootable forces MBR active flag - grub-mkstandalone with embedded bootstrap config (searches for grub.cfg) - grub.cfg placed in both /boot/grub/ AND /EFI/BOOT/ on ISO - grub.cfg uses search --label ARCHIPELAGO to find the ISO root This is the exact approach used by StartOS, TAILS, and every production custom Debian live ISO that boots from USB. Also: iso-debug, iso-branding skills + reference docs, dev-start.sh option 0 for branding dev, improved dev-branding.sh and test-iso-qemu.sh. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/build-iso/SKILL.md | 180 +++++---- .claude/skills/iso-branding/SKILL.md | 146 +++++++ .claude/skills/iso-debug/SKILL.md | 175 +++++++++ .../references/boot-chain-reference.md | 367 ++++++++++++++++++ image-recipe/build-auto-installer-iso.sh | 107 +++-- image-recipe/dev-branding.sh | 248 +++++++----- 6 files changed, 1006 insertions(+), 217 deletions(-) create mode 100644 .claude/skills/iso-branding/SKILL.md create mode 100644 .claude/skills/iso-debug/SKILL.md create mode 100644 .claude/skills/iso-debug/references/boot-chain-reference.md diff --git a/.claude/skills/build-iso/SKILL.md b/.claude/skills/build-iso/SKILL.md index 32e1d53e..c08b07f2 100644 --- a/.claude/skills/build-iso/SKILL.md +++ b/.claude/skills/build-iso/SKILL.md @@ -1,87 +1,121 @@ --- name: build-iso -description: Build a new Archipelago auto-installer ISO image (bundled or unbundled) -disable-model-invocation: true -allowed-tools: Bash, Read +description: Build Archipelago auto-installer ISOs. Custom debootstrap base (no Debian Live dependency), live-boot for squashfs root, hybrid BIOS+UEFI boot, Archipelago branding. Use when user says "build ISO", "build image", "create installer", or needs to work on the ISO build pipeline. +allowed-tools: Bash, Read, Edit, Write, Grep, Glob, Agent --- -Build a new Archipelago auto-installer ISO. +# Build Archipelago ISO -## Pre-build checklist +## Architecture (dev-iso branch) -1. Latest code deployed to server (`/deploy` first) -2. System configs synced (`/sync-configs` first) -3. Everything tested and working on live server -4. Sync build scripts to server before building: - ```bash - rsync -avz -e "ssh -i ~/.ssh/archipelago-deploy" \ - /Users/dorian/Projects/archy/image-recipe/build-auto-installer-iso.sh \ - /Users/dorian/Projects/archy/image-recipe/build-unbundled-iso.sh \ - archipelago@192.168.1.228:~/archy/image-recipe/ - ``` +Custom debootstrap-based installer. NO Debian Live ISO download. -## Build variants +| Component | Source | Size | +|-----------|--------|------| +| Installer squashfs | debootstrap --variant=minbase + live-boot | ~180MB | +| Target rootfs | Docker build (Debian bookworm, full stack) | ~1.5GB compressed | +| Kernel + initramfs | From debootstrap, with live-boot hooks | ~50MB | +| GRUB + ISOLINUX | Built from packages during Step 2 | ~1MB | +| **Total ISO** | **Unbundled** | **~2.2GB** | -### Unbundled ISO (recommended for distribution — ~3GB) -No pre-bundled container images. Apps install on-demand from Marketplace (requires internet). +## Build Pipeline (6 Steps) + +**Step 1** (lines ~200-430): Build target rootfs via Docker +- Debian bookworm + all runtime packages (podman, nginx, tor, chromium, etc.) +- `--no-install-recommends` for size reduction +- Strips docs/man/locales +- Output: `archipelago-rootfs.tar` (~1.5GB) + +**Step 2** (lines ~430-710): Build installer environment via debootstrap +- `debootstrap --variant=minbase` inside a container +- Installs live-boot via chroot (NOT --include — minbase can't resolve it) +- Custom initramfs with live-boot hooks +- Builds GRUB EFI image with grub-mkimage +- Creates ISOLINUX files, EFI boot image +- Installs GRUB theme + background +- Output: vmlinuz, initrd.img, filesystem.squashfs, BOOTX64.EFI, efi.img, isolinux.bin + +**Step 3** (lines ~710-850): Add Archipelago components +- Backend binary, web UI, rootfs.tar, scripts, Plymouth theme + +**Step 3b** (lines ~850-1230): Bundle container images (skipped if UNBUNDLED=1) + +**Step 4** (lines ~1230-2380): Generate auto-install.sh +- Embedded installer script (~1100 lines) +- Disk detection, partitioning, LUKS encryption, GRUB install +- Installs GRUB + Plymouth theme on target + +**Step 5** (lines ~2380-2460): Configure boot loaders +- Write GRUB config (boot=live components) +- Write ISOLINUX config +- Both reference kernel at /live/vmlinuz + +**Step 6** (lines ~2460-2540): Create final ISO +- xorriso with hybrid BIOS+UEFI boot +- Uses proven MBR from `branding/isohdpfx.bin` +- `-partition_offset 16` for UEFI compatibility + +## CI Workflow + +**Branch**: `dev-iso` → `.gitea/workflows/build-iso-dev.yml` +**Branch**: `main` → `.gitea/workflows/build-iso.yml` + +Dev CI includes a smoke test step that verifies: +- All critical files present in ISO +- Initrd contains live-boot scripts +- grub.cfg has boot=live +- Fails build before copying to Builds if any check fails + +## Critical Rules + +1. **MBR**: Always use `branding/isohdpfx.bin` (Debian Live MBR, starts with `4552`). The ISOLINUX generic MBR (`33ed`) doesn't boot on all hardware. + +2. **live-boot**: Must be installed via `chroot /installer apt-get install` AFTER debootstrap completes. The `--include` flag silently fails for live-boot. + +3. **Initramfs**: `update-initramfs` needs `/proc`, `/sys`, `/dev` bind-mounted in the chroot. Without them, the initramfs is broken. + +4. **scripts/live is a FILE**: Verify with `[ -e ]` not `[ -d ]`. + +5. **Kernel params**: Must include `boot=live components`. Without `boot=live`, live-boot hooks never activate. + +6. **partition_offset 16**: Required in xorriso for UEFI firmware to recognize the USB. + +7. **Never push during a running CI build**: The gitea-runner kills in-progress builds when a new commit arrives on the same branch. + +## Quick Commands ```bash +# Build locally (on .228): +ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 +cd ~/archy/image-recipe +sudo UNBUNDLED=1 DEV_SERVER=localhost BUILD_FROM_SOURCE=0 ./build-auto-installer-iso.sh + +# Check build status: ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \ - 'cd ~/archy/image-recipe && sudo ./build-unbundled-iso.sh' + "ps aux | grep build-auto | grep -v grep" + +# Check latest ISO: +ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \ + "ls -lt /var/lib/archipelago/filebrowser/Builds/archipelago-dev-*.iso | head -3" + +# Verify ISO: +# See /iso-debug skill for the full verification checklist + +# Iterate on branding without rebuilding: +./image-recipe/dev-branding.sh [path-to-iso] +# Or: ./scripts/dev-start.sh → option 0 ``` -Output: `results/archipelago-installer-unbundled-x86_64.iso` +## Key Files -### Full bundled ISO (~11GB) -All container images pre-bundled for offline install. - -```bash -ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \ - 'cd ~/archy/image-recipe && sudo ./build-auto-installer-iso.sh' -``` - -Output: `results/archipelago-installer-x86_64.iso` - -## Post-build: ALWAYS publish to FileBrowser - -After EVERY successful build, copy the ISO to the FileBrowser `Builds` folder so it's downloadable from the web UI. This is mandatory — do not skip. - -**FileBrowser data root**: `/var/lib/archipelago/filebrowser/` - -```bash -# For unbundled: -ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \ - 'sudo mkdir -p /var/lib/archipelago/filebrowser/Builds && \ - sudo cp ~/archy/image-recipe/results/archipelago-installer-unbundled-x86_64.iso /var/lib/archipelago/filebrowser/Builds/ && \ - sudo chown 1000:1000 /var/lib/archipelago/filebrowser/Builds/archipelago-installer-unbundled-x86_64.iso' - -# For bundled: -ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \ - 'sudo mkdir -p /var/lib/archipelago/filebrowser/Builds && \ - sudo cp ~/archy/image-recipe/results/archipelago-installer-x86_64.iso /var/lib/archipelago/filebrowser/Builds/ && \ - sudo chown 1000:1000 /var/lib/archipelago/filebrowser/Builds/archipelago-installer-x86_64.iso' -``` - -## Post-build: Download to Mac (optional) - -```bash -# Unbundled: -scp -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228:~/archy/image-recipe/results/archipelago-installer-unbundled-x86_64.iso ~/Downloads/ - -# Bundled: -scp -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228:~/archy/image-recipe/results/archipelago-installer-x86_64.iso ~/Downloads/ -``` - -## Key paths on server - -- Build scripts: `~/archy/image-recipe/build-auto-installer-iso.sh`, `build-unbundled-iso.sh` -- Build output: `~/archy/image-recipe/results/` -- Build cache (rootfs, base ISO): `~/archy/image-recipe/build/auto-installer/` -- FileBrowser Builds: `/var/lib/archipelago/filebrowser/Builds/` - -## Notes - -- Use `--rebuild` flag to force rootfs rebuild (otherwise uses cached) -- FileBrowser container mounts `/var/lib/archipelago/filebrowser` → `/srv` -- Always `chown 1000:1000` files in FileBrowser so the app can serve them -- **IMPORTANT**: Use `build-auto-installer-iso.sh` (or `build-unbundled-iso.sh`) only. The deprecated `build-debian-iso.sh` causes boot-to-prompt issues. +| File | Role | +|------|------| +| `image-recipe/build-auto-installer-iso.sh` | Main build script (~2600 lines) | +| `image-recipe/build-unbundled-iso.sh` | Wrapper: sets UNBUNDLED=1 | +| `image-recipe/branding/isohdpfx.bin` | Proven MBR (432 bytes) | +| `image-recipe/branding/grub-theme/` | GRUB theme + background | +| `image-recipe/branding/plymouth-theme/` | Plymouth boot splash | +| `scripts/image-versions.sh` | Pinned container image versions | +| `.gitea/workflows/build-iso-dev.yml` | CI for dev-iso branch | +| `image-recipe/test-iso-qemu.sh` | QEMU test script | +| `image-recipe/dev-branding.sh` | Quick branding iteration | diff --git a/.claude/skills/iso-branding/SKILL.md b/.claude/skills/iso-branding/SKILL.md new file mode 100644 index 00000000..708432ba --- /dev/null +++ b/.claude/skills/iso-branding/SKILL.md @@ -0,0 +1,146 @@ +--- +name: iso-branding +description: Design and implement Archipelago boot visuals — GRUB theme, Plymouth splash, ISOLINUX menu, console banners. Handles pixel-art cyberpunk aesthetic with Bitcoin orange accents. Use when working on boot screen design, splash animations, GRUB backgrounds, or installer UI appearance. +allowed-tools: Bash, Read, Write, Edit, Grep, Glob, Agent +--- + +# ISO Boot Branding — Archipelago + +Design and build the visual boot experience from USB power-on to web UI. + +## Brand Identity + +**Archipelago** = self-sovereign Bitcoin node OS. Floating islands in the sky. + +| Element | Value | +|---------|-------| +| Primary accent | `#fb923c` (Bitcoin orange) | +| Secondary accent | `#f7931a` (deeper orange) | +| Success | `#4ade80` (green) | +| Background | `#0a0a0a` → `#050505` (near-black) | +| Text | `#ffffff` (white), `#aaaaaa` (dim), `#555555` (subtle) | +| Glass | `rgba(255,255,255,0.06)` frost overlay | +| Style | Pixel art cyberpunk, dark glass morphism, CRT scanlines | +| Logo | Pixel-art lowercase "a" (from SVG favicon) | + +## Boot Stages & What's Customizable + +### 1. GRUB Menu (UEFI boot) +- **Background**: `branding/grub-theme/background.png` — any PNG, GRUB scales it +- **Theme**: `branding/grub-theme/theme.txt` — colors, layout, labels +- **Fonts**: Generated with `grub-mkfont` during build, .pf2 format +- **Config**: Written by build script in Step 5 (`grub.cfg` heredoc) + +GRUB theme.txt properties that work: +``` +desktop-color: "#rrggbb" # Fallback if no background +desktop-image: "background.png" # Background image +title-text: "" # Empty = no title + ++ boot_menu { + left/top/width/height = N% + item_color = "#rrggbb" + selected_item_color = "#rrggbb" + item_height = N + item_spacing = N + scrollbar = false +} + ++ label { + left/top/width = N% + text = "string" + color = "#rrggbb" + align = "center" +} +``` + +**IMPORTANT**: Do NOT reference font names in theme.txt unless you know the exact internal name from grub-mkfont output. GRUB falls back to default if a font reference fails, which causes the ENTIRE theme to not load. + +### 2. ISOLINUX Menu (BIOS boot) +- **Config**: Written by build script in Step 5 (`isolinux.cfg` heredoc) +- **Colors**: ANSI-style color codes in `MENU COLOR` directives +- **Title**: `MENU TITLE` string +- Text-only — no background image (use `vesamenu.c32` for graphical, but `menu.c32` is more compatible) + +### 3. Plymouth Splash (kernel boot → login) +- **Theme**: `branding/plymouth-theme/archipelago.script` +- **Logo**: `branding/plymouth-theme/logo.png` (PNG with transparency) +- **Config**: `branding/plymouth-theme/archipelago.plymouth` +- Supports: animated progress bar, logo sprites, LUKS password prompt +- Kernel param `splash` must be present (added to GRUB_CMDLINE_LINUX_DEFAULT) + +Plymouth script language: +```javascript +Window.SetBackgroundTopColor(r, g, b); // 0.0-1.0 +logo = Image("logo.png"); +sprite = Sprite(logo); +sprite.SetX(x); sprite.SetY(y); +Plymouth.SetRefreshFunction(callback); +Plymouth.SetBootProgressFunction(callback); +Plymouth.SetDisplayPasswordFunction(callback); +``` + +### 4. Console Banner (TTY login) +- ASCII art + system info in `/etc/profile.d/archipelago.sh` +- Generated in auto-install.sh (Step 4, the INSTALLER_SCRIPT heredoc) +- Uses ANSI escape codes for color + +### 5. Installer Prompt +- "ARCHIPELAGO BITCOIN NODE OS / Automatic Installer" +- In the systemd service wrapper: `/usr/local/bin/archipelago-start-installer` +- Built inside the debootstrap container in Step 2 + +## Dev Workflow + +### Quick preview (no ISO needed) +```bash +# Edit background, see it instantly: +open image-recipe/branding/grub-theme/background.png + +# Generate procedural background: +python3 image-recipe/branding/generate-grub-background.py /tmp/bg.png && open /tmp/bg.png + +# Generate Plymouth logo: +python3 image-recipe/branding/generate-plymouth-logo.py /tmp/logo.png && open /tmp/logo.png +``` + +### Full boot test (needs base ISO) +```bash +./image-recipe/dev-branding.sh [path-to-iso] +# Or via dev-start.sh option 0 +``` +Extracts ISO → patches branding → repackages → boots QEMU. ~30 seconds. + +### What to edit +| File | Affects | +|------|---------| +| `branding/grub-theme/background.png` | GRUB boot screen image | +| `branding/grub-theme/theme.txt` | GRUB menu colors, layout | +| `branding/plymouth-theme/logo.png` | Plymouth boot logo | +| `branding/plymouth-theme/archipelago.script` | Plymouth animation/progress | +| `branding/generate-grub-background.py` | Procedural background generator | +| `branding/generate-plymouth-logo.py` | Procedural logo generator | + +## Image Specs + +| Asset | Format | Size | Notes | +|-------|--------|------|-------| +| GRUB background | PNG | 1024x768 recommended | GRUB scales any size, but large images slow boot | +| Plymouth logo | PNG (RGBA) | 256x256 recommended | Transparent background | +| GRUB fonts | .pf2 | Generated | `grub-mkfont -s SIZE -o out.pf2 input.ttf` | + +## Build Integration + +GRUB theme is installed in Step 2 (after artifacts placed): +- Static `background.png` copied from `branding/grub-theme/` +- Falls back to Python generator if static file missing +- Fonts generated in debootstrap container with `grub-mkfont` + +Plymouth theme installed in Step 3 (component copy) + Step 4 (auto-install.sh): +- Files copied to `$ARCH_DIR/plymouth-theme/` in ISO +- Auto-install.sh copies to target at `/usr/share/plymouth/themes/archipelago/` +- Sets as default via `plymouth-set-default-theme` + +GRUB theme also installed on TARGET system (not just installer): +- Auto-install.sh copies theme to `/mnt/target/boot/grub/themes/archipelago/` +- Adds `GRUB_THEME=` to `/mnt/target/etc/default/grub` diff --git a/.claude/skills/iso-debug/SKILL.md b/.claude/skills/iso-debug/SKILL.md new file mode 100644 index 00000000..99426a3b --- /dev/null +++ b/.claude/skills/iso-debug/SKILL.md @@ -0,0 +1,175 @@ +--- +name: iso-debug +description: Diagnose and fix Archipelago ISO boot failures. Covers hybrid MBR/GPT, UEFI/BIOS boot chains, live-boot initramfs, GRUB/ISOLINUX configuration, xorriso packaging, and USB boot compatibility. Use when ISO doesn't boot, installer doesn't start, kernel panics, or USB isn't recognized by BIOS/UEFI. +allowed-tools: Bash, Read, Grep, Glob, Agent, Edit +--- + +# ISO Boot Debugging — Archipelago Custom Base + +Systematic diagnosis of ISO boot failures for the Archipelago debootstrap-based installer. + +## Architecture + +The ISO boot chain has 5 stages. Failure at any stage has distinct symptoms: + +| Stage | Component | Symptom if broken | +|-------|-----------|-------------------| +| 1. BIOS/UEFI recognition | Hybrid MBR + GPT | USB not in boot menu at all | +| 2. Bootloader | ISOLINUX (BIOS) or GRUB EFI (UEFI) | Black screen after selecting USB | +| 3. Kernel + initramfs | vmlinuz + initrd.img with live-boot | Kernel panic or initramfs shell | +| 4. Root filesystem | live-boot mounts filesystem.squashfs | "No root device" or blank screen | +| 5. Installer | systemd service + auto-install.sh | Boots to shell but no installer prompt | + +## Stage 1: USB Not Recognized + +**Most common cause**: Wrong MBR code in the ISO hybrid boot sector. + +### Diagnosis +```bash +# Compare first 16 bytes of working vs broken ISO +xxd -l 16 working.iso +xxd -l 16 broken.iso + +# Check for valid boot signature at offset 510 +xxd -s 510 -l 2 broken.iso +# Must show: 55aa +``` + +### Known MBR codes +- `4552` — Debian Live MBR (extracted from Debian Live ISO). **Works on all tested hardware.** +- `33ed` — ISOLINUX package generic isohdpfx.bin. **Does NOT work on some UEFI hardware.** + +### Fix +The project ships the proven MBR at `image-recipe/branding/isohdpfx.bin` (432 bytes, starts with `4552`). +Build script uses it via: `-isohybrid-mbr "$SCRIPT_DIR/branding/isohdpfx.bin"` + +### xorriso flags that matter +- `-isohybrid-mbr ` — Embeds MBR code for USB hybrid boot +- `-isohybrid-gpt-basdat` — Adds GPT partition entry for EFI (REQUIRED for UEFI USB boot) +- `-partition_offset 16` — Reserves space for GPT table (REQUIRED — without this some UEFI firmware won't see the USB) +- `-eltorito-alt-boot -e boot/grub/efi.img -no-emul-boot` — EFI boot catalog entry + +### Balena Etcher +Writes raw ISO to USB — no special formatting. If the ISO boots in QEMU but not on hardware, the MBR code is the issue, not Etcher. + +## Stage 2: Bootloader Failure + +### BIOS path: ISOLINUX +Required files in ISO: `isolinux/isolinux.bin`, `isolinux/ldlinux.c32`, `isolinux/boot.cat` +Config: `isolinux/isolinux.cfg` + +### UEFI path: GRUB +Required files: `EFI/BOOT/BOOTX64.EFI`, `boot/grub/efi.img`, `boot/grub/grub.cfg` +The EFI image is a FAT32 filesystem containing the GRUB binary, built with: +```bash +grub-mkimage -O x86_64-efi -o BOOTX64.EFI -p /boot/grub \ + part_gpt part_msdos fat iso9660 udf normal boot linux search \ + search_fs_uuid search_fs_file search_label configfile echo cat \ + ls test true loopback gfxterm gfxmenu font png all_video video \ + video_bochs video_cirrus efi_gop efi_uga +``` +**Critical**: `all_video`, `efi_gop`, `efi_uga` needed for display on real hardware. + +### Diagnosis +```bash +# Mount ISO and verify files +sudo mount -o loop,ro broken.iso /mnt +ls -la /mnt/isolinux/ +ls -la /mnt/EFI/BOOT/ +cat /mnt/boot/grub/grub.cfg +cat /mnt/isolinux/isolinux.cfg +sudo umount /mnt +``` + +## Stage 3: Kernel / Initramfs + +### live-boot +The initramfs must contain live-boot hooks. Without them, the kernel boots but can't find root. + +**Kernel params required**: `boot=live components` +- `boot=live` — triggers live-boot's initramfs scripts +- `components` — tells live-boot to scan live/ for squashfs files + +### Verify initramfs has live-boot +```bash +TMPDIR=$(mktemp -d) +unmkinitramfs /path/to/initrd.img $TMPDIR +# live-boot installs scripts/live as a FILE (not directory) +ls -la $TMPDIR/scripts/live # or $TMPDIR/main/scripts/live +file $TMPDIR/scripts/live # Should say "ASCII text" +``` + +### Common initramfs failures +1. **live-boot not installed**: debootstrap `--include` can't resolve its deps. Must install via `chroot apt-get` after debootstrap. +2. **Broken initramfs from container build**: `update-initramfs` needs `/proc`, `/sys`, `/dev` mounted in the chroot. +3. **scripts/live is a FILE not directory**: Verification code must use `[ -e ]` not `[ -d ]`. + +## Stage 4: Root Filesystem + +live-boot searches for squashfs files in `live/` on the boot media. +- Mounts boot media (USB/CDROM) at `/run/live/medium` +- Finds `live/filesystem.squashfs` +- Mounts it read-only, creates tmpfs overlay +- pivot_root into the combined root + +### Diagnosis +If you get an initramfs shell prompt `(initramfs)`: +```bash +# Inside initramfs shell: +ls /run/live/medium/ # Is boot media mounted? +ls /run/live/medium/live/ # Is squashfs there? +cat /proc/cmdline # Does it have boot=live? +``` + +## Stage 5: Installer Not Starting + +The installer auto-starts via: +1. Getty auto-login on tty1 (root, no password) +2. systemd service `archipelago-installer.service` +3. Wrapper script searches for boot media at: `/run/live/medium`, `/run/archiso`, `/cdrom` + +### Diagnosis +If you get a shell but no installer prompt: +```bash +systemctl status archipelago-installer.service +cat /usr/local/bin/archipelago-start-installer +ls /run/live/medium/archipelago/auto-install.sh +``` + +## Quick Verification Checklist + +Run against any ISO before flashing: +```bash +ISO=path/to/iso +MNT=$(mktemp -d) +sudo mount -o loop,ro $ISO $MNT + +echo "=== MBR ===" && xxd -l 4 $ISO +echo "=== Boot sig ===" && xxd -s 510 -l 2 $ISO +echo "=== Files ===" && for f in live/vmlinuz live/initrd.img live/filesystem.squashfs isolinux/isolinux.bin EFI/BOOT/BOOTX64.EFI boot/grub/grub.cfg archipelago/auto-install.sh; do [ -e $MNT/$f ] && echo "OK: $f" || echo "MISSING: $f"; done +echo "=== Kernel params ===" && grep "boot=live" $MNT/boot/grub/grub.cfg && echo OK || echo MISSING +echo "=== live-boot ===" && INITRD=$(mktemp -d) && unmkinitramfs $MNT/live/initrd.img $INITRD 2>/dev/null && ([ -e $INITRD/scripts/live ] && echo "OK" || echo "MISSING") + +sudo umount $MNT +``` + +## Key Files + +| File | Purpose | +|------|---------| +| `image-recipe/build-auto-installer-iso.sh` | Main build script (~2600 lines) | +| `image-recipe/branding/isohdpfx.bin` | Proven MBR code (432 bytes) | +| `image-recipe/branding/grub-theme/` | GRUB theme (theme.txt + background.png) | +| `image-recipe/branding/plymouth-theme/` | Plymouth boot splash | +| `.gitea/workflows/build-iso-dev.yml` | CI workflow with smoke test | +| `image-recipe/test-iso-qemu.sh` | QEMU testing script | +| `image-recipe/dev-branding.sh` | Quick branding iteration (patch + repackage) | + +## Infrastructure + +| What | Where | +|------|-------| +| CI runner | gitea-runner.service on 192.168.1.228 | +| ISO builds | FileBrowser at http://192.168.1.228:8083 → Builds/ | +| Dev branch | dev-iso (separate CI: build-iso-dev.yml) | +| Main branch | main (CI: build-iso.yml) — DO NOT break | diff --git a/.claude/skills/iso-debug/references/boot-chain-reference.md b/.claude/skills/iso-debug/references/boot-chain-reference.md new file mode 100644 index 00000000..af92f101 --- /dev/null +++ b/.claude/skills/iso-debug/references/boot-chain-reference.md @@ -0,0 +1,367 @@ +# Custom Debian ISO Boot Chain — Technical Reference + +Expert reference for building and debugging custom bootable Debian-based ISOs. +Covers hybrid MBR/GPT, live-boot, debootstrap, GRUB, ISOLINUX, Plymouth, and xorriso. + +--- + +## 1. Hybrid MBR/GPT for USB Boot + +### What is isohdpfx.bin? +The first 432 bytes of a hybrid-bootable ISO. Contains the Master Boot Record code that BIOS firmware executes when booting from USB. Different sources produce different MBR code: + +| Source | First bytes | Compatibility | +|--------|-------------|---------------| +| Debian Live ISO (`dd if=debian-live.iso bs=1 count=432`) | `45 52` | Best — works on all tested hardware | +| `/usr/lib/ISOLINUX/isohdpfx.bin` | `33 ed` | Generic — fails on some UEFI hardware | +| Manually built with `isohybrid` | Varies | Unpredictable | + +**Rule**: Always extract MBR from a known-working ISO. Never rely on the generic ISOLINUX one. + +### xorriso flags for hybrid boot +```bash +xorriso -as mkisofs -o output.iso \ + -isohybrid-mbr isohdpfx.bin \ # Embeds MBR for BIOS USB boot + -c isolinux/boot.cat \ # El Torito boot catalog + -b isolinux/isolinux.bin \ # BIOS bootloader + -no-emul-boot -boot-load-size 4 -boot-info-table \ + -eltorito-alt-boot \ # Second boot entry (EFI) + -e boot/grub/efi.img \ # EFI boot image + -no-emul-boot \ + -isohybrid-gpt-basdat \ # Adds GPT partition for EFI + -partition_offset 16 \ # Space for GPT table — REQUIRED for UEFI + /path/to/iso/contents +``` + +**Critical flags**: +- `-isohybrid-gpt-basdat`: Without this, UEFI firmware won't see the EFI partition +- `-partition_offset 16`: Reserves 16 sectors for GPT. Without it, some UEFI firmware ignores the USB entirely +- `-isohybrid-mbr`: Without this, the ISO won't boot from USB at all (only CD-ROM) + +### Balena Etcher +Writes the ISO byte-for-byte to USB — no reformatting, no special partition creation. If the ISO works with `dd`, it works with Etcher. If BIOS doesn't see the USB, the MBR code is wrong, not Etcher. + +### Verifying hybrid structure +```bash +xxd -l 4 image.iso # MBR code (should be 45 52 for Debian Live) +xxd -s 510 -l 2 image.iso # Boot signature (must be 55 aa) +xxd -s 512 -l 8 image.iso # GPT signature at LBA 1 (should be "EFI PART") +file image.iso # Should say "DOS/MBR boot sector" and "bootable" +``` + +--- + +## 2. live-boot Package + +### What it does +Provides initramfs hooks that mount a squashfs file as the root filesystem using overlayfs. This is how every Debian/Ubuntu live ISO works. + +Boot flow: kernel → initramfs → live-boot scripts → find squashfs → mount overlayfs → pivot_root → systemd + +### Package structure +- `live-boot` (~29KB): Main package, boot scripts +- `live-boot-initramfs-tools` (~6KB): Initramfs hooks that get baked into initrd.img + +**Critical**: `scripts/live` is a **FILE**, not a directory. Verification must use `[ -e ]` not `[ -d ]`. + +### Kernel parameters +| Parameter | Required | Effect | +|-----------|----------|--------| +| `boot=live` | YES | Activates live-boot's initramfs hooks | +| `components` | YES | Scans live/ for additional squashfs modules | +| `toram` | No | Copies squashfs to RAM (faster, allows USB removal) | +| `persistence` | No | Enables writable overlay on a partition labeled "persistence" | +| `quiet` | No | Suppresses boot messages | +| `splash` | No | Enables Plymouth splash screen | +| `console=ttyS0,115200` | No | Serial console for QEMU debugging | + +### Where live-boot mounts things +- `/run/live/medium` — The boot media (USB/CDROM) mount point +- `/run/live/rootfs/filesystem.squashfs` — The mounted squashfs +- `/run/live/overlay` — The tmpfs overlay for writes + +### Verifying live-boot in initramfs +```bash +TMPDIR=$(mktemp -d) +unmkinitramfs /path/to/initrd.img $TMPDIR +# Check for live-boot scripts +file $TMPDIR/scripts/live # Should be "ASCII text" +# OR (some initramfs have main/ prefix) +file $TMPDIR/main/scripts/live +``` + +### Common failures +1. **live-boot not in initrd**: Installed in rootfs but initramfs not regenerated after +2. **Missing kernel params**: `boot=live` not in GRUB/ISOLINUX config +3. **Broken initramfs**: Built without /proc /sys /dev mounted in chroot +4. **Wrong verification**: `[ -d scripts/live ]` fails because it's a file + +--- + +## 3. debootstrap for Installer Environments + +### Variants +- `--variant=minbase`: Absolute minimum (~150MB). Only essential + apt. Good for installer squashfs. +- Default (no variant): Full base system (~300MB). More packages, fewer missing deps. + +### --include limitations +debootstrap's minbase resolver is simplified and **cannot resolve complex dependency chains**. Packages like `live-boot` that depend on `initramfs-tools` which depends on many other packages will silently fail or be skipped. + +**Fix**: Install complex packages via `chroot apt-get` after debootstrap completes: +```bash +debootstrap --variant=minbase --include=basic,packages bookworm /installer http://deb.debian.org/debian +# Then: +mount --bind /proc /installer/proc +mount --bind /sys /installer/sys +mount --bind /dev /installer/dev +chroot /installer apt-get update +chroot /installer apt-get install -y live-boot live-boot-initramfs-tools +umount /installer/dev /installer/sys /installer/proc +``` + +### Initramfs generation inside containers +`update-initramfs` REQUIRES `/proc`, `/sys`, `/dev` to be mounted in the chroot. Without them: +- Module detection fails (can't read /proc/modules) +- Device nodes missing (can't detect hardware) +- The resulting initramfs boots but can't load kernel modules + +### Container-in-container considerations +When running debootstrap inside a Podman/Docker container on a CI runner: +- `--privileged` flag needed for chroot to work +- The container runtime may kill the container after debootstrap exits if using `set -e` +- proc/sys/dev mounts inside the debootstrapped chroot work fine with `--privileged` + +--- + +## 4. GRUB Theming + +### theme.txt format +``` +desktop-color: "#0a0a0a" # Fallback background color +desktop-image: "background.png" # Background image (any PNG, GRUB scales) +title-text: "" # Empty = hide title + ++ boot_menu { + left = 25% + top = 40% + width = 50% + height = 30% + item_color = "#aaaaaa" # Normal menu item color + selected_item_color = "#fb923c" # Selected item color + item_height = 36 + item_spacing = 8 + scrollbar = false +} + ++ label { + left = 25% + top = 20% + width = 50% + text = "Some Text" + color = "#f7931a" + align = "center" +} +``` + +**IMPORTANT**: Do NOT specify `font = "Name Size"` in theme elements unless you know the exact internal font name. If GRUB can't find the font, the ENTIRE theme fails to load and you get the ugly default. + +### Font handling +```bash +# Generate .pf2 font file +grub-mkfont -s 16 -o dejavu_16.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf + +# In grub.cfg, load fonts BEFORE setting theme: +loadfont /boot/grub/font.pf2 +loadfont /boot/grub/themes/archipelago/dejavu_16.pf2 +set theme=/boot/grub/themes/archipelago/theme.txt +``` + +### Background images +- Any PNG works, GRUB scales to screen resolution +- Smaller images (1024x768) load faster +- Large images (3000x2000+) add seconds to boot and may fail on limited GRUB heap + +### grub-mkimage — essential modules for ISO boot +```bash +grub-mkimage -O x86_64-efi -o BOOTX64.EFI -p /boot/grub \ + part_gpt part_msdos fat iso9660 udf \ # Filesystem access + normal boot linux search search_fs_uuid search_fs_file search_label \ + configfile echo cat ls test true \ # Basic commands + loopback \ # Loop device support + gfxterm gfxmenu font png \ # Graphical display + all_video video video_bochs video_cirrus \ # Video drivers + efi_gop efi_uga # EFI display protocols +``` + +Missing `all_video`/`efi_gop` = black screen on real hardware (works in QEMU). + +### EFI boot image creation +```bash +dd if=/dev/zero of=efi.img bs=1M count=4 +mkfs.vfat efi.img +mmd -i efi.img ::/EFI ::/EFI/BOOT +mcopy -i efi.img BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI +``` + +--- + +## 5. Plymouth Boot Splash + +### Theme types +- **script**: Most flexible. Lua-like scripting with sprites, animations, callbacks. +- **two-step**: Simple logo + spinner. Less customizable but easier. +- **fade-in**: Logo fades in. Minimal. + +### Script theme structure +``` +/usr/share/plymouth/themes/mytheme/ + mytheme.plymouth # Theme metadata + mytheme.script # Animation script + logo.png # Logo image (PNG with alpha) +``` + +### mytheme.plymouth +```ini +[Plymouth Theme] +Name=MyTheme +Description=Custom boot splash +ModuleName=script + +[script] +ImageDir=/usr/share/plymouth/themes/mytheme +ScriptFile=/usr/share/plymouth/themes/mytheme/mytheme.script +``` + +### Script language key functions +```javascript +Window.SetBackgroundTopColor(r, g, b); // 0.0-1.0 floats +Window.SetBackgroundBottomColor(r, g, b); +image = Image("logo.png"); +sprite = Sprite(image); +sprite.SetX(x); sprite.SetY(y); sprite.SetOpacity(0.0-1.0); +Plymouth.SetRefreshFunction(fn); // Called every frame +Plymouth.SetBootProgressFunction(fn); // fn(duration, progress) +Plymouth.SetDisplayPasswordFunction(fn); // fn(prompt, bullets) +Plymouth.SetQuitFunction(fn); +screen_w = Window.GetWidth(); +screen_h = Window.GetHeight(); +``` + +### Setting default theme +```bash +plymouth-set-default-theme mytheme +# OR manually: +ln -sf /usr/share/plymouth/themes/mytheme/mytheme.plymouth /etc/alternatives/default.plymouth +``` + +### Kernel params +- `splash` in GRUB_CMDLINE_LINUX_DEFAULT enables Plymouth +- `quiet` suppresses text that would overlay Plymouth + +--- + +## 6. ISOLINUX/SYSLINUX + +### Required files +| File | Source | Purpose | +|------|--------|---------| +| `isolinux.bin` | `/usr/lib/ISOLINUX/isolinux.bin` | BIOS bootloader | +| `ldlinux.c32` | `/usr/lib/syslinux/modules/bios/ldlinux.c32` | Core library (REQUIRED) | +| `menu.c32` | `/usr/lib/syslinux/modules/bios/menu.c32` | Text menu UI | +| `libutil.c32` | `/usr/lib/syslinux/modules/bios/libutil.c32` | Utility library | +| `boot.cat` | Auto-generated by xorriso | El Torito boot catalog | +| `isohdpfx.bin` | Extracted from working ISO | Hybrid MBR code | + +### Configuration (isolinux.cfg) +``` +UI menu.c32 +PROMPT 0 +TIMEOUT 50 # 5 seconds (units of 1/10 second) +DEFAULT install + +MENU TITLE MY INSTALLER +MENU COLOR border 30;44 #40ffffff #00000000 std +MENU COLOR title 1;36;44 #ff00b7ff #00000000 std +MENU COLOR sel 7;37;40 #ffffffff #ff333333 std +MENU COLOR unsel 37;44 #ffaaaaaa #00000000 std + +LABEL install + MENU LABEL Install System + KERNEL /live/vmlinuz + APPEND initrd=/live/initrd.img boot=live components quiet + MENU DEFAULT +``` + +### menu.c32 vs vesamenu.c32 +- `menu.c32`: Text-mode menu. More compatible, no background image. +- `vesamenu.c32`: VESA graphical menu. Supports background PNG, but some hardware/VMs don't support VESA. + +--- + +## 7. Testing Without Real Hardware + +### QEMU UEFI boot +```bash +qemu-system-x86_64 \ + -machine q35 \ + -drive if=pflash,format=raw,readonly=on,file=/path/to/OVMF_CODE.fd \ + -m 4G -smp 2 \ + -boot d -cdrom image.iso \ + -drive if=virtio,format=qcow2,file=test-disk.qcow2 \ + -vga virtio -display default +``` + +### QEMU BIOS boot (sees ISOLINUX) +```bash +qemu-system-x86_64 \ + -machine pc \ + -m 4G -smp 2 \ + -boot d -cdrom image.iso \ + -drive if=virtio,format=qcow2,file=test-disk.qcow2 \ + -vga virtio -display default +``` + +### Serial console capture +Add to QEMU: `-serial file:/tmp/serial.log` +Add to kernel params: `console=ttyS0,115200 console=tty0` + +### ISO structure verification (no boot required) +```bash +MNT=$(mktemp -d) +sudo mount -o loop,ro image.iso $MNT + +# Check all critical files +for f in live/vmlinuz live/initrd.img live/filesystem.squashfs \ + isolinux/isolinux.bin EFI/BOOT/BOOTX64.EFI boot/grub/grub.cfg; do + [ -e $MNT/$f ] && echo "OK: $f" || echo "MISSING: $f" +done + +# Check initramfs for live-boot +INITRD=$(mktemp -d) +unmkinitramfs $MNT/live/initrd.img $INITRD +[ -e $INITRD/scripts/live ] && echo "live-boot: OK" || echo "live-boot: MISSING" + +# Check kernel params +grep "boot=live" $MNT/boot/grub/grub.cfg && echo "params: OK" + +sudo umount $MNT +``` + +--- + +## 8. Security Considerations for Custom ISOs + +### Supply chain +- Pin the Debian mirror URL (don't use redirectors in production) +- Verify package signatures (debootstrap does this by default) +- Pin kernel and GRUB package versions for reproducibility + +### Installer security +- Auto-install.sh runs as root — validate all inputs before path construction +- LUKS key generation must use CSPRNG (`/dev/urandom`, never `/dev/random` which blocks) +- Drop the LUKS key file after writing to crypttab (or store in root-only location with 0400) + +### Boot security +- Secure Boot requires signed GRUB EFI binary (shim-signed package) +- Without Secure Boot, the unsigned BOOTX64.EFI works but users must disable Secure Boot in BIOS +- The MBR code (isohdpfx.bin) is not signed — Secure Boot only validates EFI path diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 6b9c34b4..21133a97 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -637,16 +637,25 @@ umount /installer/proc 2>/dev/null || true echo " [container] Creating installer squashfs..." mksquashfs /installer /output/filesystem.squashfs -comp xz -Xbcj x86 -noappend -quiet -# Build GRUB EFI image +# Build GRUB EFI image with embedded bootstrap config (grub-mkstandalone) +# This ensures GRUB can find its config on real hardware, not just QEMU echo " [container] Building GRUB EFI image..." -grub-mkimage -O x86_64-efi -o /output/BOOTX64.EFI -p /boot/grub \ - part_gpt part_msdos fat iso9660 udf normal boot linux search \ - search_fs_uuid search_fs_file search_label configfile echo cat \ - ls test true loopback gfxterm gfxmenu font png all_video video \ - video_bochs video_cirrus efi_gop efi_uga +cat > /tmp/grub-embed.cfg </dev/null +grub-mkstandalone -O x86_64-efi \ + --modules="part_gpt part_msdos fat iso9660 all_video font gfxterm" \ + --locales="" \ + --themes="" \ + --fonts="" \ + --output=/output/BOOTX64.EFI \ + "boot/grub/grub.cfg=/tmp/grub-embed.cfg" + +# Create EFI FAT image (20MB — includes GRUB binary + grub.cfg) +dd if=/dev/zero of=/output/efi.img bs=1M count=20 2>/dev/null mkfs.vfat /output/efi.img >/dev/null mmd -i /output/efi.img ::/EFI ::/EFI/BOOT mcopy -i /output/efi.img /output/BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI @@ -683,7 +692,7 @@ cp "$WORK_DIR/vmlinuz" "$INSTALLER_ISO/live/vmlinuz" cp "$WORK_DIR/initrd.img" "$INSTALLER_ISO/live/initrd.img" cp "$WORK_DIR/filesystem.squashfs" "$INSTALLER_ISO/live/filesystem.squashfs" cp "$WORK_DIR/BOOTX64.EFI" "$INSTALLER_ISO/EFI/BOOT/BOOTX64.EFI" -cp "$WORK_DIR/efi.img" "$INSTALLER_ISO/boot/grub/efi.img" +# Note: efi.img stays in $WORK_DIR — it gets appended as GPT partition 2 by xorriso cp "$WORK_DIR/isolinux.bin" "$INSTALLER_ISO/isolinux/isolinux.bin" cp "$WORK_DIR/ldlinux.c32" "$INSTALLER_ISO/isolinux/ldlinux.c32" cp "$WORK_DIR/menu.c32" "$INSTALLER_ISO/isolinux/menu.c32" 2>/dev/null || true @@ -2501,11 +2510,19 @@ echo "Step 5: Configuring boot loaders..." # Create GRUB configuration echo " Writing GRUB config..." cat > "$INSTALLER_ISO/boot/grub/grub.cfg" <<'GRUBCFG' +insmod part_gpt +insmod part_msdos +insmod fat +insmod iso9660 +insmod all_video + +search --no-floppy --set=root --label ARCHIPELAGO + set timeout=5 set default=0 # Load font for graphical menu -if loadfont /boot/grub/font.pf2; then +if loadfont ($root)/boot/grub/font.pf2; then set gfxmode=auto insmod gfxterm insmod png @@ -2513,26 +2530,25 @@ if loadfont /boot/grub/font.pf2; then fi # Archipelago GRUB theme -if [ -f /boot/grub/themes/archipelago/theme.txt ]; then - # Load theme fonts - loadfont /boot/grub/themes/archipelago/dejavu_12.pf2 - loadfont /boot/grub/themes/archipelago/dejavu_14.pf2 - loadfont /boot/grub/themes/archipelago/dejavu_16.pf2 - loadfont /boot/grub/themes/archipelago/dejavu_24.pf2 - set theme=/boot/grub/themes/archipelago/theme.txt +if [ -f ($root)/boot/grub/themes/archipelago/theme.txt ]; then + loadfont ($root)/boot/grub/themes/archipelago/dejavu_12.pf2 + loadfont ($root)/boot/grub/themes/archipelago/dejavu_14.pf2 + loadfont ($root)/boot/grub/themes/archipelago/dejavu_16.pf2 + loadfont ($root)/boot/grub/themes/archipelago/dejavu_24.pf2 + set theme=($root)/boot/grub/themes/archipelago/theme.txt else set menu_color_normal=light-gray/black set menu_color_highlight=white/dark-gray fi menuentry "Install Archipelago" --hotkey=i { - linux /live/vmlinuz boot=live components quiet console=ttyS0,115200 console=tty0 - initrd /live/initrd.img + linux ($root)/live/vmlinuz boot=live components quiet console=ttyS0,115200 console=tty0 + initrd ($root)/live/initrd.img } menuentry "Install Archipelago (verbose)" --hotkey=v { - linux /live/vmlinuz boot=live components - initrd /live/initrd.img + linux ($root)/live/vmlinuz boot=live components console=ttyS0,115200 console=tty0 + initrd ($root)/live/initrd.img } menuentry "Boot from local disk" --hotkey=b { @@ -2541,6 +2557,14 @@ menuentry "Boot from local disk" --hotkey=b { } GRUBCFG +# Copy grub.cfg to EFI/BOOT on ISO filesystem AND into the FAT EFI image +# The embedded grub bootstrap does configfile "${cmdpath}/grub.cfg" +cp "$INSTALLER_ISO/boot/grub/grub.cfg" "$INSTALLER_ISO/EFI/BOOT/grub.cfg" +if [ -f "$WORK_DIR/efi.img" ]; then + mcopy -oi "$WORK_DIR/efi.img" "$INSTALLER_ISO/boot/grub/grub.cfg" ::/EFI/BOOT/grub.cfg 2>/dev/null || \ + echo " WARNING: Could not copy grub.cfg into efi.img (mtools required)" +fi + # Create ISOLINUX configuration (legacy BIOS boot) echo " Writing ISOLINUX config..." cat > "$INSTALLER_ISO/isolinux/isolinux.cfg" <<'ISOCFG' @@ -2607,35 +2631,44 @@ if [ ! -f "$ISOHDPFX" ]; then done fi -# EFI boot image was built in Step 2 -EFI_IMG="$INSTALLER_ISO/boot/grub/efi.img" +# EFI boot image was built in Step 2 and placed at staging/efiboot.img +# The Will Haley / Debian live-build approach: append EFI as GPT partition 2 +# This is what makes USB boot work on real UEFI hardware (not just QEMU) +EFIBOOT="$WORK_DIR/efi.img" -if [ ! -f "$EFI_IMG" ]; then +if [ ! -f "$EFIBOOT" ]; then echo " WARNING: No EFI boot image — ISO will only support Legacy BIOS boot" xorriso -as mkisofs -o "$OUTPUT_ISO" \ -volid "ARCHIPELAGO" \ -iso-level 3 \ - -J -joliet-long -R \ + -full-iso9660-filenames \ + --mbr-force-bootable -partition_offset 16 \ + -joliet -joliet-long -rational-rock \ -isohybrid-mbr "$ISOHDPFX" \ - -c isolinux/boot.cat \ - -b isolinux/isolinux.bin \ - -no-emul-boot -boot-load-size 4 -boot-info-table \ - -partition_offset 16 \ + -eltorito-boot isolinux/isolinux.bin \ + -no-emul-boot \ + -boot-load-size 4 \ + -boot-info-table \ + --eltorito-catalog isolinux/isolinux.cat \ "$INSTALLER_ISO" else xorriso -as mkisofs -o "$OUTPUT_ISO" \ -volid "ARCHIPELAGO" \ -iso-level 3 \ - -J -joliet-long -R \ + -full-iso9660-filenames \ + --mbr-force-bootable -partition_offset 16 \ + -joliet -joliet-long -rational-rock \ -isohybrid-mbr "$ISOHDPFX" \ - -c isolinux/boot.cat \ - -b isolinux/isolinux.bin \ - -no-emul-boot -boot-load-size 4 -boot-info-table \ + -eltorito-boot isolinux/isolinux.bin \ + -no-emul-boot \ + -boot-load-size 4 \ + -boot-info-table \ + --eltorito-catalog isolinux/isolinux.cat \ -eltorito-alt-boot \ - -e boot/grub/efi.img \ - -no-emul-boot \ - -isohybrid-gpt-basdat \ - -partition_offset 16 \ + -e --interval:appended_partition_2:all:: \ + -no-emul-boot \ + -isohybrid-gpt-basdat \ + -append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B "$EFIBOOT" \ "$INSTALLER_ISO" fi diff --git a/image-recipe/dev-branding.sh b/image-recipe/dev-branding.sh index 53d722e9..57bef6a4 100755 --- a/image-recipe/dev-branding.sh +++ b/image-recipe/dev-branding.sh @@ -1,173 +1,207 @@ #!/bin/bash # -# Quick-iterate on boot branding without rebuilding the ISO. +# Boot branding dev — iterate on GRUB theme, Plymouth, and installer visuals +# without rebuilding the ISO. Patches an existing ISO and boots in QEMU. # # Usage: -# ./dev-branding.sh +# ./dev-branding.sh [path-to-iso] # -# What it does: -# 1. Regenerates GRUB background and Plymouth logo from Python scripts -# 2. Extracts the existing ISO -# 3. Swaps in updated branding files (theme, background, Plymouth) -# 4. Repackages as a new ISO -# 5. Boots it in QEMU for testing -# -# This takes ~10 seconds instead of 20 minutes. -# -# For design-only iteration (no QEMU boot): -# python3 branding/generate-grub-background.py /tmp/grub-bg.png && open /tmp/grub-bg.png +# If no ISO is found locally, downloads the latest from the build server. +# Edit files in branding/, re-run, see changes in ~10 seconds. # set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -ISO="${1:-}" - -if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then - # Auto-detect latest dev ISO on Desktop - ISO=$(ls -t ~/Desktop/archipelago-dev-*.iso 2>/dev/null | head -1) -fi -if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then - ISO=$(ls -t "$SCRIPT_DIR/results/archipelago-*.iso" 2>/dev/null | head -1) -fi -if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then - echo "No ISO found. Provide a path or place one on Desktop/results." - echo "Usage: $0 " - exit 1 -fi - +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" WORK="/tmp/archipelago-dev-branding" PATCHED="$SCRIPT_DIR/results/archipelago-dev-patched.iso" +CACHED_ISO="$SCRIPT_DIR/results/archipelago-dev-base.iso" +DEV_SERVER="archipelago@192.168.1.228" +SSH_KEY="$HOME/.ssh/archipelago-deploy" -echo "=== Archipelago Branding Dev ===" -echo " Base ISO: $ISO" +echo "" +echo " Archipelago Boot Branding Dev" echo "" -# Step 1: Regenerate assets -echo "[1/4] Generating assets..." -python3 "$SCRIPT_DIR/branding/generate-grub-background.py" /tmp/grub-bg.png 2>/dev/null && \ - echo " GRUB background: OK" || echo " GRUB background: FAILED" -python3 "$SCRIPT_DIR/branding/generate-plymouth-logo.py" /tmp/plymouth-logo.png 2>/dev/null && \ - echo " Plymouth logo: OK" || echo " Plymouth logo: FAILED" +# --- Find or download an ISO --- +ISO="${1:-}" -# Also show the background for quick visual check -if command -v open >/dev/null 2>&1; then - open /tmp/grub-bg.png 2>/dev/null & +# Search locally +if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then + for pattern in \ + "$HOME/Desktop/archipelago-dev-"*.iso \ + "$HOME/Desktop/archipelago-unbundled-"*.iso \ + "$HOME/Desktop/archipelago-"*.iso \ + "$SCRIPT_DIR/results/archipelago-dev-base.iso" \ + "$SCRIPT_DIR/results/archipelago-"*.iso; do + found=$(ls -t $pattern 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found" ]; then + ISO="$found" + break + fi + done fi -# Step 2: Extract ISO -echo "[2/4] Extracting ISO..." +# Download from server if not found +if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then + echo " No ISO found locally. Downloading latest from build server..." + REMOTE_ISO=$(ssh -i "$SSH_KEY" "$DEV_SERVER" \ + "ls -t /var/lib/archipelago/filebrowser/Builds/archipelago-dev-*.iso 2>/dev/null | head -1" 2>/dev/null) + if [ -z "$REMOTE_ISO" ]; then + REMOTE_ISO=$(ssh -i "$SSH_KEY" "$DEV_SERVER" \ + "ls -t /var/lib/archipelago/filebrowser/Builds/archipelago-unbundled-*.iso 2>/dev/null | head -1" 2>/dev/null) + fi + if [ -n "$REMOTE_ISO" ]; then + mkdir -p "$SCRIPT_DIR/results" + echo " Downloading: $(basename "$REMOTE_ISO")..." + scp -i "$SSH_KEY" "$DEV_SERVER:$REMOTE_ISO" "$CACHED_ISO" + ISO="$CACHED_ISO" + echo " Saved to: $ISO" + else + echo " No ISO on server either. Run a CI build first." + echo " Or place an ISO on your Desktop." + exit 1 + fi +fi + +echo " Base ISO: $(basename "$ISO") ($(du -h "$ISO" | cut -f1))" +echo "" + +# --- Extract ISO --- +echo " [1/3] Extracting ISO..." +if [ -d "$WORK" ]; then + chmod -R u+w "$WORK" 2>/dev/null || true +fi rm -rf "$WORK" mkdir -p "$WORK" + xorriso -osirrox on -indev "$ISO" -extract / "$WORK" 2>/dev/null || { - # Fallback: mount + copy + echo " xorriso extraction failed, trying hdiutil..." MNT=$(mktemp -d) - if [ "$(uname)" = "Darwin" ]; then - hdiutil attach "$ISO" -mountpoint "$MNT" -readonly -nobrowse 2>/dev/null - else - sudo mount -o loop,ro "$ISO" "$MNT" - fi + hdiutil attach "$ISO" -mountpoint "$MNT" -readonly -nobrowse 2>/dev/null || { + echo " Could not mount ISO. Is it corrupt?" + exit 1 + } cp -a "$MNT"/* "$WORK/" 2>/dev/null || true - if [ "$(uname)" = "Darwin" ]; then - hdiutil detach "$MNT" 2>/dev/null - else - sudo umount "$MNT" - fi + hdiutil detach "$MNT" 2>/dev/null || true rmdir "$MNT" 2>/dev/null || true } +# Ensure files are writable after extraction +chmod -R u+w "$WORK" 2>/dev/null || true -# Step 3: Patch branding -echo "[3/4] Patching branding..." +# --- Patch branding --- +echo " [2/3] Patching branding..." THEME_DST="$WORK/boot/grub/themes/archipelago" mkdir -p "$THEME_DST" -# GRUB theme -cp "$SCRIPT_DIR/branding/grub-theme/theme.txt" "$THEME_DST/" 2>/dev/null && \ - echo " theme.txt: OK" -cp /tmp/grub-bg.png "$THEME_DST/background.png" 2>/dev/null && \ - echo " background.png: OK" +# GRUB theme.txt +if [ -f "$SCRIPT_DIR/branding/grub-theme/theme.txt" ]; then + cp "$SCRIPT_DIR/branding/grub-theme/theme.txt" "$THEME_DST/" + echo " theme.txt" +fi + +# GRUB background — use static file from branding dir +if [ -f "$SCRIPT_DIR/branding/grub-theme/background.png" ]; then + cp "$SCRIPT_DIR/branding/grub-theme/background.png" "$THEME_DST/background.png" + echo " background.png (static)" +elif [ -f "$SCRIPT_DIR/branding/generate-grub-background.py" ]; then + python3 "$SCRIPT_DIR/branding/generate-grub-background.py" "$THEME_DST/background.png" 2>/dev/null + echo " background.png (generated)" +fi # Plymouth theme -if [ -d "$WORK/archipelago/plymouth-theme" ]; then - cp "$SCRIPT_DIR/branding/plymouth-theme/"* "$WORK/archipelago/plymouth-theme/" 2>/dev/null - cp /tmp/plymouth-logo.png "$WORK/archipelago/plymouth-theme/logo.png" 2>/dev/null - echo " Plymouth theme: OK" +PLYMOUTH_DST="$WORK/archipelago/plymouth-theme" +mkdir -p "$PLYMOUTH_DST" +if [ -d "$SCRIPT_DIR/branding/plymouth-theme" ]; then + cp "$SCRIPT_DIR/branding/plymouth-theme/"* "$PLYMOUTH_DST/" 2>/dev/null || true + echo " plymouth theme" fi -# GRUB config (in case you edited it) -if [ -f "$SCRIPT_DIR/branding/grub.cfg" ]; then - cp "$SCRIPT_DIR/branding/grub.cfg" "$WORK/boot/grub/grub.cfg" - echo " grub.cfg: OK (custom)" -fi - -# ISOLINUX config -if [ -f "$SCRIPT_DIR/branding/isolinux.cfg" ]; then - cp "$SCRIPT_DIR/branding/isolinux.cfg" "$WORK/isolinux/isolinux.cfg" - echo " isolinux.cfg: OK (custom)" -fi - -# Step 4: Repackage ISO -echo "[4/4] Repackaging ISO..." +# --- Repackage ISO --- +echo " [3/3] Repackaging ISO..." mkdir -p "$SCRIPT_DIR/results" -# Find isohdpfx.bin +# Find isohdpfx.bin — project copy first, then system ISOHDPFX="" -for p in "$WORK/isolinux/isohdpfx.bin" \ +for p in "$SCRIPT_DIR/branding/isohdpfx.bin" \ + "$WORK/isolinux/isohdpfx.bin" \ /usr/lib/ISOLINUX/isohdpfx.bin \ /usr/share/syslinux/isohdpfx.bin \ /opt/homebrew/share/syslinux/isohdpfx.bin; do [ -f "$p" ] && ISOHDPFX="$p" && break done -# Check for EFI image -EFI_IMG="$WORK/boot/grub/efi.img" +if [ -z "$ISOHDPFX" ]; then + echo " ERROR: No isohdpfx.bin found. Cannot create bootable ISO." + echo " Preview only — open the background:" + open "$THEME_DST/background.png" 2>/dev/null || true + exit 1 +fi -if [ -n "$ISOHDPFX" ] && [ -f "$EFI_IMG" ]; then +EFI_IMG="$WORK/boot/grub/efi.img" +if [ -f "$EFI_IMG" ]; then xorriso -as mkisofs -o "$PATCHED" \ -volid "ARCHIPELAGO" \ - -iso-level 3 \ - -J -joliet-long -R \ + -iso-level 3 -J -joliet-long -R \ -isohybrid-mbr "$ISOHDPFX" \ -c isolinux/boot.cat \ -b isolinux/isolinux.bin \ -no-emul-boot -boot-load-size 4 -boot-info-table \ -eltorito-alt-boot \ -e boot/grub/efi.img \ - -no-emul-boot \ - -isohybrid-gpt-basdat \ + -no-emul-boot -isohybrid-gpt-basdat \ -partition_offset 16 \ "$WORK" 2>/dev/null -elif [ -n "$ISOHDPFX" ]; then +else xorriso -as mkisofs -o "$PATCHED" \ -volid "ARCHIPELAGO" \ - -iso-level 3 \ - -J -joliet-long -R \ + -iso-level 3 -J -joliet-long -R \ -isohybrid-mbr "$ISOHDPFX" \ -c isolinux/boot.cat \ -b isolinux/isolinux.bin \ -no-emul-boot -boot-load-size 4 -boot-info-table \ -partition_offset 16 \ "$WORK" 2>/dev/null -else - echo "Cannot repackage: no isohdpfx.bin found." - echo "Install xorriso and isolinux: brew install xorriso" - echo "" - echo "You can still preview the assets:" - echo " open /tmp/grub-bg.png" - echo " open /tmp/plymouth-logo.png" +fi + +echo "" +echo " Patched: $PATCHED ($(du -h "$PATCHED" | cut -f1))" +echo "" + +# --- Boot in QEMU --- +if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then + echo " QEMU not found. Install: brew install qemu" + echo " Opening background preview instead..." + open "$THEME_DST/background.png" 2>/dev/null || true exit 0 fi -echo "" -echo " Patched ISO: $PATCHED ($(du -h "$PATCHED" | cut -f1))" +echo " Booting in QEMU (BIOS mode — shows ISOLINUX menu)..." +echo " Press Ctrl+C to stop." echo "" -# Auto-boot in QEMU if available -if command -v qemu-system-x86_64 >/dev/null 2>&1; then - read -p "Boot in QEMU? [Y/n] " -n 1 -r - echo "" - if [[ ! $REPLY =~ ^[Nn]$ ]]; then - exec "$SCRIPT_DIR/test-iso-qemu.sh" "$PATCHED" --bios - fi -else - echo "Install QEMU to test: brew install qemu" +# Create test disk (use separate disk from other QEMU instances) +DISK="/tmp/archipelago-branding-test.qcow2" +# Kill any leftover QEMU from previous branding test +pkill -f "archipelago-branding-test" 2>/dev/null || true +sleep 1 +if [ ! -f "$DISK" ]; then + qemu-img create -f qcow2 "$DISK" 20G 2>/dev/null fi + +# Boot with BIOS to see the ISOLINUX/GRUB menu +qemu-system-x86_64 \ + -machine pc \ + -m 4G \ + -smp 2 \ + -boot d \ + -cdrom "$PATCHED" \ + -drive if=virtio,format=qcow2,file="$DISK" \ + -net nic,model=virtio -net user,hostfwd=tcp::2222-:22,hostfwd=tcp::8100-:80 \ + -vga virtio \ + -display default \ + -serial file:/tmp/archipelago-qemu-serial.log + +echo "" +echo " QEMU stopped. Serial log: /tmp/archipelago-qemu-serial.log" +echo " Re-run to test again after editing branding files."