yino/bin/yino-iso
Michael Smith 17fa52824c 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>
2026-02-18 00:47:23 +01:00

202 lines
6.8 KiB
Bash
Executable File

#!/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