#!/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=() 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 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 [--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)..." if [[ "$serial" != true ]]; then echo "SSH available at: ssh -p 2222 localhost" fi 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_args[@]}" \ "${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 [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 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