#!/usr/bin/env bash set -euo pipefail script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" state_dir="${WIN11_VM_STATE_DIR:-${script_dir}/.vm/windows11}" disk_image="${WIN11_VM_DISK_IMAGE:-${state_dir}/disk.qcow2}" seed_iso="${WIN11_VM_SEED_ISO:-$(dirname -- "$disk_image")/seed.iso}" disk_size="${WIN11_VM_DISK_SIZE:-64G}" memory="${WIN11_VM_MEMORY:-8G}" cpus="${WIN11_VM_CPUS:-8}" rdp_port="${WIN11_VM_RDP_PORT:-3389}" ssh_port="${WIN11_VM_SSH_PORT:-2222}" uefi_code="${WIN11_VM_UEFI_CODE:-/usr/share/OVMF/OVMF_CODE_4M.fd}" uefi_vars_template="${WIN11_VM_UEFI_VARS:-/usr/share/OVMF/OVMF_VARS_4M.fd}" uefi_vars="${state_dir}/OVMF_VARS_4M.fd" pid_file="${state_dir}/qemu.pid" log_file="${state_dir}/console.log" monitor_socket="${state_dir}/qemu-monitor.sock" guest_agent_socket="${state_dir}/qemu-guest-agent.sock" daemonize=false install_from_iso=false extra_args=() usage() { cat <&2; usage >&2; exit 2 ;; esac done installing=false if [[ ! -f "$disk_image" ]]; then installing=true if [[ -n "${VM_IMAGE_HELPER:-}" ]]; then image_downloader="$VM_IMAGE_HELPER" elif [[ -x "${script_dir}/vm-images/download-images.sh" ]]; then image_downloader="${script_dir}/vm-images/download-images.sh" else image_downloader="${script_dir}/../vm-images/download-images.sh" fi [[ -x "$image_downloader" ]] || { printf 'Error: shared image helper is unavailable: %s\n' "$image_downloader" >&2; exit 1; } if [[ "$install_from_iso" == true ]]; then install_iso="${WIN11_VM_INSTALL_ISO:-$("$image_downloader" --print-path windows11 --iso)}" uses_shared_tools_iso=false if [[ -n "${WIN11_VM_TOOLS_ISO:-}" ]]; then tools_iso="$WIN11_VM_TOOLS_ISO" else tools_iso="$("$image_downloader" --print-path virtio-win)" uses_shared_tools_iso=true fi else base_image="${WIN11_VM_BASE_IMAGE:-$("$image_downloader" --print-path windows11)}" fi fi for command_name in qemu-system-x86_64; do command -v "$command_name" >/dev/null 2>&1 || { printf 'Error: %s is not installed.\n' "$command_name" >&2; exit 1; } done if [[ "$installing" == true ]]; then command -v qemu-img >/dev/null 2>&1 || { printf 'Error: qemu-img is not installed.\n' >&2; exit 1; } if [[ "$install_from_iso" == true ]]; then [[ -f "$install_iso" ]] || { printf 'Error: Windows 11 ISO was not found: %s\n' "$install_iso" >&2; exit 1; } else [[ -f "$base_image" ]] || { printf 'Error: Windows 11 base QCOW2 image was not found: %s\n' "$base_image" >&2; exit 1; } fi fi for firmware_file in "$uefi_code" "$uefi_vars_template"; do [[ -f "$firmware_file" ]] || { printf 'Error: required UEFI firmware was not found: %s\n' "$firmware_file" >&2; exit 1; } done mkdir -p "$state_dir" if [[ "$installing" == true && "$install_from_iso" == true ]]; then if [[ ! -f "$tools_iso" ]]; then if [[ "$uses_shared_tools_iso" == true ]]; then "$image_downloader" virtio-win else printf 'Error: VirtIO Windows tools ISO was not found: %s\n' "$tools_iso" >&2 exit 1 fi fi [[ -f "$tools_iso" ]] || { printf 'Error: downloader did not create: %s\n' "$tools_iso" >&2; exit 1; } if [[ ! -f "$seed_iso" || "${script_dir}/seediso/windows11/Autounattend.xml" -nt "$seed_iso" || "${script_dir}/seediso/windows11/Configure-Windows.ps1" -nt "$seed_iso" ]]; then "${script_dir}/seediso/windows11/build-seed.sh" "$seed_iso" fi fi if [[ -f "$pid_file" ]]; then old_pid="$(<"$pid_file")" if [[ "$old_pid" =~ ^[0-9]+$ ]] && kill -0 "$old_pid" 2>/dev/null; then printf 'Error: Windows 11 VM is already running (PID %s).\n' "$old_pid" >&2 exit 1 fi rm -f "$pid_file" fi rm -f "$monitor_socket" "$guest_agent_socket" [[ -f "$uefi_vars" ]] || cp -- "$uefi_vars_template" "$uefi_vars" if [[ "$installing" == true ]]; then if [[ "$install_from_iso" == true ]]; then qemu-img create -q -f qcow2 "$disk_image" "$disk_size" else qemu-img create -q -f qcow2 -F qcow2 -b "$base_image" "$disk_image" "$disk_size" fi printf 'Created Windows 11 system disk: %s\n' "$disk_image" fi qemu_args=( -name windows11 -machine q35 -m "$memory" -smp "$cpus" -display gtk # -display none -spice port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off -device virtio-vga -drive "if=pflash,format=raw,readonly=on,file=${uefi_code}" -drive "if=pflash,format=raw,file=${uefi_vars}" -drive "file=${disk_image},if=none,id=system_disk,format=qcow2" -device "ide-hd,drive=system_disk,bus=ide.0,bootindex=1" -nic "user,model=e1000e,hostfwd=tcp::${rdp_port}-:3389,hostfwd=tcp::${ssh_port}-:22" -device "virtio-serial-pci,id=virtio_serial" -chardev "qemu-vdagent,id=vdagent,name=vdagent,clipboard=on,mouse=off" -device "virtserialport,chardev=vdagent,name=com.redhat.spice.0" # Register the USB tablet after vdagent so QEMU selects it as the active pointer. -device "qemu-xhci,id=usb_controller" -device "usb-tablet,bus=usb_controller.0" -chardev "socket,path=${guest_agent_socket},server=on,wait=off,id=qga" -device "virtserialport,chardev=qga,name=org.qemu.guest_agent.0" -monitor "unix:${monitor_socket},server=on,wait=off" ) if [[ "$installing" == true && "$install_from_iso" == true ]]; then qemu_args+=( -drive "file=${install_iso},if=none,id=install_media,media=cdrom,readonly=on" -device "ide-cd,drive=install_media,bus=ide.1,bootindex=2" -drive "file=${seed_iso},if=none,id=answer_media,media=cdrom,readonly=on" -device "ide-cd,drive=answer_media,bus=ide.2,bootindex=3" -drive "file=${tools_iso},if=none,id=tools_media,media=cdrom,readonly=on" -device "ide-cd,drive=tools_media,bus=ide.3,bootindex=4" ) fi if [[ -r /dev/kvm && -w /dev/kvm ]]; then qemu_args+=(-enable-kvm -cpu host) accelerator=KVM else qemu_args+=(-accel tcg -cpu max) accelerator=TCG fi if [[ "$installing" == true && "$install_from_iso" == true ]]; then printf 'Starting Windows 11 automatic installation with %s acceleration.\n' "$accelerator" elif [[ "$installing" == true ]]; then printf 'Starting Windows 11 from the QCOW2 base image with %s acceleration.\n' "$accelerator" else printf 'Starting Windows 11 normally with %s acceleration.\n' "$accelerator" fi printf 'Account: guofang / password; RDP: localhost:%s; SSH: localhost:%s\n' "$rdp_port" "$ssh_port" if [[ "$daemonize" == true ]]; then : >"$log_file" qemu_args+=(-serial "file:${log_file}" -daemonize -pidfile "$pid_file") qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}" printf 'VM started (PID %s); console log: %s\n' "$(<"$pid_file")" "$log_file" else exec qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}" fi