Add ISO build tooling and serial console support

- Add bin/yino-iso with download, build, and clean subcommands for
  creating a preseeded Debian installer ISO
- Add --serial flag to bin/yino-vm boot for headless serial console
- Add offline installation ISO spec to FSD (section 6)
- Update README TODO to reflect ISO pipeline progress

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Smith 2026-02-18 00:47:23 +01:00
parent 5fa89444d7
commit 17fa52824c
4 changed files with 233 additions and 8 deletions

View File

@ -10,8 +10,8 @@
## TODO ## TODO
- [x] Set up development environment (QEMU VM for testing) - [x] Set up development environment (QEMU VM for testing)
- [ ] Create ISO generation pipeline (Debian ISO with unattended install that triggers Yino installer) - [ ] Create ISO generation pipeline (Debian ISO with unattended install). See item "6. Offline installation ISO" in
- [ ] Configure boot and full disk encryption (LUKS, initramfs, bootloader) as part of unattended install `docs/yino-fsd.md` for full specification.
- [ ] Port Omarchy installer to Debian (runs post-install, equivalent of Omarchy's phase 2-5 scripts) - [ ] Port Omarchy installer to Debian (runs post-install, equivalent of Omarchy's phase 2-5 scripts)
- [ ] Get Hyprland running via the installer with essential dotfiles - [ ] Get Hyprland running via the installer with essential dotfiles

201
bin/yino-iso Executable file
View File

@ -0,0 +1,201 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
ISO_DIR="$REPO_DIR/iso"
DOWNLOAD_DIR="$REPO_DIR/download"
BASE_URL="https://cdimage.debian.org/debian-cd/current/amd64/iso-dvd"
# --- Subcommands ---
cmd_download() {
mkdir -p "$DOWNLOAD_DIR"
echo "Fetching SHA256SUMS..."
local sums_file="$DOWNLOAD_DIR/SHA256SUMS"
curl -fsSL -o "$sums_file" "$BASE_URL/SHA256SUMS"
local iso_name expected_hash
iso_name=$(grep 'debian-.*-amd64-DVD-1\.iso' "$sums_file" | awk '{print $2}')
expected_hash=$(grep 'debian-.*-amd64-DVD-1\.iso' "$sums_file" | awk '{print $1}')
if [[ -z "$iso_name" || -z "$expected_hash" ]]; then
echo "Error: Could not find DVD-1 ISO in SHA256SUMS." >&2
exit 1
fi
local iso_path="$DOWNLOAD_DIR/$iso_name"
if [[ -f "$iso_path" ]]; then
echo "ISO already exists, verifying checksum..."
local actual_hash
actual_hash=$(shasum -a 256 "$iso_path" | awk '{print $1}')
if [[ "$actual_hash" == "$expected_hash" ]]; then
echo "Checksum OK. Skipping download."
echo " $iso_path"
return
fi
echo "Checksum mismatch, re-downloading..."
fi
echo "Downloading $iso_name..."
curl -C - -fL -o "$iso_path" "$BASE_URL/$iso_name"
echo "Verifying checksum..."
local actual_hash
actual_hash=$(shasum -a 256 "$iso_path" | awk '{print $1}')
if [[ "$actual_hash" != "$expected_hash" ]]; then
echo "Error: Checksum verification failed." >&2
echo " Expected: $expected_hash" >&2
echo " Got: $actual_hash" >&2
exit 1
fi
echo "Checksum OK."
echo " $iso_path"
}
cmd_build() {
if ! command -v xorriso &>/dev/null; then
echo "Error: xorriso not found." >&2
echo "Install it with: brew install xorriso" >&2
exit 1
fi
# Find source ISO
local src_iso
src_iso=$(find "$DOWNLOAD_DIR" -maxdepth 1 -name 'debian-*-amd64-DVD-1.iso' -print -quit 2>/dev/null || true)
if [[ -z "$src_iso" || ! -f "$src_iso" ]]; then
echo "Error: No Debian DVD-1 ISO found in $DOWNLOAD_DIR" >&2
echo "Run 'yino-iso download' first." >&2
exit 1
fi
echo "Source ISO: $src_iso"
# Create temp working directory (not local — trap needs access after function returns)
work_dir=$(mktemp -d "${TMPDIR:-/tmp}/yino-iso.XXXXXX")
trap 'rm -rf "$work_dir"' EXIT
local extract_dir="$work_dir/iso"
# Extract ISO
echo "Extracting ISO..."
xorriso -osirrox on -indev "$src_iso" -extract / "$extract_dir"
chmod -R u+w "$extract_dir"
# Inject preseed into initrd
echo "Injecting preseed into initrd..."
gunzip "$extract_dir/install.amd/initrd.gz"
(cd "$ISO_DIR" && echo preseed.cfg | cpio -o --format newc) >> "$extract_dir/install.amd/initrd"
gzip "$extract_dir/install.amd/initrd"
# Modify boot configs — add preseed boot parameters
local boot_params="auto=true priority=critical locale=en_US.UTF-8 keymap=us console=ttyS0,115200n8"
echo "Modifying boot configuration..."
# Insert preseed params before the "---" separator in boot entries
if [[ -f "$extract_dir/isolinux/txt.cfg" ]]; then
sed -i.bak "s|--- |${boot_params} --- |" "$extract_dir/isolinux/txt.cfg"
rm -f "$extract_dir/isolinux/txt.cfg.bak"
# Set text installer as default boot entry (prepend with proper newline)
{ echo "default install"; cat "$extract_dir/isolinux/txt.cfg"; } > "$extract_dir/isolinux/txt.cfg.tmp"
mv "$extract_dir/isolinux/txt.cfg.tmp" "$extract_dir/isolinux/txt.cfg"
fi
# Fix isolinux: enable serial console, disable graphical menu, set short timeout
if [[ -f "$extract_dir/isolinux/isolinux.cfg" ]]; then
# Prepend serial directive (BSD sed '1i' doesn't insert newlines reliably)
{ echo "serial 0 115200"; cat "$extract_dir/isolinux/isolinux.cfg"; } > "$extract_dir/isolinux/isolinux.cfg.tmp"
mv "$extract_dir/isolinux/isolinux.cfg.tmp" "$extract_dir/isolinux/isolinux.cfg"
sed -i.bak 's|^default vesamenu.c32|#default vesamenu.c32|' "$extract_dir/isolinux/isolinux.cfg"
rm -f "$extract_dir/isolinux/isolinux.cfg.bak"
sed -i.bak 's|^timeout .*|timeout 50|' "$extract_dir/isolinux/isolinux.cfg"
rm -f "$extract_dir/isolinux/isolinux.cfg.bak"
fi
if [[ -f "$extract_dir/boot/grub/grub.cfg" ]]; then
sed -i.bak "s|--- |${boot_params} --- |" "$extract_dir/boot/grub/grub.cfg"
rm -f "$extract_dir/boot/grub/grub.cfg.bak"
# Add serial console support to GRUB and select text "Install" entry (index 1)
sed -i.bak 's|terminal_output gfxterm|terminal_output gfxterm serial|' "$extract_dir/boot/grub/grub.cfg"
rm -f "$extract_dir/boot/grub/grub.cfg.bak"
{ printf 'serial --unit=0 --speed=115200\nterminal_input serial console\nterminal_output serial console\nset timeout=5\nset default=1\n'; cat "$extract_dir/boot/grub/grub.cfg"; } > "$extract_dir/boot/grub/grub.cfg.tmp"
mv "$extract_dir/boot/grub/grub.cfg.tmp" "$extract_dir/boot/grub/grub.cfg"
fi
# Extract MBR template for hybrid boot
echo "Extracting MBR template..."
local mbr_template="$work_dir/isohdpfx.bin"
dd if="$src_iso" bs=1 count=432 of="$mbr_template" 2>/dev/null
# Regenerate checksums
echo "Regenerating checksums..."
(cd "$extract_dir" && find . -type f ! -name 'md5sum.txt' -exec md5sum {} + > md5sum.txt)
# Rebuild ISO
local output_iso="$DOWNLOAD_DIR/yino-$(date +%Y%m%d).iso"
echo "Building ISO..."
xorriso -as mkisofs \
-r -V "Yino" \
-o "$output_iso" \
-J -joliet-long \
-isohybrid-mbr "$mbr_template" \
-partition_offset 16 \
-b isolinux/isolinux.bin \
-c isolinux/boot.cat \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-eltorito-alt-boot \
-e boot/grub/efi.img \
-no-emul-boot \
-isohybrid-gpt-basdat \
"$extract_dir"
echo "Done."
echo " $output_iso"
}
cmd_clean() {
local yino_isos
yino_isos=$(find "$DOWNLOAD_DIR" -maxdepth 1 -name 'yino-*.iso' 2>/dev/null || true)
if [[ -z "$yino_isos" ]]; then
echo "Nothing to clean."
return
fi
echo "Removing built ISOs:"
while read -r f; do
echo " $f"
rm "$f"
done <<< "$yino_isos"
}
cmd_help() {
cat <<'EOF'
Usage: yino-iso <command>
Commands:
download Download Debian 13 DVD-1 ISO and verify checksum
build Build custom Yino ISO with preseed
clean Remove built ISOs
help Show this help
Dependencies (macOS):
brew install xorriso
EOF
}
# --- Main ---
case "${1:-help}" in
download) cmd_download ;;
build) cmd_build ;;
clean) cmd_clean ;;
help|--help|-h) cmd_help ;;
*)
echo "Error: Unknown command: $1" >&2
cmd_help >&2
exit 1
;;
esac

View File

@ -138,8 +138,13 @@ cmd_boot() {
fi fi
local iso_args=() local iso_args=()
local serial=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--serial)
serial=true
shift
;;
--iso) --iso)
if [[ -z "${2:-}" ]]; then if [[ -z "${2:-}" ]]; then
echo "Error: --iso requires a path argument." >&2 echo "Error: --iso requires a path argument." >&2
@ -154,14 +159,23 @@ cmd_boot() {
;; ;;
*) *)
echo "Error: Unknown option: $1" >&2 echo "Error: Unknown option: $1" >&2
echo "Usage: yino-vm boot [--iso PATH]" >&2 echo "Usage: yino-vm boot [--serial] [--iso PATH]" >&2
exit 1 exit 1
;; ;;
esac esac
done done
local display_args
if [[ "$serial" == true ]]; then
display_args=(-serial mon:stdio -nographic -display none)
else
display_args=(-display cocoa -vga virtio)
fi
echo "Booting VM (accel=$accel, cpu=$cpu)..." echo "Booting VM (accel=$accel, cpu=$cpu)..."
if [[ "$serial" != true ]]; then
echo "SSH available at: ssh -p 2222 localhost" echo "SSH available at: ssh -p 2222 localhost"
fi
echo "" echo ""
qemu-system-x86_64 \ qemu-system-x86_64 \
@ -175,8 +189,7 @@ cmd_boot() {
-drive "if=pflash,format=raw,file=$UEFI_VARS" \ -drive "if=pflash,format=raw,file=$UEFI_VARS" \
-device virtio-net-pci,netdev=net0 \ -device virtio-net-pci,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \
-display cocoa \ "${display_args[@]}" \
-vga virtio \
"${iso_args[@]}" "${iso_args[@]}"
} }
@ -226,7 +239,7 @@ Usage: yino-vm <command> [options]
Commands: Commands:
create Create VM disk image and UEFI vars create Create VM disk image and UEFI vars
boot [--iso P] Boot the VM (--iso to attach installer ISO) boot [opts] Boot the VM (--serial for console, --iso P for installer)
delete Delete VM disk image and UEFI vars delete Delete VM disk image and UEFI vars
status Show VM status status Show VM status
help Show this help help Show this help

View File

@ -50,7 +50,18 @@ This should match Omarchy's architecture (see [Omarchy Analysis](Omarchy.md) for
- QEMU should be used for testing and demo virtual machines - QEMU should be used for testing and demo virtual machines
- Keep a cache of downloaded assets (e.g. Debian installation ISO) in this repository's `download` directory - Keep a cache of downloaded assets (e.g. Debian installation ISO) in this repository's `download` directory
## 6. Upstream tracking ## 6. Offline installation ISO
This project should generate the tooling necessary to create a modified, offline installation ISO.
It should do this by downloading and caching the Debian iso-dvd image, extract it and apply the necessary
modifications to enable an unattended installation with the following characteristics (similiar to
how archinstall doest it):
- LUKS + btrfs
- System for a single user, automatic login because full disk decryption already authenticates user
- Graphical Wayland environment, no X11
## 7. Upstream tracking
- Track the upstream [basecamp/omarchy](https://github.com/basecamp/omarchy) repository for new releases and changes - Track the upstream [basecamp/omarchy](https://github.com/basecamp/omarchy) repository for new releases and changes
- Omarchy's `dev` branch is where active development happens; `master` is the stable branch - Omarchy's `dev` branch is where active development happens; `master` is the stable branch