Enable running the dev VM on amd64 Linux hosts with KVM for near-native speed, instead of being limited to slow TCG emulation on Apple Silicon. Detects host OS/arch to select the right accelerator (KVM, HVF, or TCG), OVMF firmware paths, and display backend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
307 lines
8.4 KiB
Bash
Executable File
307 lines
8.4 KiB
Bash
Executable File
#!/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"
|
|
HOST_OS=$(uname -s) # Darwin or Linux
|
|
HOST_ARCH=$(uname -m) # arm64 or x86_64
|
|
|
|
# --- Platform-aware install hints ---
|
|
|
|
install_hint() {
|
|
if [[ "$HOST_OS" == "Darwin" ]]; then
|
|
echo "Install it with: brew install qemu" >&2
|
|
else
|
|
echo "Install it with: sudo apt install qemu-system-x86 ovmf" >&2
|
|
fi
|
|
}
|
|
|
|
# --- OVMF firmware discovery ---
|
|
|
|
find_ovmf_code() {
|
|
local paths=()
|
|
if [[ "$HOST_OS" == "Darwin" ]]; then
|
|
paths=(
|
|
/opt/homebrew/share/qemu/edk2-x86_64-code.fd
|
|
/usr/local/share/qemu/edk2-x86_64-code.fd
|
|
)
|
|
else
|
|
paths=(
|
|
/usr/share/OVMF/OVMF_CODE_4M.fd
|
|
/usr/share/OVMF/OVMF_CODE.fd
|
|
/usr/share/qemu/OVMF_CODE_4M.fd
|
|
/usr/share/qemu/OVMF_CODE.fd
|
|
/usr/share/edk2/x64/OVMF_CODE.fd
|
|
)
|
|
fi
|
|
for p in "${paths[@]}"; do
|
|
[[ -f "$p" ]] && echo "$p" && return
|
|
done
|
|
if [[ "$HOST_OS" == "Darwin" ]]; then
|
|
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
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
find_ovmf_vars_template() {
|
|
local paths=()
|
|
if [[ "$HOST_OS" == "Darwin" ]]; then
|
|
paths=(
|
|
/opt/homebrew/share/qemu/edk2-i386-vars.fd
|
|
/usr/local/share/qemu/edk2-i386-vars.fd
|
|
)
|
|
else
|
|
paths=(
|
|
/usr/share/OVMF/OVMF_VARS_4M.fd
|
|
/usr/share/OVMF/OVMF_VARS.fd
|
|
/usr/share/qemu/OVMF_VARS_4M.fd
|
|
/usr/share/qemu/OVMF_VARS.fd
|
|
/usr/share/edk2/x64/OVMF_VARS.fd
|
|
)
|
|
fi
|
|
for p in "${paths[@]}"; do
|
|
[[ -f "$p" ]] && echo "$p" && return
|
|
done
|
|
if [[ "$HOST_OS" == "Darwin" ]]; then
|
|
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
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# --- CPU/accelerator auto-detection ---
|
|
|
|
detect_accel() {
|
|
if [[ "$HOST_OS" == "Linux" && "$HOST_ARCH" == "x86_64" ]]; then
|
|
if [[ -w /dev/kvm ]]; then
|
|
echo "kvm"
|
|
else
|
|
echo "Warning: /dev/kvm not accessible. Install KVM and check permissions." >&2
|
|
echo " sudo apt install qemu-kvm && sudo usermod -aG kvm \$USER" >&2
|
|
echo "tcg"
|
|
fi
|
|
elif [[ "$HOST_OS" == "Darwin" && "$HOST_ARCH" == "x86_64" ]]; then
|
|
echo "hvf"
|
|
else
|
|
echo "tcg"
|
|
fi
|
|
}
|
|
|
|
detect_cpu() {
|
|
local accel="$1"
|
|
if [[ "$accel" == "kvm" || "$accel" == "hvf" ]]; then
|
|
echo "host"
|
|
else
|
|
echo "qemu64"
|
|
fi
|
|
}
|
|
|
|
# --- Subcommands ---
|
|
|
|
cmd_create() {
|
|
if ! command -v qemu-system-x86_64 &>/dev/null; then
|
|
echo "Error: qemu-system-x86_64 not found." >&2
|
|
install_hint
|
|
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 not found." >&2
|
|
install_hint
|
|
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
|
|
install_hint
|
|
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 not found." >&2
|
|
install_hint
|
|
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 "$accel")
|
|
|
|
if [[ "$accel" == "tcg" ]]; then
|
|
echo "Warning: Running x86_64 VM under TCG emulation — significantly slower"
|
|
echo "than hardware virtualization (KVM on Linux, HVF on Intel Mac)."
|
|
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)
|
|
elif [[ "$HOST_OS" == "Darwin" ]]; then
|
|
display_args=(-display cocoa -vga virtio)
|
|
else
|
|
display_args=(-display gtk -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 <command> [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
|