diff --git a/README.md b/README.md index b827d45..2e84c9b 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ ## TODO - [x] Set up development environment (QEMU VM for testing) -- [ ] Create ISO generation pipeline (Debian ISO with unattended install that triggers Yino installer) -- [ ] Configure boot and full disk encryption (LUKS, initramfs, bootloader) as part of unattended install +- [ ] Create ISO generation pipeline (Debian ISO with unattended install). See item "6. Offline installation ISO" in + `docs/yino-fsd.md` for full specification. - [ ] 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 diff --git a/bin/yino-iso b/bin/yino-iso new file mode 100755 index 0000000..649a140 --- /dev/null +++ b/bin/yino-iso @@ -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 + +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 diff --git a/bin/yino-vm b/bin/yino-vm index ea8780f..374a456 100755 --- a/bin/yino-vm +++ b/bin/yino-vm @@ -138,8 +138,13 @@ cmd_boot() { fi local iso_args=() + local serial=false while [[ $# -gt 0 ]]; do case "$1" in + --serial) + serial=true + shift + ;; --iso) if [[ -z "${2:-}" ]]; then echo "Error: --iso requires a path argument." >&2 @@ -154,14 +159,23 @@ cmd_boot() { ;; *) 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 ;; esac 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 "SSH available at: ssh -p 2222 localhost" + if [[ "$serial" != true ]]; then + echo "SSH available at: ssh -p 2222 localhost" + fi echo "" qemu-system-x86_64 \ @@ -175,8 +189,7 @@ cmd_boot() { -drive "if=pflash,format=raw,file=$UEFI_VARS" \ -device virtio-net-pci,netdev=net0 \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ - -display cocoa \ - -vga virtio \ + "${display_args[@]}" \ "${iso_args[@]}" } @@ -226,7 +239,7 @@ Usage: yino-vm [options] Commands: 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 status Show VM status help Show this help diff --git a/docs/yino-fsd.md b/docs/yino-fsd.md index f46a938..5eb9066 100644 --- a/docs/yino-fsd.md +++ b/docs/yino-fsd.md @@ -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 - 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 - Omarchy's `dev` branch is where active development happens; `master` is the stable branch