From 4b30682e175b7625c1600b53b6b1412d04228554 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 16 Feb 2026 00:30:31 +0100 Subject: [PATCH] Add QEMU VM helper script for development environment Introduces bin/yino-vm with create, boot, delete, and status subcommands for managing a QEMU x86_64 VM on macOS. Auto-detects OVMF firmware paths and CPU/accelerator (HVF on Intel, TCG on Apple Silicon). VM artifacts are stored in vm/ (gitignored). Completes TODO #1. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + README.md | 2 +- bin/yino-vm | 249 ++++++++++++++++++++++++++++++++++++++++++++++++++++ vm/.keep | 0 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100755 bin/yino-vm create mode 100644 vm/.keep diff --git a/.gitignore b/.gitignore index 60cca52..6162ee1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /download +/vm .claude diff --git a/README.md b/README.md index 7fdeebc..b827d45 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## TODO -- [ ] 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) - [ ] Configure boot and full disk encryption (LUKS, initramfs, bootloader) as part of unattended install - [ ] Port Omarchy installer to Debian (runs post-install, equivalent of Omarchy's phase 2-5 scripts) diff --git a/bin/yino-vm b/bin/yino-vm new file mode 100755 index 0000000..ea8780f --- /dev/null +++ b/bin/yino-vm @@ -0,0 +1,249 @@ +#!/usr/bin/env bash +set -euo pipefail + +VM_DIR="$(cd "$(dirname "$0")/.." && pwd)/vm" +DISK_IMAGE="$VM_DIR/yino-vm.qcow2" +UEFI_VARS="$VM_DIR/yino-vm-vars.fd" + +# --- OVMF firmware discovery --- + +find_ovmf_code() { + local paths=( + /opt/homebrew/share/qemu/edk2-x86_64-code.fd + /usr/local/share/qemu/edk2-x86_64-code.fd + ) + for p in "${paths[@]}"; do + [[ -f "$p" ]] && echo "$p" && return + done + # Cellar fallbacks + local cellar_dirs=(/opt/homebrew/Cellar/qemu /usr/local/Cellar/qemu) + for d in "${cellar_dirs[@]}"; do + if [[ -d "$d" ]]; then + local found + found=$(find "$d" -name 'edk2-x86_64-code.fd' -print -quit 2>/dev/null) + [[ -n "$found" ]] && echo "$found" && return + fi + done + return 1 +} + +find_ovmf_vars_template() { + local paths=( + /opt/homebrew/share/qemu/edk2-i386-vars.fd + /usr/local/share/qemu/edk2-i386-vars.fd + ) + for p in "${paths[@]}"; do + [[ -f "$p" ]] && echo "$p" && return + done + local cellar_dirs=(/opt/homebrew/Cellar/qemu /usr/local/Cellar/qemu) + for d in "${cellar_dirs[@]}"; do + if [[ -d "$d" ]]; then + local found + found=$(find "$d" -name 'edk2-i386-vars.fd' -print -quit 2>/dev/null) + [[ -n "$found" ]] && echo "$found" && return + fi + done + return 1 +} + +# --- CPU/accelerator auto-detection --- + +detect_accel() { + local arch + arch=$(uname -m) + if [[ "$arch" == "arm64" ]]; then + echo "tcg" + else + echo "hvf" + fi +} + +detect_cpu() { + local arch + arch=$(uname -m) + if [[ "$arch" == "arm64" ]]; then + echo "qemu64" + else + echo "host" + fi +} + +# --- Subcommands --- + +cmd_create() { + if ! command -v qemu-system-x86_64 &>/dev/null; then + echo "Error: qemu-system-x86_64 not found." >&2 + echo "Install it with: brew install qemu" >&2 + exit 1 + fi + + if [[ -f "$DISK_IMAGE" ]]; then + echo "Error: Disk image already exists: $DISK_IMAGE" >&2 + echo "Run 'yino-vm delete' first to remove it." >&2 + exit 1 + fi + + local vars_template + if ! vars_template=$(find_ovmf_vars_template); then + echo "Error: OVMF vars template (edk2-i386-vars.fd) not found." >&2 + echo "Install it with: brew install qemu" >&2 + exit 1 + fi + + echo "Creating 30G sparse disk image..." + qemu-img create -f qcow2 "$DISK_IMAGE" 30G + + echo "Copying UEFI vars template..." + cp "$vars_template" "$UEFI_VARS" + + echo "VM created:" + echo " Disk: $DISK_IMAGE" + echo " UEFI vars: $UEFI_VARS" +} + +cmd_boot() { + if ! command -v qemu-system-x86_64 &>/dev/null; then + echo "Error: qemu-system-x86_64 not found." >&2 + echo "Install it with: brew install qemu" >&2 + exit 1 + fi + + if [[ ! -f "$DISK_IMAGE" ]]; then + echo "Error: Disk image not found: $DISK_IMAGE" >&2 + echo "Run 'yino-vm create' first." >&2 + exit 1 + fi + + local ovmf_code + if ! ovmf_code=$(find_ovmf_code); then + echo "Error: OVMF firmware (edk2-x86_64-code.fd) not found." >&2 + echo "Install it with: brew install qemu" >&2 + exit 1 + fi + + if [[ ! -f "$UEFI_VARS" ]]; then + echo "Error: UEFI vars not found: $UEFI_VARS" >&2 + echo "Run 'yino-vm create' first." >&2 + exit 1 + fi + + local accel cpu + accel=$(detect_accel) + cpu=$(detect_cpu) + + if [[ "$accel" == "tcg" ]]; then + echo "Warning: Running x86_64 VM under TCG emulation (Apple Silicon host)." + echo "This works but is significantly slower than hardware virtualization." + echo "" + fi + + local iso_args=() + while [[ $# -gt 0 ]]; do + case "$1" in + --iso) + if [[ -z "${2:-}" ]]; then + echo "Error: --iso requires a path argument." >&2 + exit 1 + fi + if [[ ! -f "$2" ]]; then + echo "Error: ISO file not found: $2" >&2 + exit 1 + fi + iso_args=(-drive "file=$2,media=cdrom,readonly=on" -boot d) + shift 2 + ;; + *) + echo "Error: Unknown option: $1" >&2 + echo "Usage: yino-vm boot [--iso PATH]" >&2 + exit 1 + ;; + esac + done + + echo "Booting VM (accel=$accel, cpu=$cpu)..." + echo "SSH available at: ssh -p 2222 localhost" + echo "" + + qemu-system-x86_64 \ + -machine q35 \ + -accel "$accel" \ + -cpu "$cpu" \ + -m 4G \ + -smp 2 \ + -drive "file=$DISK_IMAGE,if=virtio,cache=writeback" \ + -drive "if=pflash,format=raw,readonly=on,file=$ovmf_code" \ + -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 \ + "${iso_args[@]}" +} + +cmd_delete() { + if [[ ! -f "$DISK_IMAGE" && ! -f "$UEFI_VARS" ]]; then + echo "Nothing to delete — no VM files found." + exit 0 + fi + + echo "This will delete:" + [[ -f "$DISK_IMAGE" ]] && echo " $DISK_IMAGE" + [[ -f "$UEFI_VARS" ]] && echo " $UEFI_VARS" + echo "" + read -rp "Are you sure? [y/N] " confirm + if [[ "$confirm" != [yY] ]]; then + echo "Cancelled." + exit 0 + fi + + [[ -f "$DISK_IMAGE" ]] && rm "$DISK_IMAGE" && echo "Removed $DISK_IMAGE" + [[ -f "$UEFI_VARS" ]] && rm "$UEFI_VARS" && echo "Removed $UEFI_VARS" + echo "VM deleted." +} + +cmd_status() { + echo "VM directory: $VM_DIR" + echo "" + if [[ -f "$DISK_IMAGE" ]]; then + local size + size=$(qemu-img info --output=json "$DISK_IMAGE" 2>/dev/null \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"virtual: {d['virtual-size']/(1<<30):.0f}G, actual: {d['actual-size']/(1<<20):.1f}M\")" 2>/dev/null \ + || echo "present") + echo "Disk image: $size" + else + echo "Disk image: not created" + fi + if [[ -f "$UEFI_VARS" ]]; then + echo "UEFI vars: present" + else + echo "UEFI vars: not created" + fi +} + +cmd_help() { + cat <<'EOF' +Usage: yino-vm [options] + +Commands: + create Create VM disk image and UEFI vars + boot [--iso P] Boot the VM (--iso to attach installer ISO) + delete Delete VM disk image and UEFI vars + status Show VM status + help Show this help +EOF +} + +# --- Main --- + +case "${1:-help}" in + create) cmd_create ;; + boot) shift; cmd_boot "$@" ;; + delete) cmd_delete ;; + status) cmd_status ;; + help|--help|-h) cmd_help ;; + *) + echo "Error: Unknown command: $1" >&2 + cmd_help >&2 + exit 1 + ;; +esac diff --git a/vm/.keep b/vm/.keep new file mode 100644 index 0000000..e69de29