#!/usr/bin/env bash export LC_ALL=C if ((BASH_VERSINFO[0] < 4)); then echo "Sorry, you need bash 4.0 or newer to run this script." exit 1 fi function ignore_msrs_always() { # Make sure the host has /etc/modprobe.d if [ -d /etc/modprobe.d ]; then # Skip if ignore_msrs is already enabled, assumes initramfs has been rebuilt if ! grep -lq 'ignore_msrs=Y' /etc/modprobe.d/kvm-quickemu.conf >/dev/null 2>&1; then echo "options kvm ignore_msrs=Y" | sudo tee /etc/modprobe.d/kvm-quickemu.conf sudo update-initramfs -k all -u fi else echo "ERROR! /etc/modprobe.d was not found, I don't know how to configure this system." exit 1 fi } function ignore_msrs_alert() { local ignore_msrs="" if [ "${OS_KERNEL}" == "Darwin" ]; then return elif [ -e /sys/module/kvm/parameters/ignore_msrs ]; then ignore_msrs=$(cat /sys/module/kvm/parameters/ignore_msrs) if [ "${ignore_msrs}" == "N" ]; then echo " - MSR: WARNING! Ignoring unhandled Model-Specific Registers is disabled." echo echo " echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs" echo echo " If you are unable to run macOS or Windows VMs then run the above 👆" echo " This will enable ignoring of unhandled MSRs until you reboot the host." echo " You can make this change permanent by running: 'quickemu --ignore-msrs-always'" fi fi } # Check for TSC instability that can cause macOS Ventura+ to freeze on AMD Ryzen mobile CPUs. # Returns 0 if check passes or user acknowledges warning, exits with 1 if user aborts. # Reference: https://github.com/quickemu-project/quickemu/issues/1273 function check_macos_tsc_stability() { # Gate 1: Only on Linux hosts if [ "${OS_KERNEL}" != "Linux" ]; then return 0 fi # Gate 2: Only for AMD CPUs if [ "${HOST_CPU_VENDOR}" != "AuthenticAMD" ]; then return 0 fi # Gate 3: Only for macOS guests if [ "${guest_os}" != "macos" ]; then return 0 fi # Gate 4: Only for macOS Ventura (13) and newer case ${macos_release} in ventura|sonoma|sequoia|tahoe) ;; *) return 0 ;; esac # Gate 5: Skip if user has already set tsc=reliable in kernel cmdline local cmdline="" if [ -r /proc/cmdline ]; then cmdline=$(cat /proc/cmdline) if [[ "${cmdline}" == *"tsc=reliable"* ]]; then return 0 fi fi # Gate 6: Check if TSC is the current clocksource (indicates stable TSC) local clocksource_path="/sys/devices/system/clocksource/clocksource0/current_clocksource" local current_clocksource="" if [ -r "${clocksource_path}" ]; then current_clocksource=$(cat "${clocksource_path}") if [ "${current_clocksource}" == "tsc" ]; then return 0 fi else # Cannot determine clocksource - assume OK and let user discover issues return 0 fi # All gates failed - this system is at risk # Check if warning should be skipped if [ "${IGNORE_TSC_WARNING}" == "1" ]; then echo " - TSC: WARNING! Unstable TSC detected (clocksource: ${current_clocksource})" echo " Proceeding anyway due to --ignore-tsc-warning flag." return 0 fi # Display warning and prompt user echo " - TSC: WARNING! Unstable TSC detected (clocksource: ${current_clocksource})" echo " macOS ${macos_release^} may freeze on AMD Ryzen mobile CPUs." echo echo " Fix: Add 'tsc=reliable' to kernel boot parameters and reboot." echo " Or: Use macOS Big Sur (11) or Monterey (12) instead." echo " See: https://github.com/quickemu-project/quickemu/wiki/03-Create-macOS-virtual-machines#tsc-instability-on-amd-ryzen-mobile-cpus" echo # Log the warning echo "TSC_WARNING: clocksource=${current_clocksource} macos_release=${macos_release} cpu_vendor=${HOST_CPU_VENDOR}" >> "${VMDIR}/${VMNAME}.log" # Interactive prompt - check if stdin is a terminal if [ -t 0 ]; then echo -n "Do you want to continue anyway? [y/N] " read -r response case "${response}" in [yY]|[yY][eE][sS]) echo " - TSC: Proceeding despite unstable TSC warning." return 0 ;; *) echo " - TSC: Aborting. Please apply one of the solutions above." exit 1 ;; esac else # Non-interactive mode - abort by default for safety echo "ERROR! Non-interactive mode detected. Use --ignore-tsc-warning to bypass this check." exit 1 fi } function delete_shortcut() { local SHORTCUT_DIR="${HOME}/.local/share/applications" if [ -e "${SHORTCUT_DIR}/${VMNAME}.desktop" ]; then rm "${SHORTCUT_DIR}/${VMNAME}.desktop" echo " - Deleted ${SHORTCUT_DIR}/${VMNAME}.desktop" fi } function delete_disk() { echo "Deleting ${VMNAME} virtual hard disk" if [ -e "${disk_img}" ]; then rm "${disk_img}" >/dev/null 2>&1 # Remove any EFI vars, but not for macOS rm "${VMDIR}"/OVMF_VARS*.fd >/dev/null 2>&1 rm "${VMDIR}/${VMNAME}-vars.fd" >/dev/null 2>&1 echo " - Deleted ${disk_img}" delete_shortcut else echo " - ${disk_img} not found. Doing nothing." fi } function delete_vm() { echo "Deleting ${VMNAME} completely" if [ -d "${VMDIR}" ]; then rm -rf "${VMDIR}" rm "${VM}" echo " - Deleted ${VM} and ${VMDIR}/" delete_shortcut else echo " - ${VMDIR} not found. Doing nothing." fi } function kill_vm() { echo "Killing ${VMNAME}" if [ -z "${VM_PID}" ]; then echo " - ${VMNAME} is not running." rm -f "${VMDIR}/${VMNAME}.pid" rm -f "${VMDIR}/${VMNAME}.spice" rm -f "${VMDIR}/${VMNAME}.sock" elif [ -n "${VM_PID}" ]; then if kill -9 "${VM_PID}" > /dev/null 2>&1; then echo " - ${VMNAME} (${VM_PID}) killed." rm -f "${VMDIR}/${VMNAME}.pid" rm -f "${VMDIR}/${VMNAME}.spice" rm -f "${VMDIR}/${VMNAME}.sock" else echo " - ${VMNAME} (${VM_PID}) was not killed." fi elif [ ! -r "${VMDIR}/${VMNAME}.pid" ]; then echo " - ${VMNAME} has no ${VMDIR}/${VMNAME}.pid" fi } function snapshot_apply() { echo "Snapshot apply to ${disk_img}" local TAG="${1}" if [ -z "${TAG}" ]; then echo " - ERROR! No snapshot tag provided." exit fi if [ -e "${disk_img}" ]; then if ${QEMU_IMG} snapshot -q -a "${TAG}" "${disk_img}"; then echo " - Applied snapshot '${TAG}' to ${disk_img}" else echo " - ERROR! Failed to apply snapshot '${TAG}' to ${disk_img}" fi else echo " - NOTE! ${disk_img} not found. Doing nothing." fi } function snapshot_create() { echo "Snapshotting ${disk_img}" local TAG="${1}" if [ -z "${TAG}" ]; then echo "- ERROR! No snapshot tag provided." exit fi if [ -e "${disk_img}" ]; then if ${QEMU_IMG} snapshot -q -c "${TAG}" "${disk_img}"; then echo " - Created snapshot '${TAG}' for ${disk_img}" else echo " - ERROR! Failed to create snapshot '${TAG}' for ${disk_img}" fi else echo " - NOTE! ${disk_img} not found. Doing nothing." fi } function snapshot_delete() { echo "Snapshot removal ${disk_img}" local TAG="${1}" if [ -z "${TAG}" ]; then echo " - ERROR! No snapshot tag provided." exit fi if [ -e "${disk_img}" ]; then if ${QEMU_IMG} snapshot -q -d "${TAG}" "${disk_img}"; then echo " - Deleted snapshot '${TAG}' from ${disk_img}" else echo " - ERROR! Failed to delete snapshot '${TAG}' from ${disk_img}" fi else echo " - NOTE! ${disk_img} not found. Doing nothing." fi } function snapshot_info() { echo if [ -e "${disk_img}" ]; then ${QEMU_IMG} info "${disk_img}" fi } function get_port() { local PORT_START=$1 local PORT_RANGE=$((PORT_START+$2)) local PORT for ((PORT = PORT_START; PORT <= PORT_RANGE; PORT++)); do # Make sure port scans do not block too long. timeout 0.1s bash -c "echo >/dev/tcp/127.0.0.1/${PORT}" >/dev/null 2>&1 if [ ${?} -eq 1 ]; then echo "${PORT}" break fi done } function configure_usb() { local DEVICE="" local USB_BUS="" local USB_DEV="" local USB_NAME="" local VENDOR_ID="" local PRODUCT_ID="" local USB_NOT_READY=0 # Have any USB devices been requested for pass-through? if (( ${#usb_devices[@]} )); then echo " - USB: Host pass-through requested:" for DEVICE in "${usb_devices[@]}"; do VENDOR_ID=$(echo "${DEVICE}" | cut -d':' -f1) PRODUCT_ID=$(echo "${DEVICE}" | cut -d':' -f2) USB_BUS=$(lsusb -d "${VENDOR_ID}:${PRODUCT_ID}" | cut -d' ' -f2) USB_DEV=$(lsusb -d "${VENDOR_ID}:${PRODUCT_ID}" | cut -d' ' -f4 | cut -d':' -f1) USB_NAME=$(lsusb -d "${VENDOR_ID}:${PRODUCT_ID}" | cut -d' ' -f7-) if [ -z "${USB_NAME}" ]; then echo " ! USB device ${VENDOR_ID}:${PRODUCT_ID} not found. Check your configuration" continue elif [ -w "/dev/bus/usb/${USB_BUS}/${USB_DEV}" ]; then echo " o ${USB_NAME} on bus ${USB_BUS} device ${USB_DEV} is accessible." else echo " x ${USB_NAME} on bus ${USB_BUS} device ${USB_DEV} needs permission changes:" echo " sudo chown -v root:${USER} /dev/bus/usb/${USB_BUS}/${USB_DEV}" USB_NOT_READY=1 fi USB_PASSTHROUGH="${USB_PASSTHROUGH} -device usb-host,bus=hostpass.0,vendorid=0x${VENDOR_ID},productid=0x${PRODUCT_ID}" done if [ "${USB_NOT_READY}" -eq 1 ]; then echo " ERROR! USB permission changes are required 👆" exit 1 fi fi } # get the number of processing units function get_nproc() { if command -v nproc &>/dev/null; then nproc elif command -v sysctl &>/dev/null; then sysctl -n hw.ncpu else echo "ERROR! Unable to determine the number of processing units." exit 1 fi } # macOS and Linux compatible get_cpu_info function function get_cpu_info() { local INFO_NAME="${1}" if [ "${OS_KERNEL}" == "Darwin" ]; then if [ "^Model name:" == "${INFO_NAME}" ]; then sysctl -n machdep.cpu.brand_string elif [ "Socket" == "${INFO_NAME}" ]; then sysctl -n hw.packages elif [ "^Vendor ID" == "${INFO_NAME}" ]; then if [ "${ARCH_HOST}" == "arm64" ]; then sysctl -n machdep.cpu.brand_string | cut -d' ' -f1 else sysctl -n machdep.cpu.vendor | sed 's/ //g' fi else echo "ERROR! Could not find macOS translation for ${INFO_NAME}" exit 1 fi else if [ "^Model name:" == "${INFO_NAME}" ]; then for MODEL_NAME in $(IFS=$'\n' lscpu | grep "${INFO_NAME}" | cut -d':' -f2 | sed -e 's/^[[:space:]]*//'); do echo -n "${MODEL_NAME} " done else lscpu | grep -E "${INFO_NAME}" | cut -d':' -f2 | sed 's/ //g' | sort -u fi fi } # returns an enabled or disable CPU flag for QEMU, based on the host CPU # capabilities, or nothing if the flag is not supported # converts the flags appropriately from macOS and Linux to QEMU function configure_cpu_flag() { local HOST_CPU_FLAG="${1}" # Convert the flag to lowercase for QEMU local QEMU_CPU_FLAG=${HOST_CPU_FLAG,,} if check_cpu_flag "${HOST_CPU_FLAG}"; then # Replace _ with - to make it compatible with QEMU QEMU_CPU_FLAG="${HOST_CPU_FLAG//_/-}" QEMU_CPU_FLAG="${QEMU_CPU_FLAG//4_/4\.}" # macOS uses different flag names if [ "${OS_KERNEL}" == "Darwin" ]; then case "${HOST_CPU_FLAG}" in avx) QEMU_CPU_FLAG="AVX1.0";; esac fi echo ",+${QEMU_CPU_FLAG}" else # Fully disable any QEMU flags that are not supported by the host CPU if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then case ${HOST_CPU_FLAG} in pcid) echo ",-${QEMU_CPU_FLAG}";; esac fi fi } # checks if a CPU flag is supported by the host CPU on Linux and macOS function check_cpu_flag() { local HOST_CPU_FLAG="" if [ "${OS_KERNEL}" == "Darwin" ]; then # Make the macOS compatible: uppercase, replace _ with . and replace X2APIC with x2APIC HOST_CPU_FLAG="${1^^}" HOST_CPU_FLAG="${HOST_CPU_FLAG//_/.}" HOST_CPU_FLAG="${HOST_CPU_FLAG//X2APIC/x2APIC}" if [ "${HOST_CPU_FLAG}" == "AVX" ]; then HOST_CPU_FLAG="AVX1.0" fi if sysctl -n machdep.cpu.features | grep -o "${HOST_CPU_FLAG}" > /dev/null; then return 0 else return 1 fi else HOST_CPU_FLAG="${1}" if lscpu | grep -o "^Flags\b.*: .*\b${HOST_CPU_FLAG}\b" > /dev/null; then return 0 else # AMD CPUs report invtsc as constant_tsc; check for equivalence if [ "${HOST_CPU_FLAG}" == "invtsc" ] && [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ]; then if lscpu | grep -o "^Flags\b.*: .*\bconstant_tsc\b" > /dev/null; then return 0 fi fi return 1 fi fi } function efi_vars() { local VARS_IN="" local VARS_OUT="" VARS_IN="${1}" VARS_OUT="${2}" if [ ! -e "${VARS_OUT}" ]; then if [ -e "${VARS_IN}" ]; then cp "${VARS_IN}" "${VARS_OUT}" else echo "ERROR! ${VARS_IN} was not found. Please install edk2." exit 1 fi fi } # Reset CPU flags tracking (call when initialising new CPU string) function reset_cpu_flags() { CPU_FLAG_MAP=() } # Add a CPU flag with deduplication and conflict detection # Usage: add_cpu_flag "+vmx" or add_cpu_flag ",+vmx" function add_cpu_flag() { local flag="${1#,}" # Strip leading comma if present # Skip empty flags [[ -z "${flag}" ]] && return 0 # Validate flag format: must be [+-]?name or name=value if [[ ! "${flag}" =~ ^[+-]?[a-zA-Z][a-zA-Z0-9._-]*(=.+)?$ ]]; then echo "WARNING: Invalid CPU flag format: '${flag}' - skipping" return 1 fi # Extract base flag name (without +/- prefix and =value suffix) local prefix="" local base="${flag}" if [[ "${flag}" == [+-]* ]]; then prefix="${flag:0:1}" base="${flag:1}" fi base="${base%%=*}" # Check for exact duplicate if [[ -n "${CPU_FLAG_MAP[${flag}]:-}" ]]; then return 0 # Silently skip duplicates fi # Check for conflicts (opposite prefix) local opposite="" if [[ "${prefix}" == "+" ]]; then opposite="-${base}" elif [[ "${prefix}" == "-" ]]; then opposite="+${base}" fi if [[ -n "${opposite}" ]] && [[ -n "${CPU_FLAG_MAP[${opposite}]:-}" ]]; then echo "WARNING: Conflicting CPU flag '${flag}' ignored (${opposite} already set)" return 1 fi # Check for value conflicts (e.g., flag=on vs flag=off) if [[ "${flag}" == *=* ]]; then for existing in "${!CPU_FLAG_MAP[@]}"; do if [[ "${existing%%=*}" == "${base}" ]] && [[ "${existing}" == *=* ]]; then echo "WARNING: Conflicting CPU flag '${flag}' ignored (${existing} already set)" return 1 fi done fi # Add to tracking map and append to CPU string CPU_FLAG_MAP["${flag}"]=1 CPU+=",${flag}" return 0 } function configure_cpu() { HOST_CPU_CORES=$(get_nproc) HOST_CPU_MODEL=$(get_cpu_info '^Model name:') HOST_CPU_SOCKETS=$(get_cpu_info 'Socket') if [ "${OS_KERNEL}" == "Darwin" ]; then HOST_CPU_VENDOR=$(get_cpu_info 'Vendor') else HOST_CPU_VENDOR=$(get_cpu_info '^Vendor ID') fi if [ "${HOST_CPU_SOCKETS}" = "-" ]; then HOST_CPU_SOCKETS=1 fi CPU_MODEL="host" QEMU_ACCEL="tcg" # Configure appropriately for the host platform if [ "${OS_KERNEL}" == "Darwin" ]; then MANUFACTURER=$(ioreg -l | grep -e Manufacturer | grep -v iMan | cut -d'"' -f4 | sort -u) CPU_KVM_UNHALT="" QEMU_ACCEL="hvf" # QEMU for macOS from Homebrew does not support SMM SMM="off" else if [ -r /sys/class/dmi/id/sys_vendor ]; then MANUFACTURER=$(head -n 1 /sys/class/dmi/id/sys_vendor) fi CPU_KVM_UNHALT=",kvm_pv_unhalt" GUEST_TWEAKS+=" -global kvm-pit.lost_tick_policy=discard" QEMU_ACCEL="kvm" fi if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64 guest support # https://qemu-project.gitlab.io/qemu/system/arm/virt.html # highmem=on allows aarch64 guests on the "virt" machine type to access guest RAM # above the 4GB boundary. This is required for VMs configured with >3GB RAM and is # generally more compatible on modern aarch64 systems. # pflash0/pflash1 reference the blockdev nodes for AAVMF firmware MACHINE_TYPE="virt,highmem=on,pflash0=rom,pflash1=efivars" case ${ARCH_HOST} in arm64|aarch64) # Native ARM64 host running ARM64 guest - use hardware acceleration CPU_MODEL="max";; *) # Cross-architecture emulation (e.g., x86_64 host running ARM64 guest) CPU_MODEL="max" QEMU_ACCEL="tcg";; esac elif [ "${ARCH_VM}" != "${ARCH_HOST}" ]; then # If the architecture of the VM is different from the host, disable acceleration # and use TCG (Tiny Code Generator) software emulation. TCG emulates the target # architecture in software, allowing cross-architecture virtualisation (e.g., # running x86_64 VMs on ARM hosts). # # Users can manually force TCG mode by adding this to their VM .conf file: # cpu_model="qemu64" # extra_args="-accel tcg" # or by exporting QEMU_ACCEL in the shell environment: # export QEMU_ACCEL="tcg" # # TCG is useful for: # - Cross-architecture virtualisation (x86 on ARM, ARM on x86) # - Testing VMs on hosts without hardware virtualisation support # - Emulating CPU features in software (e.g., running macOS x86_64 VMs on ARM Macs) CPU_MODEL="qemu64" CPU_KVM_UNHALT="" QEMU_ACCEL="tcg" fi # Detect if running inside a VM based on manufacturer detection # Note: Checking CPU flags (vmx/svm) indicates hardware virtualisation SUPPORT, # not whether we're inside a VM. Nested virtualisation may expose these flags. case ${MANUFACTURER,,} in qemu|virtualbox) CPU_MODEL="qemu64" QEMU_ACCEL="tcg" HYPERVISOR="${MANUFACTURER,,}";; *) HYPERVISOR="";; esac if [ -z "${HYPERVISOR}" ]; then # A CPU with Intel VT-x / AMD SVM support is required if [ "${HOST_CPU_VENDOR}" == "GenuineIntel" ]; then if ! check_cpu_flag vmx; then echo "ERROR! Intel VT-x support is required." exit 1 fi elif [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ] || [ "${HOST_CPU_VENDOR}" == "HygonGenuine" ]; then # HygonGenuine is Chinese AMD-compatible CPUs (Hygon Dhyana) if ! check_cpu_flag svm; then echo "ERROR! AMD SVM support is required." exit 1 fi elif [ "${ARCH_HOST}" == "aarch64" ] || [ "${ARCH_HOST}" == "arm64" ]; then # ARM hosts running native ARM guests with hardware acceleration (HVF on macOS, KVM on Linux) # ARM processors don't have x86-specific virtualisation flags (VT-x/SVM) to check. # We use architecture detection here instead of vendor detection (used for x86) # because ARM CPUs don't have standardised vendor strings like x86 (GenuineIntel/AuthenticAMD). # Cross-architecture guests (x86 on ARM) use TCG and skip this validation block entirely. # No validation needed here - ARM virtualisation support is handled by the hypervisor true else # Unknown CPU vendor - could be future/custom CPUs echo "WARNING! Unknown CPU vendor '${HOST_CPU_VENDOR}' - cannot verify virtualisation support." echo " If virtualisation fails, check your CPU supports hardware virtualisation and it's enabled in firmware." fi fi CPU="-cpu ${CPU_MODEL}" reset_cpu_flags # Make any OS specific adjustments if [ "${guest_os}" == "freedos" ] || [ "${guest_os}" == "windows" ] || [ "${guest_os}" == "windows-server" ]; then # SMM is not available on QEMU for macOS via Homebrew if [ "${OS_KERNEL}" == "Linux" ]; then SMM="on" fi fi # SMM is also required for Linux guests when Secure Boot is enabled if [ "${secureboot}" == "on" ]; then if [ "${guest_os}" == "linux" ]; then # SMM is not available on QEMU for macOS via Homebrew if [ "${OS_KERNEL}" == "Linux" ]; then SMM="on" fi fi fi case ${guest_os} in batocera|freedos|haiku|solaris) MACHINE_TYPE="pc";; kolibrios|reactos) CPU="-cpu qemu32" reset_cpu_flags MACHINE_TYPE="pc";; macos) # If the host has an Intel CPU, passes the host CPU model features, model, stepping, exactly to the guest. # Disable huge pages (,-pdpe1gb) on macOS to prevent crashes # - https://stackoverflow.com/questions/60231203/qemu-qcow2-mmu-gva-to-gpa-crash-in-mac-os-x # vmware-cpuid-freq=on enables VMware TSC frequency reporting for macOS timing if [ "${HOST_CPU_VENDOR}" == "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then CPU_MODEL="host" CPU="-cpu ${CPU_MODEL},-pdpe1gb,+hypervisor,vmware-cpuid-freq=on" reset_cpu_flags else CPU_MODEL="Haswell-v2" CPU="-cpu ${CPU_MODEL},vendor=GenuineIntel,-pdpe1gb,+avx,+sse,+sse2,+ssse3,vmware-cpuid-freq=on" reset_cpu_flags fi # A CPU with fma is required for Metal support # A CPU with invtsc is required for macOS to boot # Skip CPU feature checks when using TCG emulation (cross-architecture) # as TCG will emulate the required x86 features in software. This enables # running macOS x86_64 VMs on ARM Macs through software emulation. if [ "${QEMU_ACCEL}" != "tcg" ]; then case ${macos_release} in ventura|sonoma|sequoia|tahoe) # A CPU with AVX2 support is required for >= macOS Ventura if check_cpu_flag sse4_2 && check_cpu_flag avx2; then if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then add_cpu_flag "+avx2" add_cpu_flag "+sse4.2" fi else echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.2 and AVX2 support." echo " Try macOS Monterey or Big Sur." exit 1 fi;; catalina|big-sur|monterey) # A CPU with SSE4.2 support is required for >= macOS Catalina if check_cpu_flag sse4_2; then if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then add_cpu_flag "+sse4.2" fi else echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.2 support." exit 1 fi;; *) # A CPU with SSE4.1 support is required for >= macOS Sierra if check_cpu_flag sse4_1; then if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then add_cpu_flag "+sse4.1" fi else echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.1 support." exit 1 fi;; esac if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then for FLAG in abm adx aes amd-ssbd apic arat bmi1 bmi2 clflush cmov cx8 cx16 de \ eist erms f16c fma fp87 fsgsbase fxsr invpcid invtsc lahf_lm lm \ mca mce mmx movbe mpx msr mtrr nx pae pat pcid pge pse popcnt pse36 \ rdrand rdtscp sep smep syscall tsc tsc_adjust vaes vbmi2 vmx vpclmulqdq \ x2apic xgetbv1 xsave xsaveopt; do local cpu_flag cpu_flag=$(configure_cpu_flag "${FLAG}") [[ -n "${cpu_flag}" ]] && add_cpu_flag "${cpu_flag#,}" done # AMD CPUs with constant_tsc need explicit TSC flags for macOS stability # constant_tsc is AMD's equivalent of Intel's invtsc if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ] && check_cpu_flag invtsc; then add_cpu_flag "+tsc" add_cpu_flag "+tsc-deadline" fi fi fi # Disable S3 support in the VM to prevent macOS suspending during install # Disable ACPI PCI hotplug to prevent issues with macOS (required for QEMU 6.1+) GUEST_TWEAKS+=" -global ICH9-LPC.disable_s3=1 -global ICH9-LPC.acpi-pci-hotplug-with-bridge-support=off -device isa-applesmc,osk=$(echo "bheuneqjbexolgurfrjbeqfthneqrqcyrnfrqbagfgrny(p)NccyrPbzchgreVap" | tr 'A-Za-z' 'N-ZA-Mn-za-m')" # Disable High Precision Timer if [ "${QEMU_VER_SHORT}" -ge 70 ]; then MACHINE_TYPE+=",hpet=off" else GUEST_TWEAKS+=" -no-hpet" fi ;; windows|windows-server) # Base CPU flags that work with all accelerators (KVM, HVF, TCG) CPU="-cpu ${CPU_MODEL},+hypervisor,+invtsc,l3-cache=on" reset_cpu_flags # KVM-specific flags: migratable and Hyper-V enlightenments if [ "${QEMU_ACCEL}" == "kvm" ]; then if [ "${QEMU_VER_SHORT}" -gt 60 ]; then add_cpu_flag "migratable=no" add_cpu_flag "hv_passthrough" else add_cpu_flag "migratable=no" add_cpu_flag "hv_frequencies" [[ -n "${CPU_KVM_UNHALT}" ]] && add_cpu_flag "${CPU_KVM_UNHALT#,}" add_cpu_flag "hv_reenlightenment" add_cpu_flag "hv_relaxed" add_cpu_flag "hv_spinlocks=8191" add_cpu_flag "hv_stimer" add_cpu_flag "hv_synic" add_cpu_flag "hv_time" add_cpu_flag "hv_vapic" add_cpu_flag "hv_vendor_id=1234567890ab" add_cpu_flag "hv_vpindex" fi fi # Disable S3 support in the VM to ensure Windows can boot with SecureBoot enabled # - https://wiki.archlinux.org/title/QEMU/Troubleshooting#Virtual_machine_not_booting_when_using_a_Secure_Boot_enabled_OVMF GUEST_TWEAKS+=" -global ICH9-LPC.disable_s3=1" # Disable High Precision Timer if [ "${QEMU_VER_SHORT}" -ge 70 ]; then MACHINE_TYPE+=",hpet=off" else GUEST_TWEAKS+=" -no-hpet" fi ;; esac if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ] && [ "${guest_os}" != "macos" ] && [ "${ARCH_VM}" == "x86_64" ]; then add_cpu_flag "topoext" fi if [ -z "${cpu_cores}" ]; then if [ "${HOST_CPU_CORES}" -ge 32 ]; then GUEST_CPU_CORES="16" elif [ "${HOST_CPU_CORES}" -ge 16 ]; then GUEST_CPU_CORES="8" elif [ "${HOST_CPU_CORES}" -ge 8 ]; then GUEST_CPU_CORES="4" elif [ "${HOST_CPU_CORES}" -ge 4 ]; then GUEST_CPU_CORES="2" else GUEST_CPU_CORES="1" fi else GUEST_CPU_CORES="${cpu_cores}" fi # Windows 11 requires a minimum of 2 CPU cores to install # https://github.com/quickemu-project/quickemu/issues/1423 if [ "${guest_os}" == "windows" ] && [[ "${VMNAME}" == *"windows-11"* || "${VMNAME}" == *"win11"* ]]; then if [ "${GUEST_CPU_CORES}" -lt 2 ]; then echo " - CPU: Adjusting CPU cores from ${GUEST_CPU_CORES} to 2 (Windows 11 minimum requirement)" GUEST_CPU_CORES="2" fi fi # macOS guests cannot boot with most core counts not powers of 2. # Find the nearest but lowest power of 2 using a predefined table if [ "${guest_os}" == "macos" ]; then local POWERS=(1 2 4 8 16 32 64 128 256 512 1024) for (( i=${#POWERS[@]}-1; i>=0; i-- )); do if [ "${POWERS[i]}" -le "${GUEST_CPU_CORES}" ]; then GUEST_CPU_CORES="${POWERS[i]}" break fi done fi if [ "${OS_KERNEL}" == "Darwin" ]; then # Get the number of physical cores physicalcpu=$(sysctl -n hw.physicalcpu) # Get the number of logical processors logicalcpu=$(sysctl -n hw.logicalcpu) # Check if Hyper-Threading is enabled if [ "${logicalcpu}" -gt "${physicalcpu}" ]; then HOST_CPU_SMT="on" else HOST_CPU_SMT="off" fi elif [ -e /sys/devices/system/cpu/smt/control ]; then HOST_CPU_SMT=$(cat /sys/devices/system/cpu/smt/control) fi # Account for Hyperthreading/SMT. if [ "${GUEST_CPU_CORES}" -ge 2 ]; then case ${HOST_CPU_SMT} in on) GUEST_CPU_THREADS=2 GUEST_CPU_LOGICAL_CORES=$(( GUEST_CPU_CORES / GUEST_CPU_THREADS ));; *) GUEST_CPU_THREADS=1 GUEST_CPU_LOGICAL_CORES=${GUEST_CPU_CORES};; esac else GUEST_CPU_THREADS=1 GUEST_CPU_LOGICAL_CORES=${GUEST_CPU_CORES} fi SMP="-smp cores=${GUEST_CPU_LOGICAL_CORES},threads=${GUEST_CPU_THREADS},sockets=${HOST_CPU_SOCKETS}" echo " - CPU: ${HOST_CPU_MODEL}" echo " - CPU VM: ${CPU_MODEL%%,*}, ${HOST_CPU_SOCKETS} Socket(s), ${GUEST_CPU_LOGICAL_CORES} Core(s), ${GUEST_CPU_THREADS} Thread(s)" if [ "${guest_os}" == "macos" ] || [ "${guest_os}" == "windows" ] || [ "${guest_os}" == "windows-server" ]; then # Display MSRs alert if the guest is macOS or windows ignore_msrs_alert fi } function configure_ram() { local OS_PRETTY_NAME="" RAM_VM="2G" if [ -z "${ram}" ]; then local RAM_HOST="" if [ "${OS_KERNEL}" == "Darwin" ]; then RAM_HOST=$(($(sysctl -n hw.memsize) / (1048576*1024))) else # Determine the number of gigabytes of RAM in the host by extracting the first numerical value from the output. RAM_HOST=$(free --giga | tr ' ' '\n' | grep -m 1 "[0-9]" ) fi if [ "${RAM_HOST}" -ge 128 ]; then RAM_VM="32G" elif [ "${RAM_HOST}" -ge 64 ]; then RAM_VM="16G" elif [ "${RAM_HOST}" -ge 16 ]; then RAM_VM="8G" elif [ "${RAM_HOST}" -ge 8 ]; then RAM_VM="4G" fi else RAM_VM="${ram}" fi echo " - RAM VM: ${RAM_VM} RAM" case "${guest_os}" in windows|windows-server) OS_PRETTY_NAME="Windows" min_ram="4" ;; macos) OS_PRETTY_NAME="macOS" min_ram="8" ;; esac if [ -n "${min_ram}" ] && [ "${RAM_VM//G/}" -lt "${min_ram}" ]; then if [ -z "${ram}" ]; then echo " ERROR! The guest virtual machine has been allocated insufficient RAM to run ${OS_PRETTY_NAME}." echo " You can override the guest RAM allocation by adding 'ram=${min_ram}G' to ${VM}" exit 1 else echo " WARNING! You have allocated less than the recommended amount of RAM to run ${OS_PRETTY_NAME}." fi fi } function is_firmware_qcow2() { # Check for the magic bytes that indicate the firmware is in qcow2 format, # otherwise default to assuming firmware files are in raw format. # Use od to read bytes as hex to avoid null byte warnings in command substitution. local magic magic=$(od -An -tx1 -N4 "$1" 2>/dev/null | tr -d ' ') [ "$magic" = "514649fb" ] && echo "true" || echo "false" } # Derive QEMU share path from binary location # Handles Nix, Homebrew, MacPorts, system packages, custom builds function get_qemu_share_path() { local qemu_bin="${QEMU}" local qemu_real qemu_prefix share_path # Resolve the actual binary path (handles symlinks, Nix store paths) if command -v realpath &>/dev/null; then qemu_real=$(realpath "${qemu_bin}" 2>/dev/null) || qemu_real="${qemu_bin}" else # macOS fallback: follow symlink chain manually qemu_real="${qemu_bin}" while [ -L "${qemu_real}" ]; do local link_target link_target=$(readlink "${qemu_real}") # Handle relative symlinks if [[ "${link_target}" != /* ]]; then qemu_real="$(dirname "${qemu_real}")/${link_target}" else qemu_real="${link_target}" fi done fi # /path/to/bin/qemu-system-x86_64 -> /path/to/share qemu_prefix="$(dirname "$(dirname "${qemu_real}")")" share_path="${qemu_prefix}/share" # Validate: must contain qemu firmware directory if [ -d "${share_path}/qemu" ]; then echo "${share_path}" return 0 fi # Fallback for system installations echo "/usr/share" } function configure_bios() { # Always Boot macOS using EFI if [ "${guest_os}" == "macos" ]; then boot="efi" if [ -e "${VMDIR}/OVMF_CODE.fd" ] && [ -e "${VMDIR}/OVMF_VARS-1024x768.fd" ]; then EFI_CODE="${VMDIR}/OVMF_CODE.fd" EFI_VARS="${VMDIR}/OVMF_VARS-1024x768.fd" elif [ -e "${VMDIR}/OVMF_CODE.fd" ] && [ -e "${VMDIR}/OVMF_VARS-1920x1080.fd" ]; then EFI_CODE="${VMDIR}/OVMF_CODE.fd" EFI_VARS="${VMDIR}/OVMF_VARS-1920x1080.fd" else MAC_MISSING="Firmware" fi # Check for OpenCore bootloader if [ -e "${VMDIR}/OpenCore.qcow2" ]; then MAC_BOOTLOADER="${VMDIR}/OpenCore.qcow2" elif [ -e "${VMDIR}/ESP.qcow2" ]; then # Backwards compatibility for Clover MAC_BOOTLOADER="${VMDIR}/ESP.qcow2" else MAC_MISSING="Bootloader" fi if [ -n "${MAC_MISSING}" ]; then echo "ERROR! macOS ${MAC_MISSING} was not found." echo " Use 'quickget' to download the required files." exit 1 fi BOOT_STATUS="EFI (macOS), OVMF ($(basename "${EFI_CODE}")), SecureBoot (${secureboot})." elif [[ "${boot}" == *"efi"* ]]; then EFI_VARS="${VMDIR}/OVMF_VARS.fd" # Preserve backward compatibility if [ -e "${VMDIR}/${VMNAME}-vars.fd" ]; then mv "${VMDIR}/${VMNAME}-vars.fd" "${EFI_VARS}" elif [ -e "${VMDIR}/OVMF_VARS_4M.fd" ]; then mv "${VMDIR}/OVMF_VARS_4M.fd" "${EFI_VARS}" fi # OVMF_CODE_4M.fd is for booting guests in non-Secure Boot mode. # While this image technically supports Secure Boot, it does so # without requiring SMM support from QEMU # OVMF_CODE.secboot.fd is like OVMF_CODE_4M.fd, but will abort if QEMU # does not support SMM. local SHARE_PATH SHARE_PATH=$(get_qemu_share_path) # https://bugzilla.redhat.com/show_bug.cgi?id=1929357#c5 # TODO: Check if macOS should use 'edk2-i386-vars.fd' # Search for firmware if EFI_CODE is not set or the specified file doesn't exist if [ -z "${EFI_CODE}" ] || [ ! -e "${EFI_CODE}" ]; then if [ "${ARCH_VM}" == "aarch64" ]; then # AAVMF firmware paths for ARM64 guests # SecureBoot is not commonly supported on ARM64, use standard firmware # shellcheck disable=SC2054,SC2140 ovmfs=("/usr/share/AAVMF/AAVMF_CODE.fd","/usr/share/AAVMF/AAVMF_VARS.fd" \ "${SHARE_PATH}/edk2/aarch64/QEMU_CODE.fd","${SHARE_PATH}/edk2/aarch64/QEMU_VARS.fd" \ "${SHARE_PATH}/edk2/aarch64/QEMU_EFI-pflash.raw","${SHARE_PATH}/edk2/aarch64/vars-template-pflash.raw" \ "${SHARE_PATH}/qemu/edk2-aarch64-code.fd","${SHARE_PATH}/qemu/edk2-arm-vars.fd" \ "${SHARE_PATH}/AAVMF/AAVMF_CODE.fd","${SHARE_PATH}/AAVMF/AAVMF_VARS.fd" ) else # x86_64 OVMF firmware paths case ${secureboot} in on) # shellcheck disable=SC2054,SC2140 ovmfs=("${SHARE_PATH}/OVMF/OVMF_CODE_4M.secboot.fd","${SHARE_PATH}/OVMF/OVMF_VARS_4M.ms.fd" \ "${SHARE_PATH}/edk2/ovmf/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2/ovmf/OVMF_VARS.secboot.fd" \ "${SHARE_PATH}/OVMF/x64/OVMF_CODE.secboot.fd","${SHARE_PATH}/OVMF/x64/OVMF_VARS.fd" \ "${SHARE_PATH}/edk2-ovmf/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2-ovmf/OVMF_VARS.fd" \ "${SHARE_PATH}/qemu/ovmf-x86_64-smm-ms-code.bin","${SHARE_PATH}/qemu/ovmf-x86_64-smm-ms-vars.bin" \ "${SHARE_PATH}/qemu/edk2-x86_64-secure-code.fd","${SHARE_PATH}/qemu/edk2-x86_64-code.fd" \ "${SHARE_PATH}/edk2-ovmf/x64/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2-ovmf/x64/OVMF_VARS.fd" \ "${SHARE_PATH}/edk2/x64/OVMF_CODE.secboot.4m.fd","${SHARE_PATH}/edk2/x64/OVMF_VARS.4m.fd" \ "${SHARE_PATH}/edk2/ovmf/OVMF_CODE_4M.secboot.qcow2","${SHARE_PATH}/edk2/ovmf/OVMF_VARS_4M.secboot.qcow2" );; *) # shellcheck disable=SC2054,SC2140 ovmfs=("${SHARE_PATH}/OVMF/OVMF_CODE_4M.fd","${SHARE_PATH}/OVMF/OVMF_VARS_4M.fd" \ "${SHARE_PATH}/edk2/ovmf/OVMF_CODE.fd","${SHARE_PATH}/edk2/ovmf/OVMF_VARS.fd" \ "${SHARE_PATH}/OVMF/OVMF_CODE.fd","${SHARE_PATH}/OVMF/OVMF_VARS.fd" \ "${SHARE_PATH}/OVMF/x64/OVMF_CODE.fd","${SHARE_PATH}/OVMF/x64/OVMF_VARS.fd" \ "${SHARE_PATH}/edk2-ovmf/OVMF_CODE.fd","${SHARE_PATH}/edk2-ovmf/OVMF_VARS.fd" \ "${SHARE_PATH}/qemu/ovmf-x86_64-4m-code.bin","${SHARE_PATH}/qemu/ovmf-x86_64-4m-vars.bin" \ "${SHARE_PATH}/qemu/edk2-x86_64-code.fd","${SHARE_PATH}/qemu/edk2-x86_64-code.fd" \ "${SHARE_PATH}/edk2-ovmf/x64/OVMF_CODE.fd","${SHARE_PATH}/edk2-ovmf/x64/OVMF_VARS.fd" \ "${SHARE_PATH}/edk2/x64/OVMF_CODE.4m.fd","${SHARE_PATH}/edk2/x64/OVMF_VARS.4m.fd" \ "${SHARE_PATH}/edk2/ovmf/OVMF_CODE_4M.qcow2","${SHARE_PATH}/edk2/ovmf/OVMF_VARS_4M.qcow2" );; esac fi # Attempt each EFI_CODE file one by one, selecting the corresponding code and vars # when an existing file is found. _IFS=$IFS IFS="," for f in "${ovmfs[@]}"; do # shellcheck disable=SC2086 set -- ${f}; if [ -e "${1}" ]; then EFI_CODE="${1}" EFI_EXTRA_VARS="${2}" fi done IFS=$_IFS fi if [ -z "${EFI_CODE}" ] || [ ! -e "${EFI_CODE}" ]; then if [ "${secureboot}" == "on" ]; then echo "ERROR! SecureBoot was requested but no SecureBoot capable firmware was found." else echo "ERROR! EFI boot requested but no EFI firmware found." fi echo " Please install OVMF firmware." exit 1 fi if [ -n "${EFI_EXTRA_VARS}" ]; then if [ ! -e "${EFI_EXTRA_VARS}" ]; then echo " - EFI: ERROR! EFI_EXTRA_VARS file ${EFI_EXTRA_VARS} does not exist." exit 1 fi # Write destination vars file with correct extension # based on source format of the EFI_EXTRA_VARS file QCOW2VARS=$(is_firmware_qcow2 "${EFI_EXTRA_VARS}") if [ "${QCOW2VARS}" = "true" ]; then EFI_VARS="${VMDIR}/OVMF_VARS.qcow2" else EFI_VARS="${VMDIR}/OVMF_VARS.fd" fi efi_vars "${EFI_EXTRA_VARS}" "${EFI_VARS}" fi # Make sure EFI_VARS references an actual, writeable, file if [ ! -f "${EFI_VARS}" ] || [ ! -w "${EFI_VARS}" ]; then echo " - EFI: ERROR! ${EFI_VARS} is not a regular file or not writeable." echo " Deleting ${EFI_VARS}. Please re-run quickemu." rm -f "${EFI_VARS}" exit 1 fi # If EFI_CODE references a symlink, resolve it to the real file. if [ -L "${EFI_CODE}" ]; then echo " - EFI: WARNING! ${EFI_CODE} is a symlink." echo -n " Resolving to... " EFI_CODE=$(realpath "${EFI_CODE}") echo "${EFI_CODE}" fi BOOT_STATUS="EFI (${guest_os^}), OVMF (${EFI_CODE}), EFI Vars (${EFI_VARS}), SecureBoot (${secureboot})." else BOOT_STATUS="Legacy BIOS (${guest_os^})" boot="legacy" secureboot="off" # Legacy BIOS boot requires the i440fx/PIIX3 chipset (pc), not Q35 MACHINE_TYPE="pc" fi echo " - BOOT: ${BOOT_STATUS}" } function configure_os_quirks() { if [ "${guest_os}" == "batocera" ] || [ "${guest_os}" == "haiku" ] || [ "${guest_os}" == "kolibrios" ]; then NET_DEVICE="rtl8139" fi if [ "${guest_os}" == "freebsd" ] || [ "${guest_os}" == "ghostbsd" ]; then mouse="usb" fi case ${guest_os} in windows-server) NET_DEVICE="e1000";; *bsd|linux*|windows) NET_DEVICE="virtio-net-pci";; freedos) sound_card="sb16" NET_DEVICE="pcnet";; *solaris) usb_controller="xhci" sound_card="ac97";; reactos) NET_DEVICE="e1000" keyboard="ps2";; macos) # Tune QEMU optimisations based on the macOS release, or fallback to lowest # common supported options if none is specified. # * VirtIO Block Media doesn't work in High Sierra (at all) or the Mojave (Recovery Image) # * VirtIO Network is supported since Big Sur # * VirtIO Memory Balloning is supported since Big Sur (https://pmhahn.github.io/virtio-balloon/) # * VirtIO RNG is supported since Big Sur, but exposed to all guests by default. case ${macos_release} in monterey|ventura|sonoma|sequoia|tahoe) # macOS 12+ (Monterey onwards) supports virtio-sound-pci natively BALLOON="-device virtio-balloon" MAC_DISK_DEV="virtio-blk-pci" NET_DEVICE="virtio-net-pci" USB_HOST_PASSTHROUGH_CONTROLLER="nec-usb-xhci" GUEST_TWEAKS+=" -global nec-usb-xhci.msi=off" sound_card="virtio-sound-pci" usb_controller="xhci";; big-sur) # Big Sur uses VirtIO but usb-audio/VoodooHDA don't work reliably # Fall back to ich9-intel-hda which may work with VoodooHDA BALLOON="-device virtio-balloon" MAC_DISK_DEV="virtio-blk-pci" NET_DEVICE="virtio-net-pci" USB_HOST_PASSTHROUGH_CONTROLLER="nec-usb-xhci" GUEST_TWEAKS+=" -global nec-usb-xhci.msi=off" sound_card="ich9-intel-hda" usb_controller="xhci";; *) # Backwards compatibility if no macos_release is specified. # Also safe catch all for High Sierra and Mojave BALLOON="" if [ "${macos_release}" == "catalina" ]; then MAC_DISK_DEV="virtio-blk-pci" else MAC_DISK_DEV="ide-hd,bus=ahci.2" fi NET_DEVICE="vmxnet3" USB_HOST_PASSTHROUGH_CONTROLLER="usb-ehci";; esac ;; *) NET_DEVICE="rtl8139";; esac } function configure_storage() { local create_options="" echo " - Disk: ${disk_img} (${disk_size})" if [ ! -f "${disk_img}" ]; then # If there is no disk image, create a new image. mkdir -p "${VMDIR}" 2>/dev/null case ${preallocation} in off|metadata|falloc|full) true;; *) echo "ERROR! ${preallocation} is an unsupported disk preallocation option." exit 1;; esac case ${disk_format} in qcow2) create_options="lazy_refcounts=on,preallocation=${preallocation},nocow=on";; raw) create_options="preallocation=${preallocation}";; *) true;; esac # https://blog.programster.org/qcow2-performance if ! ${QEMU_IMG} create -q -f "${disk_format}" -o "${create_options=}" "${disk_img}" "${disk_size}"; then echo "ERROR! Failed to create ${disk_img} using ${disk_format} format." exit 1 fi if [ -z "${iso}" ] && [ -z "${img}" ]; then echo "ERROR! You haven't specified a .iso or .img image to boot from." exit 1 fi echo " Just created, booting from ${iso}${img}" DISK_USED="no" elif [ -e "${disk_img}" ]; then # If the VM is not running, check for disk related issues. if [ -z "${VM_PID}" ]; then # Check there isn't already a process attached to the disk image. if ! ${QEMU_IMG} info "${disk_img}" >/dev/null; then echo " Failed to get \"write\" lock. Is another process using the disk?" exit 1 fi else if ! ${QEMU_IMG} check -q "${disk_img}"; then echo " Disk integrity check failed. Please run qemu-img check --help." echo "${QEMU_IMG}" check "${disk_img}" exit 1 fi fi # Only check disk image size if preallocation is off if [ "${preallocation}" == "off" ]; then DISK_CURR_SIZE=$(${STAT} -c%s "${disk_img}") if [ "${DISK_CURR_SIZE}" -le "${DISK_MIN_SIZE}" ]; then echo " Looks unused, booting from ${iso}${img}" if [ -z "${iso}" ] && [ -z "${img}" ]; then echo "ERROR! You haven't specified a .iso or .img image to boot from." exit 1 fi else DISK_USED="yes" fi else DISK_USED="yes" fi fi if [ "${DISK_USED}" == "yes" ] && [ "${guest_os}" != "kolibrios" ]; then # If there is a disk image that appears to be used do not boot from installation media. iso="" img="" fi # Has the status quo been requested? if [ "${STATUS_QUO}" == "-snapshot" ]; then if [ -z "${img}" ] && [ -z "${iso}" ]; then echo " Existing disk state will be preserved, no writes will be committed." fi fi if [ -n "${iso}" ] && [ -e "${iso}" ]; then echo " - Boot ISO: ${iso}" elif [ -n "${img}" ] && [ -e "${img}" ]; then echo " - Recovery: ${img}" fi if [ -n "${fixed_iso}" ] && [ -e "${fixed_iso}" ]; then echo " - CD-ROM: ${fixed_iso}" fi } function check_cocoa_gl_es_support() { [ "${OS_KERNEL}" != "Darwin" ] && return 1 # Test QEMU directly for gl=es support - most reliable method # This catches both missing OpenGL build support and missing ANGLE libraries if "${QEMU}" -display cocoa,gl=es -M none 2>&1 | grep -Eqi "OpenGL support was not enabled|does not accept"; then return 1 fi # Fallback: check for ANGLE libraries if QEMU test is inconclusive # Resolve QEMU's real path (follows Nix symlinks) local qemu_real qemu_dir qemu_prefix qemu_real=$(realpath "${QEMU}" 2>/dev/null || readlink -f "${QEMU}" 2>/dev/null || echo "${QEMU}") qemu_dir=$(dirname "${qemu_real}") qemu_prefix="${qemu_dir%/bin}" local angle_libs=( "${qemu_prefix}/lib/libEGL.dylib" "/opt/homebrew/lib/libEGL.dylib" "/usr/local/lib/libEGL.dylib" ) # Also check DYLD paths if set (covers additional Nix scenarios) if [ -n "${DYLD_LIBRARY_PATH:-}" ]; then local IFS=':' for path in ${DYLD_LIBRARY_PATH}; do angle_libs+=("${path}/libEGL.dylib") done fi for lib in "${angle_libs[@]}"; do [ -f "$lib" ] && return 0 done return 1 } function configure_display() { # Determine which audio driver to use: PipeWire, PulseAudio, or ALSA # Socket detection is more reliable than process detection on headless servers local AUDIO_DRIVER="alsa" local pw_socket="${PIPEWIRE_REMOTE:-${XDG_RUNTIME_DIR}/pipewire-0}" local pa_socket="${PULSE_SERVER:-${XDG_RUNTIME_DIR}/pulse/native}" if [ "${QEMU_VER_SHORT}" -ge 81 ] && [ -S "${pw_socket}" ]; then # QEMU's pipewire audio backend was added in version 8.1 AUDIO_DRIVER="pipewire" elif [ -S "${pa_socket}" ]; then AUDIO_DRIVER="pa" fi # Setup the appropriate audio device based on the display output # https://www.kraxel.org/blog/2020/01/qemu-sound-audiodev/ case ${display} in cocoa) AUDIO_DEV="coreaudio,id=audio0";; none|spice|spice-app) AUDIO_DEV="spice,id=audio0";; *) AUDIO_DEV="${AUDIO_DRIVER},id=audio0";; esac # Determine a sane resolution for Linux guests. local X_RES="1280" local Y_RES="800" if [ -n "${width}" ] && [ -n "${height}" ]; then local X_RES="${width}" local Y_RES="${height}" fi # https://www.kraxel.org/blog/2019/09/display-devices-in-qemu/ case ${guest_os} in *bsd) DISPLAY_DEVICE="VGA";; linux_old|solaris) DISPLAY_DEVICE="vmware-svga";; linux) # ARM64 does not have VGA hardware - use virtio-gpu-pci instead of virtio-vga if [ "${ARCH_VM}" == "aarch64" ]; then DISPLAY_DEVICE="virtio-gpu-pci" else case ${display} in none|spice|spice-app) DISPLAY_DEVICE="virtio-gpu";; *) DISPLAY_DEVICE="virtio-vga";; esac fi;; macos) # macOS has native VMware display driver support; aligns with OSX-KVM DISPLAY_DEVICE="vmware-svga";; windows|windows-server) case ${display} in none|spice) DISPLAY_DEVICE="qxl-vga";; cocoa|gtk|sdl|spice-app) DISPLAY_DEVICE="virtio-vga";; esac;; *) DISPLAY_DEVICE="qxl-vga";; esac # Map Quickemu $display to QEMU -display case ${display} in cocoa) # macOS: prefer OpenGL ES (via ANGLE) for stability and performance # ANGLE provides OpenGL ES on macOS through Metal, which is more stable # than the deprecated native OpenGL implementation # Reference: https://gist.github.com/akihikodaki/87df4149e7ca87f18dc56807ec5a1bc5 if [ "${gl}" == "on" ] && check_cocoa_gl_es_support; then DISPLAY_RENDER="${display},gl=es" gl="es" else DISPLAY_RENDER="${display}" [ "${gl}" == "on" ] && gl="off" fi;; gtk) DISPLAY_RENDER="${display},grab-on-hover=on,zoom-to-fit=off,gl=${gl}";; none) DISPLAY_RENDER="none" gl="off";; # No display backend means no GL context spice) # For local SPICE with GL, use egl-headless to provide the GL context # for VirGL. Don't use gl=on in SPICE itself as it blocks the main loop. if [ -z "${ACCESS}" ] || [ "${ACCESS}" == "local" ]; then if [ "${gl}" == "on" ]; then DISPLAY_RENDER="egl-headless,rendernode=/dev/dri/renderD128" else DISPLAY_RENDER="none" fi else # Remote access cannot use GL DISPLAY_RENDER="none" gl="off" fi;; sdl) DISPLAY_RENDER="${display},gl=${gl}";; spice-app) DISPLAY_RENDER="${display},gl=${gl}";; *) DISPLAY_RENDER="${display}";; esac # https://www.kraxel.org/blog/2021/05/virtio-gpu-qemu-graphics-update/ # For GL-enabled displays, check if dedicated GL device variants are available. # Note: virtio-gpu-pci becomes virtio-gpu-gl-pci (not virtio-gpu-pci-gl) if [ "${gl}" != "off" ] && [[ "${DISPLAY_DEVICE}" =~ ^virtio-(vga|gpu|gpu-pci)$ ]]; then local GL_DEVICE="" case "${DISPLAY_DEVICE}" in virtio-gpu-pci) GL_DEVICE="virtio-gpu-gl-pci";; virtio-gpu) GL_DEVICE="virtio-gpu-gl";; virtio-vga) GL_DEVICE="virtio-vga-gl";; esac if "${QEMU}" -device help 2>&1 | grep -q "\"${GL_DEVICE}\""; then DISPLAY_DEVICE="${GL_DEVICE}" fi echo -n " - Display: ${display^^}, ${DISPLAY_DEVICE}, GL (${gl}), VirGL (on)" else echo -n " - Display: ${display^^}, ${DISPLAY_DEVICE}, GL (${gl}), VirGL (off)" fi # Disable default VGA for SPICE modes to prevent duplicate scanouts # SPICE creates its own display output; the default VGA would create a second one case ${display} in none|spice|spice-app) VGA="-vga none";; *) VGA="";; esac # Build the video configuration VIDEO="${VGA:+${VGA} }-device ${DISPLAY_DEVICE}" # ARM64 needs ramfb for UEFI boot display before virtio-gpu driver loads if [ "${ARCH_VM}" == "aarch64" ]; then VIDEO="-device ramfb ${VIDEO}" fi # Set display resolution for devices that support xres/yres parameters # Use (,|$) anchor to match device names with or without comma-separated parameters # Pattern ordered most-specific to least-specific for clarity (vga-gl before vga, etc.) if [[ "${DISPLAY_DEVICE}" =~ ^(virtio-(vga|vga-gl|gpu|gpu-gl|gpu-pci|gpu-gl-pci)|qxl|qxl-vga|bochs-display)(,|$) ]]; then VIDEO="${VIDEO},xres=${X_RES},yres=${Y_RES}" echo " @ (${X_RES} x ${Y_RES})" else echo " " fi # Allocate VRAM to VGA devices # Note: virtio devices (virtio-vga, virtio-gpu-pci, and their -gl variants) use # dynamic memory management and don't require explicit VRAM allocation via parameters. # They use QEMU's default max_hostmem setting (256 MiB) which is sufficient for most use cases. case ${DISPLAY_DEVICE} in bochs-display) VIDEO="${VIDEO},vgamem=67108864";; qxl|qxl-vga) VIDEO="${VIDEO},ram_size=65536,vram_size=65536,vgamem_mb=64";; ati-vga|cirrus-vga|VGA|vmware-svga) VIDEO="${VIDEO},vgamem_mb=256";; esac # Configure multiscreen if max_outputs was provided in the .conf file if [ -n "${max_outputs}" ]; then VIDEO="${VIDEO},max_outputs=${max_outputs}" fi # Add fullscreen options VIDEO="${VIDEO} ${FULLSCREEN}" } function configure_audio() { # Build the sound hardware configuration case ${sound_card} in ich9-intel-hda|intel-hda) SOUND="-device ${sound_card} -device ${sound_duplex},audiodev=audio0";; usb-audio) SOUND="-device ${sound_card},audiodev=audio0";; virtio-sound-pci) SOUND="-device ${sound_card},audiodev=audio0";; ac97|es1370|sb16) SOUND="-device ${sound_card},audiodev=audio0";; none) SOUND="";; esac echo " - Sound: ${sound_card} (${sound_duplex})" } function configure_ports() { echo -n "" > "${VMDIR}/${VMNAME}.ports" rm -f "${VMDIR}/${VMNAME}.spice" rm -f "${VMDIR}/${VMNAME}.sock" if [ -z "${ssh_port}" ]; then # Find a free port to expose ssh to the guest ssh_port=$(get_port 22220 9) fi if [ -n "${ssh_port}" ]; then echo "ssh,${ssh_port}" >> "${VMDIR}/${VMNAME}.ports" NET="${NET},hostfwd=tcp::${ssh_port}-:22" echo " - ssh: On host: ssh user@localhost -p ${ssh_port}" else echo " - ssh: All ssh ports have been exhausted." fi # Have any port forwards been requested? if (( ${#port_forwards[@]} )); then echo " - PORTS: Port forwards requested:" for FORWARD in "${port_forwards[@]}"; do HOST_PORT=$(echo "${FORWARD}" | cut -d':' -f1) GUEST_PORT=$(echo "${FORWARD}" | cut -d':' -f2) echo " - ${HOST_PORT} => ${GUEST_PORT}" NET="${NET},hostfwd=tcp::${HOST_PORT}-:${GUEST_PORT}" NET="${NET},hostfwd=udp::${HOST_PORT}-:${GUEST_PORT}" done fi if [ "${display}" == "none" ] || [ "${display}" == "spice" ] || [ "${display}" == "spice-app" ]; then SPICE="disable-ticketing=on" if [ "${display}" == "spice-app" ]; then # spice-app uses QEMU's built-in viewer with GL support SPICE+=",gl=${gl}" echo " - SPICE: Enabled" elif [ "${display}" == "spice" ]; then # For spice display, use Unix socket for local or TCP for remote if [ -z "${ACCESS}" ] || [ "${ACCESS}" == "local" ]; then # Unix socket mode for local access # GL context is provided by egl-headless display, not SPICE SPICE+=",unix=on,addr=${VMDIR}/${VMNAME}.sock" echo "unix,${VMDIR}/${VMNAME}.sock" >> "${VMDIR}/${VMNAME}.ports" echo "${VMDIR}/${VMNAME}.sock" > "${VMDIR}/${VMNAME}.spice" echo -n " - SPICE: On host: spicy --uri=\"spice+unix://${VMDIR}/${VMNAME}.sock\" --title \"${VMNAME}\"" if [ "${guest_os}" != "macos" ] && [ -n "${PUBLIC}" ]; then echo -n " --spice-shared-dir ${PUBLIC}" fi echo "${FULLSCREEN}" else # TCP mode for remote access (no GL support) if [ -z "${spice_port}" ]; then spice_port=$(get_port 5930 9) fi if [ "${ACCESS}" == "remote" ]; then SPICE_ADDR="" else SPICE_ADDR="${ACCESS}" fi if [ -z "${spice_port}" ]; then echo " - SPICE: All SPICE ports have been exhausted." echo " ERROR! Requested SPICE display, but no SPICE ports are free." exit 1 fi SPICE+=",port=${spice_port},addr=${SPICE_ADDR}" echo "spice,${spice_port}" >> "${VMDIR}/${VMNAME}.ports" echo "${spice_port}" > "${VMDIR}/${VMNAME}.spice" echo -n " - SPICE: On host: spicy --title \"${VMNAME}\" --port ${spice_port}" if [ "${guest_os}" != "macos" ] && [ -n "${PUBLIC}" ]; then echo -n " --spice-shared-dir ${PUBLIC}" fi echo "${FULLSCREEN}" fi elif [ "${display}" == "none" ]; then # display=none with SPICE for headless VMs - use TCP for remote access if [ -z "${spice_port}" ]; then spice_port=$(get_port 5930 9) fi if [ -z "${ACCESS}" ]; then SPICE_ADDR="127.0.0.1" elif [ "${ACCESS}" == "remote" ]; then SPICE_ADDR="" elif [ "${ACCESS}" == "local" ]; then SPICE_ADDR="127.0.0.1" else SPICE_ADDR="${ACCESS}" fi if [ -z "${spice_port}" ]; then echo " - SPICE: All SPICE ports have been exhausted." echo " ERROR! Requested SPICE display, but no SPICE ports are free." exit 1 fi SPICE+=",port=${spice_port},addr=${SPICE_ADDR}" echo "spice,${spice_port}" >> "${VMDIR}/${VMNAME}.ports" echo "${spice_port}" > "${VMDIR}/${VMNAME}.spice" echo -n " - SPICE: On host: spicy --title \"${VMNAME}\" --port ${spice_port}" if [ "${guest_os}" != "macos" ] && [ -n "${PUBLIC}" ]; then echo -n " --spice-shared-dir ${PUBLIC}" fi echo "${FULLSCREEN}" fi fi } function configure_file_sharing() { if [ -n "${PUBLIC}" ]; then # WebDAV case ${guest_os} in macos) if [ "${display}" == "none" ] || [ "${display}" == "spice" ] || [ "${display}" == "spice-app" ]; then # Reference: https://gitlab.gnome.org/GNOME/phodav/-/issues/5 echo " - WebDAV: On guest: build spice-webdavd (https://gitlab.gnome.org/GNOME/phodav/-/merge_requests/24)" echo " - WebDAV: On guest: Finder -> Connect to Server -> http://localhost:9843/" fi;; *) echo " - WebDAV: On guest: dav://localhost:9843/";; esac # 9P if [ "${guest_os}" != "windows" ] || [ "${guest_os}" == "windows-server" ]; then echo -n " - 9P: On guest: " if [ "${guest_os}" == "linux" ]; then echo "sudo mount -t 9p -o trans=virtio,version=9p2000.L,msize=104857600 ${PUBLIC_TAG} ~/$(basename "${PUBLIC}")" elif [ "${guest_os}" == "macos" ]; then # PUBLICSHARE needs to be world writeable for seamless integration with # macOS. Test if it is world writeable, and prompt what to do if not. echo "sudo mount_9p ${PUBLIC_TAG}" if [ "${PUBLIC_PERMS}" != "drwxrwxrwx" ]; then echo " - 9P: On host: chmod 777 ${PUBLIC}" echo " Required for macOS integration 👆" fi fi fi # SMB # We need to search in NixOS compatible paths as well as the standard location # since /usr/sbin/smbd may not be in the PATH. if [ -x "$(command -v smbd)" ] || [ -x "/usr/sbin/smbd" ]; then NET+=",smb=${PUBLIC}" echo " - smbd: On guest: smb://10.0.2.4/qemu" fi fi } function configure_tpm() { # Start TPM if [ "${tpm}" == "on" ]; then local tpm_args=() # shellcheck disable=SC2054 tpm_args+=(socket --ctrl type=unixio,path="${VMDIR}/${VMNAME}.swtpm-sock" --terminate --tpmstate dir="${VMDIR}" --tpm2) echo "${SWTPM} ${tpm_args[*]} &" >> "${VMDIR}/${VMNAME}.sh" ${SWTPM} "${tpm_args[@]}" >> "${VMDIR}/${VMNAME}.log" & echo " - TPM: ${VMDIR}/${VMNAME}.swtpm-sock (${!})" sleep 0.25 fi } function configure_cpu_pinning() { if [ -z "${CORE_MAPPING}" ]; then return fi GUEST_CPUS="" idx=0 for tid_dir in /proc/"${VM_PID}"/task/*; do tid=$(basename "$tid_dir") name=$(cat "$tid_dir/comm") if [[ "$name" == CPU* ]]; then # Map per core, if threads are specified pin them to the same core core_idx=$(( idx / GUEST_CPU_THREADS )) host_cpu=${CORE_MAPPING[$core_idx]} if (( idx % GUEST_CPU_THREADS == 0 )); then [[ -n "$GUEST_CPUS" ]] && GUEST_CPUS+="," GUEST_CPUS+="$core_idx" fi taskset -cp "$host_cpu" "$tid" &>/dev/null idx=$((idx + 1)) fi done echo " - CPU Pinning: Bind guest cores to host cores (${GUEST_CPUS} -> ${CPU_PINNING})" } function vm_boot() { AUDIO_DEV="" BALLOON="-device virtio-balloon" BOOT_STATUS="" CPU="" CORE_MAPPING="" DISK_USED="" DISPLAY_DEVICE="" DISPLAY_RENDER="" EFI_CODE="" EFI_VARS="" GUEST_CPU_CORES="" GUEST_CPU_LOGICAL_CORES="" GUEST_CPU_THREADS="" HOST_CPU_CORES="" HOST_CPU_SMT="" HOST_CPU_SOCKETS="" HOST_CPU_VENDOR="" GUEST_TWEAKS="" KERNEL_NAME="Unknown" KERNEL_NODE="" KERNEL_VER="?" OS_RELEASE="Unknown OS" MACHINE_TYPE="${MACHINE_TYPE:-q35}" MAC_BOOTLOADER="" MAC_MISSING="" MAC_DISK_DEV="${MAC_DISK_DEV:-ide-hd,bus=ahci.2}" NET_DEVICE="${NET_DEVICE:-virtio-net-pci}" SOUND="" SPICE="" SMM="${SMM:-off}" local TEMP_PORT="" USB_HOST_PASSTHROUGH_CONTROLLER="qemu-xhci" VIDEO="" KERNEL_NAME="$(uname -s)" KERNEL_NODE="$(uname -n | cut -d'.' -f 1)" KERNEL_VER="$(uname -r)" if [ "${OS_KERNEL}" == "Darwin" ]; then # Get macOS product name and version using swvers if [ -x "$(command -v sw_vers)" ]; then OS_RELEASE="$(sw_vers -productName) $(sw_vers -productVersion)" fi elif [ -e /etc/os-release ]; then OS_RELEASE=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2) fi echo "Quickemu ${VERSION} using ${QEMU} v${QEMU_VER_LONG}" echo " - Host: ${OS_RELEASE} running ${KERNEL_NAME} ${KERNEL_VER} ${KERNEL_NODE}" # Force to lowercase. boot=${boot,,} guest_os=${guest_os,,} args=() # Set the hostname of the VM NET="user,hostname=${VMNAME}" echo "#!/usr/bin/env bash" > "${VMDIR}/${VMNAME}.sh" configure_cpu configure_ram check_macos_tsc_stability configure_bios configure_os_quirks configure_storage configure_display configure_audio configure_ports configure_file_sharing configure_usb configure_tpm # Changing process name is not supported on macOS if [ "${OS_KERNEL}" == "Linux" ]; then # shellcheck disable=SC2054,SC2206,SC2140 args+=(-name ${VMNAME},process=${VMNAME},debug-threads=on) fi # Build machine arguments - SMM and vmport are x86-only options # SMM (System Management Mode) is an x86-specific CPU mode used for firmware operations # and is required for Secure Boot. ARM64 uses different mechanisms for firmware security. # vmport emulates VMware's I/O port for guest tools, which is also x86-specific. # # TCG-specific optimisations for cross-architecture emulation: # - Use -accel tcg,... to specify tb-size and thread options # - QEMU does not allow both -machine accel= and -accel simultaneously # - For KVM/HVF, continue using -machine accel= (simpler, no extra options needed) if [ "${QEMU_ACCEL}" == "tcg" ]; then local HOST_RAM_GB=0 if [ "${OS_KERNEL}" == "Darwin" ]; then HOST_RAM_GB=$(($(sysctl -n hw.memsize) / (1024*1024*1024))) else HOST_RAM_GB=$(awk '/MemTotal/ {printf "%.0f", $2/1024/1024}' /proc/meminfo) fi # Use larger translation cache on hosts with 16GB+ RAM local TCG_TB_SIZE=256 if [ "${HOST_RAM_GB}" -ge 16 ]; then TCG_TB_SIZE=512 fi # shellcheck disable=SC2054,SC2206 args+=(-accel tcg,tb-size=${TCG_TB_SIZE},thread=multi) if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64 uses 'virt' machine type without x86-specific options # shellcheck disable=SC2054,SC2206,SC2140 args+=(-machine ${MACHINE_TYPE} ${GUEST_TWEAKS} ${CPU} ${SMP} -m ${RAM_VM} ${BALLOON} -pidfile "${VMDIR}/${VMNAME}.pid") else # x86_64 includes SMM (System Management Mode) and vmport options # shellcheck disable=SC2054,SC2206,SC2140 args+=(-machine ${MACHINE_TYPE},smm=${SMM},vmport=off ${GUEST_TWEAKS} ${CPU} ${SMP} -m ${RAM_VM} ${BALLOON} -pidfile "${VMDIR}/${VMNAME}.pid") fi else # KVM/HVF: use -machine accel= (no extra options needed) if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64 uses 'virt' machine type without x86-specific options # shellcheck disable=SC2054,SC2206,SC2140 args+=(-machine ${MACHINE_TYPE},accel=${QEMU_ACCEL} ${GUEST_TWEAKS} ${CPU} ${SMP} -m ${RAM_VM} ${BALLOON} -pidfile "${VMDIR}/${VMNAME}.pid") else # x86_64 includes SMM (System Management Mode) and vmport options # shellcheck disable=SC2054,SC2206,SC2140 args+=(-machine ${MACHINE_TYPE},smm=${SMM},vmport=off,accel=${QEMU_ACCEL} ${GUEST_TWEAKS} ${CPU} ${SMP} -m ${RAM_VM} ${BALLOON} -pidfile "${VMDIR}/${VMNAME}.pid") fi fi if [ "${guest_os}" == "windows" ] || [ "${guest_os}" == "windows-server" ] || [ "${guest_os}" == "reactos" ] || [ "${guest_os}" == "freedos" ]; then # shellcheck disable=SC2054 args+=(-rtc base=localtime,clock=host,driftfix=slew) else # shellcheck disable=SC2054 args+=(-rtc base=utc,clock=host) fi # shellcheck disable=SC2206 args+=(${VIDEO} -display ${DISPLAY_RENDER}) # Only enable SPICE is using SPICE display if [ "${display}" == "none" ] || [ "${display}" == "spice" ] || [ "${display}" == "spice-app" ]; then # shellcheck disable=SC2054 args+=(-spice "${SPICE}" -device virtio-serial-pci -chardev socket,id=agent0,path="${VMDIR}/${VMNAME}-agent.sock",server=on,wait=off -device virtserialport,chardev=agent0,name=org.qemu.guest_agent.0 -chardev spicevmc,id=vdagent0,name=vdagent -device virtserialport,chardev=vdagent0,name=com.redhat.spice.0 -chardev spiceport,id=webdav0,name=org.spice-space.webdav.0 -device virtserialport,chardev=webdav0,name=org.spice-space.webdav.0) fi # shellcheck disable=SC2054 args+=(-device virtio-rng-pci,rng=rng0 -object rng-random,id=rng0,filename=/dev/urandom) # macOS doesn't support SPICE if [ "${OS_KERNEL}" == "Linux" ]; then # shellcheck disable=SC2054 args+=(-device "${USB_HOST_PASSTHROUGH_CONTROLLER}",id=spicepass -chardev spicevmc,id=usbredirchardev1,name=usbredir -device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 -chardev spicevmc,id=usbredirchardev2,name=usbredir -device usb-redir,chardev=usbredirchardev2,id=usbredirdev2 -chardev spicevmc,id=usbredirchardev3,name=usbredir -device usb-redir,chardev=usbredirchardev3,id=usbredirdev3 -device pci-ohci,id=smartpass -device usb-ccid) if ${QEMU} -device help | grep -q "passthrough smartcard"; then # shellcheck disable=SC2054 args+=(-chardev spicevmc,id=ccid,name=smartcard -device ccid-card-passthru,chardev=ccid) else echo " - WARNING! ${QEMU} or SPICE was not compiled with support for smartcard devices" fi fi # setup usb-controller if [ "${usb_controller}" == "ehci" ]; then # shellcheck disable=SC2054 args+=(-device usb-ehci,id=input) elif [ "${usb_controller}" == "xhci" ]; then # shellcheck disable=SC2054 args+=(-device qemu-xhci,id=input) elif [ "${usb_controller}" == "none" ]; then # add nothing : else echo " - WARNING! Unknown usb-controller value: '${usb_controller}'" fi # setup keyboard # @INFO: must be set after usb-controller if [ "${keyboard}" == "usb" ]; then # shellcheck disable=SC2054 args+=(-device usb-kbd,bus=input.0) elif [ "${keyboard}" == "virtio" ]; then # shellcheck disable=SC2054 args+=(-device virtio-keyboard) elif [ "${keyboard}" == "ps2" ]; then # add nothing, default is ps/2 keyboard : else echo " - WARNING! Unknown keyboard value: '${keyboard}'; Fallback to ps2" fi # setup keyboard_layout # @INFO: When using the VNC display, you must use the -k parameter to set the keyboard layout if you are not using en-us. if [ -n "${keyboard_layout}" ]; then args+=(-k "${keyboard_layout}") fi # Braille requires SDL, so disable for macOS if [ -n "${BRAILLE}" ] && [ "${OS_KERNEL}" == "Linux" ]; then if ${QEMU} -chardev help | grep -q braille; then # shellcheck disable=SC2054 #args+=(-chardev braille,id=brltty # -device usb-braille,id=usbbrl,chardev=brltty) args+=(-usbdevice braille) else echo " - WARNING! ${QEMU} does not support -chardev braille " fi fi # Validate input of the CPU_PINNING, pinning is not supported on macOS if [ -n "${CPU_PINNING}" ] && [ "${OS_KERNEL}" == "Linux" ]; then if ! [[ "${CPU_PINNING}" =~ ^[0-9]+(,[0-9]+)*$ ]]; then echo " - ERROR! Couldn't parse CPU pinning: '${CPU_PINNING}', only comma-separated list is supported" exit 1 fi IFS=',' read -r -a CORE_MAPPING <<< "$CPU_PINNING" NUM_CORE_MAPPING=${#CORE_MAPPING[@]} if [ "$NUM_CORE_MAPPING" -ne "$GUEST_CPU_LOGICAL_CORES" ]; then echo " - ERROR! Number of host cores for pinning should be equal to VM core count ($NUM_CORE_MAPPING != $GUEST_CPU_LOGICAL_CORES)" exit 1 fi fi # setup mouse # @INFO: must be set after usb-controller if [ "${mouse}" == "usb" ]; then # shellcheck disable=SC2054 args+=(-device usb-mouse,bus=input.0) elif [ "${mouse}" == "tablet" ]; then # shellcheck disable=SC2054 args+=(-device usb-tablet,bus=input.0) elif [ "${mouse}" == "virtio" ]; then # shellcheck disable=SC2054 args+=(-device virtio-mouse) elif [ "${mouse}" == "ps2" ]; then # add nothing, default is ps/2 mouse : else echo " - WARNING! Unknown mouse value: '${mouse}'; Falling back to ps2" fi # setup audio # @INFO: must be set after usb-controller; in case usb-audio is used # shellcheck disable=SC2206 args+=(-audiodev ${AUDIO_DEV} ${SOUND}) # $bridge backwards compatibility for Quickemu <= 4.0 if [ -n "${bridge}" ]; then network="${bridge}" fi if [ "${network}" == "none" ]; then # Disable all networking echo " - Network: Disabled" args+=(-nic none) elif [ "${network}" == "restrict" ]; then echo " - Network: Restricted (${NET_DEVICE})" # shellcheck disable=SC2054,SC2206 args+=(-device ${NET_DEVICE},netdev=nic -netdev ${NET},restrict=y,id=nic) elif [ -n "${network}" ]; then # Enable bridge mode networking echo " - Network: Bridged (${network})" # If a persistent MAC address is provided, use it. local MAC="" if [ -n "${macaddr}" ]; then MAC=",mac=${macaddr}" fi # shellcheck disable=SC2054,SC2206 args+=(-nic bridge,br=${network},model=${NET_DEVICE}${MAC}) else echo " - Network: User (${NET_DEVICE})" # shellcheck disable=SC2054,SC2206 args+=(-device ${NET_DEVICE},netdev=nic -netdev ${NET},id=nic) fi # Add the disks # - https://turlucode.com/qemu-disk-io-performance-comparison-native-or-threads-windows-10-version/ # Optimise disk I/O: enable TRIM/discard, zero detection for thin provisioning, # writeback caching and threaded async I/O DRIVE_OPTIMISATIONS="discard=unmap,detect-zeroes=unmap,cache=writeback,aio=threads" if [[ "${boot}" == *"efi"* ]]; then QCOW2CODE=$(is_firmware_qcow2 "${EFI_CODE}") QCOW2VARS=$(is_firmware_qcow2 "${EFI_VARS}") if [ "${QCOW2CODE}" = "true" ]; then EFI_CODE_FORMAT="qcow2"; else EFI_CODE_FORMAT="raw"; fi if [ "${QCOW2VARS}" = "true" ]; then EFI_VARS_FORMAT="qcow2"; else EFI_VARS_FORMAT="raw"; fi if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64 uses blockdev with named nodes referenced by machine pflash parameters # Do NOT use -global cfi.pflash01 secure property - that's x86 SMM-specific # shellcheck disable=SC2054 args+=(-blockdev node-name=rom,driver=file,filename="${EFI_CODE}",read-only=true -blockdev node-name=efivars,driver=file,filename="${EFI_VARS}") else # x86 uses traditional pflash drives with secure boot support # shellcheck disable=SC2054 args+=(-global driver=cfi.pflash01,property=secure,value=on -drive if=pflash,format="${EFI_CODE_FORMAT}",unit=0,file="${EFI_CODE}",readonly=on -drive if=pflash,format="${EFI_VARS_FORMAT}",unit=1,file="${EFI_VARS}") fi fi if [ -n "${iso}" ] && [ "${guest_os}" == "freedos" ]; then # FreeDOS reboots after partitioning the disk, and QEMU tries to boot from disk after first restart # This flag sets the boot order to cdrom,disk. It will persist until powering down the VM args+=(-boot order=dc) elif [ -n "${iso}" ] && [ "${guest_os}" == "kolibrios" ]; then # Since there is bug (probably) in KolibriOS: cdrom indexes 0 or 1 make system show an extra unexisting iso, so we use index=2 # shellcheck disable=SC2054 args+=(-drive media=cdrom,index=2,file="${iso}") iso="" elif [ -n "${iso}" ] && [ "${guest_os}" == "reactos" ]; then # https://reactos.org/wiki/QEMU # shellcheck disable=SC2054 args+=(-boot order=d -drive if=ide,index=2,media=cdrom,file="${iso}") iso="" elif [ -n "${iso}" ] && [ "${guest_os}" == "windows" ] && [ -e "${VMDIR}/unattended.iso" ]; then # Attach the unattended configuration to Windows guests when booting from ISO # shellcheck disable=SC2054 args+=(-drive media=cdrom,index=2,file="${VMDIR}/unattended.iso") fi if [ -n "${floppy}" ]; then # shellcheck disable=SC2054 args+=(-drive if=floppy,format=raw,file="${floppy}") fi # ARM64: create virtio-scsi controller if any CD-ROM ISOs are present # (virt machine has no IDE controller) if [ "${ARCH_VM}" == "aarch64" ] && { [ -n "${iso}" ] || [ -n "${fixed_iso}" ]; }; then # shellcheck disable=SC2054 args+=(-device virtio-scsi-pci,id=scsi0) fi if [ -n "${iso}" ]; then if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64: bootindex=1 ensures UEFI boots from CD-ROM first during installation # shellcheck disable=SC2054 args+=(-device scsi-cd,drive=cd0,bus=scsi0.0,bootindex=1 -drive id=cd0,if=none,format=raw,media=cdrom,readonly=on,file="${iso}") else # shellcheck disable=SC2054 args+=(-drive media=cdrom,index=0,file="${iso}") fi fi if [ -n "${fixed_iso}" ]; then if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64: attach second ISO to virtio-scsi controller # shellcheck disable=SC2054 args+=(-device scsi-cd,drive=cd1,bus=scsi0.0,bootindex=3 -drive id=cd1,if=none,format=raw,media=cdrom,readonly=on,file="${fixed_iso}") else # shellcheck disable=SC2054 args+=(-drive media=cdrom,index=1,file="${fixed_iso}") fi fi if [ "${guest_os}" == "macos" ]; then # shellcheck disable=SC2054 args+=(-device ahci,id=ahci -device ide-hd,bus=ahci.0,drive=BootLoader,bootindex=0 -drive id=BootLoader,if=none,format=qcow2,file="${MAC_BOOTLOADER}") if [ -n "${img}" ]; then # shellcheck disable=SC2054 args+=(-device ide-hd,bus=ahci.1,drive=RecoveryImage -drive id=RecoveryImage,if=none,format=raw,file="${img}") fi # shellcheck disable=SC2054,SC2206 args+=(-device ${MAC_DISK_DEV},drive=SystemDisk -drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}",${DRIVE_OPTIMISATIONS} ${STATUS_QUO}) elif [ "${guest_os}" == "kolibrios" ]; then # shellcheck disable=SC2054,SC2206 args+=(-device ahci,id=ahci -device ide-hd,bus=ahci.0,drive=SystemDisk -drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}",${DRIVE_OPTIMISATIONS} ${STATUS_QUO}) elif [ "${guest_os}" == "batocera" ] ; then # shellcheck disable=SC2054,SC2206 args+=(-device virtio-blk-pci,drive=BootDisk -drive id=BootDisk,if=none,format=raw,file="${img}" -device virtio-blk-pci,drive=SystemDisk -drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}",${DRIVE_OPTIMISATIONS} ${STATUS_QUO}) elif [ "${guest_os}" == "reactos" ]; then # https://reactos.org/wiki/QEMU # shellcheck disable=SC2054,SC2206 args+=(-drive if=ide,index=0,media=disk,file="${disk_img}") elif [ "${guest_os}" == "windows-server" ]; then # shellcheck disable=SC2054,SC2206 args+=(-device ide-hd,drive=SystemDisk -drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}",${DRIVE_OPTIMISATIONS} ${STATUS_QUO}) else if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64: bootindex=2 ensures disk boots after CD-ROM (bootindex=1) during installation # shellcheck disable=SC2054,SC2206 args+=(-device virtio-blk-pci,drive=SystemDisk,bootindex=2 -drive id=SystemDisk,if=none,format=${disk_format},file="${disk_img}",${DRIVE_OPTIMISATIONS} ${STATUS_QUO}) else # shellcheck disable=SC2054,SC2206 args+=(-device virtio-blk-pci,drive=SystemDisk -drive id=SystemDisk,if=none,format=${disk_format},file="${disk_img}",${DRIVE_OPTIMISATIONS} ${STATUS_QUO}) fi fi # https://wiki.qemu.org/Documentation/9psetup # https://askubuntu.com/questions/772784/9p-libvirt-qemu-share-modes if [ "${guest_os}" != "windows" ] || [ "${guest_os}" == "windows-server" ] && [ -n "${PUBLIC}" ]; then # shellcheck disable=SC2054 args+=(-fsdev local,id=fsdev0,path="${PUBLIC}",security_model=mapped-xattr -device virtio-9p-pci,fsdev=fsdev0,mount_tag="${PUBLIC_TAG}") fi if [ -n "${USB_PASSTHROUGH}" ]; then # shellcheck disable=SC2054,SC2206 args+=(-device ${USB_HOST_PASSTHROUGH_CONTROLLER},id=hostpass ${USB_PASSTHROUGH}) fi if [ "${tpm}" == "on" ] && [ -S "${VMDIR}/${VMNAME}.swtpm-sock" ]; then # shellcheck disable=SC2054 if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64 uses tpm-tis-device (system bus) instead of tpm-tis (ISA/LPC bus) args+=(-chardev socket,id=chrtpm,path="${VMDIR}/${VMNAME}.swtpm-sock" -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis-device,tpmdev=tpm0) else args+=(-chardev socket,id=chrtpm,path="${VMDIR}/${VMNAME}.swtpm-sock" -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0) fi fi if [ "${monitor}" == "none" ]; then args+=(-monitor none) echo " - Monitor: (off)" elif [ "${monitor}" == "telnet" ]; then # Find a free port to expose monitor-telnet to the guest TEMP_PORT="$(get_port "${monitor_telnet_port}" 9)" if [ -z "${TEMP_PORT}" ]; then echo " - Monitor: All Monitor-Telnet ports have been exhausted." else monitor_telnet_port="${TEMP_PORT}" # shellcheck disable=SC2054 args+=(-monitor telnet:"${monitor_telnet_host}:${monitor_telnet_port}",server,nowait) echo " - Monitor: On host: telnet ${monitor_telnet_host} ${monitor_telnet_port}" echo "monitor-telnet,${monitor_telnet_port},${monitor_telnet_host}" >> "${VMDIR}/${VMNAME}.ports" fi elif [ "${monitor}" == "socket" ]; then # shellcheck disable=SC2054,SC2206 args+=(-monitor unix:${SOCKET_MONITOR},server,nowait) if command -v socat &>/dev/null; then echo " - Monitor: On host: socat -,echo=0,icanon=0 unix-connect:${SOCKET_MONITOR}" elif command -v nc &>/dev/null; then echo " - Monitor: On host: nc -U \"${SOCKET_MONITOR}\"" fi else echo "ERROR! \"${monitor}\" is an unknown monitor option." exit 1 fi if [ "${serial}" == "none" ]; then args+=(-serial none) # No log output when serial is disabled - it's the default for macOS/Windows # and provides no useful information to the user elif [ "${serial}" == "telnet" ]; then # Find a free port to expose serial-telnet to the guest TEMP_PORT="$(get_port "${serial_telnet_port}" 9)" if [ -z "${TEMP_PORT}" ]; then echo " - Serial: All Serial Telnet ports have been exhausted." else serial_telnet_port="${TEMP_PORT}" # shellcheck disable=SC2054,SC2206 args+=(-serial telnet:${serial_telnet_host}:${serial_telnet_port},server,nowait) echo " - Serial: On host: telnet ${serial_telnet_host} ${serial_telnet_port}" echo "serial-telnet,${serial_telnet_port},${serial_telnet_host}" >> "${VMDIR}/${VMNAME}.ports" fi elif [ "${serial}" == "socket" ]; then # shellcheck disable=SC2054,SC2206 args+=(-serial unix:${SOCKET_SERIAL},server,nowait) if command -v socat &>/dev/null; then echo " - Serial: On host: socat -,echo=0,icanon=0 unix-connect:${SOCKET_SERIAL}" elif command -v nc &>/dev/null; then echo " - Serial: On host: nc -U \"${SOCKET_SERIAL}\"" fi else echo "ERROR! \"${serial}\" is an unknown serial option." exit 1 fi if [ -n "${extra_args}" ]; then # shellcheck disable=SC2206 args+=(${extra_args}) fi # The OSK parameter contains parenthesis, they need to be escaped in the shell # scripts. The vendor name, Quickemu Project, contains a space. It needs to be # double-quoted. SHELL_ARGS="${args[*]}" SHELL_ARGS="${SHELL_ARGS//\(/\\(}" SHELL_ARGS="${SHELL_ARGS//)/\\)}" SHELL_ARGS="${SHELL_ARGS//Quickemu Project/\"Quickemu Project\"}" if [ -z "${VM_PID}" ]; then echo "${QEMU}" "${SHELL_ARGS}" "2>/dev/null" >> "${VMDIR}/${VMNAME}.sh" sed -i -e 's/ -/ \\\n -/g' "${VMDIR}/${VMNAME}.sh" ${QEMU} "${args[@]}" &> "${VMDIR}/${VMNAME}.log" & VM_PID=$! sleep 0.25 if kill -0 "${VM_PID}" 2>/dev/null; then echo " - Process: Started ${VM} as ${VMNAME} (${VM_PID})" configure_cpu_pinning else echo " - Process: ERROR! Failed to start ${VM} as ${VMNAME}" rm -f "${VMDIR}/${VMNAME}.pid" rm -f "${VMDIR}/${VMNAME}.spice" rm -f "${VMDIR}/${VMNAME}.sock" echo && cat "${VMDIR}/${VMNAME}.log" exit 1 fi fi } function start_viewer { # Exit early if viewer is disabled or display is not SPICE if [ "${viewer}" == "none" ] || [ "${display}" != "spice" ]; then return fi # Build viewer arguments based on connection mode (Unix socket or TCP) local viewer_args=() local viewer_uri="" local errno=0 # Determine connection mode from .spice file content # - Unix socket mode: file contains a path (with /) # - TCP mode: file contains just a port number local spice_info="" if [ -r "${VMDIR}/${VMNAME}.spice" ]; then spice_info=$(cat "${VMDIR}/${VMNAME}.spice") fi if [[ "${spice_info}" == */* ]]; then # Unix socket mode (path contains /) local SPICE_SOCKET="${spice_info}" if [ "${viewer}" == "spicy" ]; then viewer_args+=("--uri=spice+unix://${SPICE_SOCKET}") else viewer_uri="spice+unix://${SPICE_SOCKET}" fi elif [ -n "${spice_info}" ]; then # TCP mode (port number) if [ "${viewer}" == "spicy" ]; then viewer_args+=("--port" "${spice_info}") else viewer_uri="spice://localhost:${spice_info}" fi else # Fallback: no .spice file, use ACCESS variable to determine mode if [ -z "${ACCESS}" ] || [ "${ACCESS}" == "local" ]; then # Unix socket mode local SPICE_SOCKET="${VMDIR}/${VMNAME}.sock" if [ "${viewer}" == "spicy" ]; then viewer_args+=("--uri=spice+unix://${SPICE_SOCKET}") else viewer_uri="spice+unix://${SPICE_SOCKET}" fi else # TCP mode for remote access if [ "${viewer}" == "spicy" ]; then viewer_args+=("--port" "${spice_port}") else viewer_uri="spice://localhost:${spice_port}" fi fi fi # Add common arguments viewer_args+=("--title" "${VMNAME}") # Add shared directory if configured (not for macOS guests) if [ "${guest_os}" != "macos" ] && [ -n "${PUBLIC}" ]; then viewer_args+=("--spice-shared-dir" "${PUBLIC}") fi # Add fullscreen if requested if [ -n "${FULLSCREEN}" ]; then viewer_args+=("${FULLSCREEN}") fi # Add URI for remote-viewer (spicy uses --uri= in the args already) if [ "${viewer}" == "remote-viewer" ] && [ -n "${viewer_uri}" ]; then viewer_args+=("${viewer_uri}") fi # Launch the viewer echo " - Viewer: ${viewer} ${viewer_args[*]} >/dev/null 2>&1 &" "${viewer}" "${viewer_args[@]}" >/dev/null 2>&1 & errno=$? if [ ${errno} -ne 0 ]; then echo "WARNING! Could not start viewer (${viewer}) Err: ${errno}" fi } function shortcut_create { local dirname="${HOME}/.local/share/applications" local filename="${HOME}/.local/share/applications/${VMNAME}.desktop" echo "Creating ${VMNAME} desktop shortcut file" if [ ! -d "${dirname}" ]; then mkdir -p "${dirname}" fi cat << EOF > "${filename}" [Desktop Entry] Version=1.0 Type=Application Terminal=false Exec=$(basename "${0}") --vm ${VM} ${SHORTCUT_OPTIONS} Path=${VMPATH} Name=${VMNAME} Icon=qemu EOF echo " - ${filename} created." } function usage() { echo " _ _" echo " __ _ _ _(_) ___| | _____ _ __ ___ _ _" echo " / _' | | | | |/ __| |/ / _ \ '_ ' _ \| | | |" echo "| (_| | |_| | | (__| < __/ | | | | | |_| |" echo " \__, |\__,_|_|\___|_|\_\___|_| |_| |_|\__,_|" echo " |_| v${VERSION}, using qemu ${QEMU_VER_LONG}" echo "--------------------------------------------------------------------------------" echo " Project - https://github.com/quickemu-project/quickemu" echo " Discord - https://wimpysworld.io/discord" echo "--------------------------------------------------------------------------------" echo echo "Usage" echo " ${LAUNCHER} --vm ubuntu.conf " echo echo "Arguments" echo " --access : Enable remote spice access support. 'local' (default), 'remote', 'clientipaddress'" echo " --braille : Enable braille support. Requires SDL." echo " --cpu-pinning : Choose which host cores correspond to which guest cores." echo " --delete-disk : Delete the disk image and EFI variables" echo " --delete-vm : Delete the entire VM and its configuration" echo " --display : Select display backend. 'gtk' (default), 'sdl', 'cocoa', 'none', 'spice' or 'spice-app'" echo " --fullscreen : Starts VM in full screen mode (Ctl+Alt+f to exit)" echo " --ignore-msrs-always : Configure KVM to always ignore unhandled machine-specific registers" echo " --ignore-tsc-warning : Skip TSC stability warning for macOS VMs on AMD" echo " --kill : Kill the VM process if it is running" echo " --offline : Override all network settings and start the VM offline" echo " --shortcut : Create a desktop shortcut" echo " --snapshot apply : Apply/restore a snapshot." echo " --snapshot create : Create a snapshot." echo " --snapshot delete : Delete a snapshot." echo " --snapshot info : Show disk/snapshot info." echo " --status-quo : Do not commit any changes to disk/snapshot." echo " --viewer : Choose an alternative viewer. @Options: 'spicy' (default), 'remote-viewer', 'none'" echo " --width : Set VM screen width; requires '--height'" echo " --height : Set VM screen height; requires '--width'" echo " --ssh-port : Set SSH port manually" echo " --spice-port : Set SPICE port manually" echo " --public-dir : Expose share directory. @Options: '' (default: xdg-user-dir PUBLICSHARE), '', 'none'" echo " --monitor : Set monitor connection type. @Options: 'socket' (default), 'telnet', 'none'" echo " --monitor-telnet-host : Set telnet host for monitor. (default: 'localhost')" echo " --monitor-telnet-port : Set telnet port for monitor. (default: '4440')" echo " --monitor-cmd : Send command to monitor if available. (Example: system_powerdown)" echo " --serial : Set serial connection type. @Options: 'socket' (default), 'telnet', 'none'" echo " --serial-telnet-host : Set telnet host for serial. (default: 'localhost')" echo " --serial-telnet-port : Set telnet port for serial. (default: '6660')" echo " --keyboard : Set keyboard. @Options: 'usb' (default), 'ps2', 'virtio'" echo " --keyboard_layout : Set keyboard layout: 'en-us' (default)" echo " --mouse : Set mouse. @Options: 'tablet' (default), 'ps2', 'usb', 'virtio'" echo " --usb-controller : Set usb-controller. @Options: 'ehci' (default), 'xhci', 'none'" echo " --sound-card : Set sound card. @Options: 'intel-hda' (default), 'ac97', 'es1370', 'sb16', 'usb-audio', 'virtio-sound-pci', 'none'" echo " --sound-duplex : Set sound card duplex. @Options: 'hda-micro' (default: speaker/mic), 'hda-duplex' (line-in/line-out), 'hda-output' (output-only)" echo " --extra_args : Pass additional arguments to qemu" echo " --version : Print version" } function display_param_check() { # Braille support requires SDL. Override $display if braille was requested. if [ -n "${BRAILLE}" ]; then display="sdl" # XHCI supports USB 1.1/2.0/3.0; required for full-speed braille devices usb_controller="xhci" fi # Fallback to SDL if GTK display is not available if [ "${display}" == "gtk" ]; then if ! "${QEMU}" -display help 2>&1 | grep -q "^gtk$"; then echo " - NOTE: GTK display not available, falling back to SDL" display="sdl" fi fi if [ "${OS_KERNEL}" == "Darwin" ]; then if [ "${display}" != "cocoa" ] && [ "${display}" != "none" ]; then echo "ERROR! Requested output '${display}' but only 'cocoa' and 'none' are avalible on macOS." exit 1 fi else if [ "${display}" != "gtk" ] && [ "${display}" != "none" ] && [ "${display}" != "sdl" ] && [ "${display}" != "spice" ] && [ "${display}" != "spice-app" ]; then echo "ERROR! Requested output '${display}' is not recognised." exit 1 fi fi # Set the default 3D acceleration. if [ -z "${gl}" ]; then if command -v glxinfo &>/dev/null; then GLSL_VER=$(glxinfo | grep "OpenGL ES GLSL" | awk '{print $NF}') case ${GLSL_VER} in 1*|2*) gl="off";; *) gl="on";; esac else gl="on" fi fi # Enable grab-on-hover for SDL: https://github.com/quickemu-project/quickemu/issues/541 case "${display}" in sdl) export SDL_MOUSE_FOCUS_CLICKTHROUGH=1;; esac } function ports_param_check() { if [ -n "${ssh_port}" ] && ! is_numeric "${ssh_port}"; then echo "ERROR: ssh_port must be a number!" exit 1 fi if [ -n "${spice_port}" ] && ! is_numeric "${spice_port}"; then echo "ERROR: spice_port must be a number!" exit 1 fi if [ -n "${monitor_telnet_port}" ] && ! is_numeric "${monitor_telnet_port}"; then echo "ERROR: telnet port must be a number!" exit 1 fi if [ -n "${serial_telnet_port}" ] && ! is_numeric "${serial_telnet_port}"; then echo "ERROR: serial port must be a number!" exit 1 fi } function sound_card_param_check() { if [ "${sound_card}" != "ac97" ] && [ "${sound_card}" != "es1370" ] && [ "${sound_card}" != "ich9-intel-hda" ] && [ "${sound_card}" != "intel-hda" ] && [ "${sound_card}" != "sb16" ] && [ "${sound_card}" != "usb-audio" ] && [ "${sound_card}" != "virtio-sound-pci" ] && [ "${sound_card}" != "none" ]; then echo "ERROR! Requested sound card '${sound_card}' is not recognised." exit 1 fi # USB audio requires xhci controller if [ "${sound_card}" == "usb-audio" ]; then usb_controller="xhci"; fi #name "hda-duplex", bus HDA, desc "HDA Audio Codec, duplex (line-out, line-in)" #name "hda-micro", bus HDA, desc "HDA Audio Codec, duplex (speaker, microphone)" #name "hda-output", bus HDA, desc "HDA Audio Codec, output-only (line-out)" if [ "${sound_duplex}" != "hda-duplex" ] && [ "${sound_duplex}" != "hda-micro" ] && [ "${sound_duplex}" != "hda-output" ]; then echo "ERROR! Requested sound duplex '${sound_duplex}' is not recognised." exit 1 fi } function tpm_param_check() { if [ "${tpm}" == "on" ]; then SWTPM=$(command -v swtpm) if [ ! -e "${SWTPM}" ]; then echo "ERROR! TPM is enabled, but swtpm was not found." exit 1 fi fi } function viewer_param_check() { if [ "${OS_KERNEL}" == "Darwin" ]; then return fi if [ "${viewer}" != "none" ] && [ "${viewer}" != "spicy" ] && [ "${viewer}" != "remote-viewer" ]; then echo "ERROR! Requested viewer '${viewer}' is not recognised." exit 1 fi if [ "${viewer}" == "spicy" ] && ! command -v spicy &>/dev/null; then echo "ERROR! Requested 'spicy' as viewer, but 'spicy' is not installed." exit 1 elif [ "${viewer}" == "remote-viewer" ] && ! command -v remote-viewer &>/dev/null; then echo "ERROR! Requested 'remote-viewer' as viewer, but 'remote-viewer' is not installed." exit 1 fi } function fileshare_param_check() { if [ "${PUBLIC}" == "none" ]; then PUBLIC="" else # PUBLICSHARE is the only directory exposed to guest VMs for file # sharing via 9P, spice-webdavd and Samba. This path is not configurable. if [ -z "${PUBLIC}" ]; then if command -v xdg-user-dir &>/dev/null; then PUBLIC=$(xdg-user-dir PUBLICSHARE) elif [ -d "${HOME}/Public" ]; then PUBLIC="${HOME}/Public" fi fi if [ ! -d "${PUBLIC}" ]; then echo " - WARNING! Public directory: '${PUBLIC}' doesn't exist!" PUBLIC="" else PUBLIC_TAG="Public-${USER,,}" PUBLIC_PERMS=$(${STAT} -c "%A" "${PUBLIC}") fi fi } function parse_ports_from_file { local FILE="${VMDIR}/${VMNAME}.ports" local host_name="" local port_name="" local port_number="" # Loop over each line in the file while IFS= read -r CONF || [ -n "${CONF}" ]; do # parse ports port_name=$(echo "${CONF}" | cut -d',' -f 1) port_number=$(echo "${CONF}" | cut -d',' -f 2) host_name=$(echo "${CONF}" | awk 'FS="," {print $3,"."}') if [ "${port_name}" == "ssh" ]; then ssh_port="${port_number}" elif [ "${port_name}" == "spice" ]; then spice_port="${port_number}" elif [ "${port_name}" == "monitor-telnet" ]; then monitor_telnet_port="${port_number}" monitor_telnet_host="${host_name}" elif [ "${port_name}" == "serial-telnet" ]; then serial_telnet_port="${port_number}" serial_telnet_host="${host_name}" fi done < "${FILE}" } function is_numeric { [[ "$1" =~ ^[0-9]+$ ]] } function monitor_send_cmd { local MSG="${1}" if [ -z "${MSG}" ]; then echo "WARNING! Send to QEMU-Monitor: Message empty!" return 1 fi case "${monitor}" in socket) echo -e " - Sending: via socket ${MSG}" echo -e "${MSG}" | socat -,shut-down unix-connect:"${SOCKET_MONITOR}" > /dev/null 2>&1;; telnet) echo -e " - Sending: via telnet ${MSG}" echo -e "${MSG}" | socat - tcp:"${monitor_telnet_host}":"${monitor_telnet_port}" > /dev/null 2>&1;; *) echo "WARNING! No qemu-monitor channel available - Couldn't send message to monitor!" return 1;; esac return 0 } ### MAIN # Lowercase variables are used in the VM config file only boot="efi" cpu_cores="" disk_format="${disk_format:-qcow2}" disk_img="${disk_img:-}" disk_size="${disk_size:-16G}" display="${display:-gtk}" extra_args="${extra_args:-}" fixed_iso="" floppy="" guest_os="linux" img="" iso="" macaddr="" macos_release="" network="" port_forwards=() preallocation="off" ram="" secureboot="off" tpm="off" usb_devices=() viewer="${viewer:-spicy}" width="${width:-}" height="${height:-}" ssh_port="${ssh_port:-}" spice_port="${spice_port:-}" monitor="${monitor:-socket}" monitor_telnet_port="${monitor_telnet_port:-4440}" monitor_telnet_host="${monitor_telnet_host:-localhost}" # Serial default is set later based on guest_os (after config is sourced) serial="${serial:-}" serial_telnet_port="${serial_telnet_port:-6660}" serial_telnet_host="${serial_telnet_host:-localhost}" # options: ehci (USB2.0), xhci (USB3.0) usb_controller="${usb_controller:-ehci}" keyboard="${keyboard:-usb}" keyboard_layout="${keyboard_layout:-en-us}" mouse="${mouse:-tablet}" sound_card="${sound_card:-intel-hda}" sound_duplex="${sound_duplex:-hda-micro}" ACCESS="" ACTIONS=() BRAILLE="" IGNORE_TSC_WARNING="" CPU_PINNING="" FULLSCREEN="" MONITOR_CMD="" PUBLIC="" PUBLIC_PERMS="" PUBLIC_TAG="" SHORTCUT_OPTIONS="" SNAPSHOT_ACTION="" SNAPSHOT_TAG="" SOCKET_MONITOR="" SOCKET_SERIAL="" STATUS_QUO="" USB_PASSTHROUGH="" VM="" VMDIR="" VMNAME="" VMPATH="" # CPU flag tracking map for deduplication and conflict detection declare -A CPU_FLAG_MAP # shellcheck disable=SC2155 readonly LAUNCHER=$(basename "${0}") readonly DISK_MIN_SIZE=$((197632 * 8)) readonly VERSION="4.9.9" # Default architecture is x86_64, can be overridden by config file (arch="aarch64") arch="${arch:-x86_64}" ARCH_VM="${arch}" ARCH_HOST=$(uname -m) QEMU=$(command -v "qemu-system-${ARCH_VM}") QEMU_IMG=$(command -v qemu-img) if [ ! -x "${QEMU}" ] || [ ! -x "${QEMU_IMG}" ]; then echo "ERROR! QEMU not found. Please make sure 'qemu-system-${ARCH_VM}' and 'qemu-img' are installed." exit 1 fi # Check for gnu tools on macOS STAT="stat" if command -v gstat &>/dev/null; then STAT="gstat" fi OS_KERNEL=$(uname -s) if [ "${OS_KERNEL}" == "Darwin" ]; then display="cocoa" fi QEMU_VER_LONG=$(${QEMU_IMG} --version | head -n 1 | awk '{print $3}') # strip patch version and remove dots. 6.0.0 => 60 / 10.0.0 => 100 QEMU_VER_SHORT="${QEMU_VER_LONG%.*}" QEMU_VER_SHORT="${QEMU_VER_SHORT/./}" if [ "${QEMU_VER_SHORT}" -lt 61 ]; then echo "ERROR! QEMU 6.1.0 or newer is required, detected ${QEMU_VER_LONG}." exit 1 fi # Take command line arguments if [ $# -lt 1 ]; then usage exit 1 else while [ $# -gt 0 ]; do case "${1}" in -access|--access) SHORTCUT_OPTIONS+="--access ${2} " ACCESS="${2}" shift 2;; -braille|--braille) SHORTCUT_OPTIONS+="--braille " BRAILLE="on" shift;; -cpu-pinning|--cpu-pinning) SHORTCUT_OPTIONS+="--cpu-pinning ${2} " CPU_PINNING=${2} shift 2;; -delete|--delete|-delete-disk|--delete-disk) ACTIONS+=(delete_disk) shift;; -delete-vm|--delete-vm) ACTIONS+=(delete_vm) shift;; -display|--display) SHORTCUT_OPTIONS+="--display ${2} " display="${2}" display_param_check shift 2;; -fullscreen|--fullscreen|-full-screen|--full-screen) SHORTCUT_OPTIONS+="--fullscreen " FULLSCREEN="--full-screen" shift;; -ignore-msrs-always|--ignore-msrs-always) ignore_msrs_always exit;; -ignore-tsc-warning|--ignore-tsc-warning) IGNORE_TSC_WARNING="1" shift;; -kill|--kill) ACTIONS+=(kill_vm) shift;; -offline|--offline) SHORTCUT_OPTIONS+="--offline " network="none" shift;; -snapshot|--snapshot) if [ -z "${2}" ]; then echo "ERROR! '--snapshot' needs an action to perform." exit 1 fi SNAPSHOT_ACTION="${2}" if [ -z "${3}" ] && [ "${SNAPSHOT_ACTION}" != "info" ]; then echo "ERROR! '--snapshot ${SNAPSHOT_ACTION}' needs a tag." exit 1 fi SNAPSHOT_TAG="${3}" if [ "${SNAPSHOT_ACTION}" == "info" ]; then shift 2 else shift 3 fi;; -status-quo|--status-quo) STATUS_QUO="-snapshot" shift;; -shortcut|--shortcut) ACTIONS+=(shortcut_create) shift;; -vm|--vm) VM="${2}" shift 2;; -viewer|--viewer) SHORTCUT_OPTIONS+="--viewer ${2} " viewer="${2}" shift 2;; -width|--width) SHORTCUT_OPTIONS+="--width ${2} " width="${2}" shift 2;; -height|--height) SHORTCUT_OPTIONS+="--height ${2} " height="${2}" shift 2;; -ssh-port|--ssh-port) SHORTCUT_OPTIONS+="--ssh-port ${2} " ssh_port="${2}" shift 2;; -spice-port|--spice-port) SHORTCUT_OPTIONS+="--spice-port ${2} " spice_port="${2}" shift 2;; -public-dir|--public-dir) SHORTCUT_OPTIONS+="--public-dir ${2} " PUBLIC="${2}" shift 2;; -monitor|--monitor) SHORTCUT_OPTIONS+="--monitor ${2} " monitor="${2}" shift 2;; -monitor-cmd|--monitor-cmd) SHORTCUT_OPTIONS+="--monitor-cmd ${2} " MONITOR_CMD="${2}" shift 2;; -monitor-telnet-host|--monitor-telnet-host) SHORTCUT_OPTIONS+="--monitor-telnet-host ${2} " monitor_telnet_host="${2}" shift 2;; -monitor-telnet-port|--monitor-telnet-port) SHORTCUT_OPTIONS+="--monitor-telnet-port ${2} " monitor_telnet_port="${2}" shift 2;; -serial|--serial) SHORTCUT_OPTIONS+="--serial ${2} " serial="${2}" shift 2;; -serial-telnet-host|--serial-telnet-host) SHORTCUT_OPTIONS+="--serial-telnet-host ${2} " serial_telnet_host="${2}" shift 2;; -serial-telnet-port|--serial-telnet-port) SHORTCUT_OPTIONS+="--serial-telnet-port ${2} " serial_telnet_port="${2}" shift 2;; -keyboard|--keyboard) SHORTCUT_OPTIONS+="--keyboard ${2} " keyboard="${2}" shift 2;; -keyboard_layout|--keyboard_layout) SHORTCUT_OPTIONS+="--keyboard_layout ${2} " keyboard_layout="${2}" shift 2;; -mouse|--mouse) SHORTCUT_OPTIONS+="--mouse ${2} " mouse="${2}" shift 2;; -usb-controller|--usb-controller) SHORTCUT_OPTIONS+="--usb-controller ${2} " usb_controller="${2}" shift 2;; -extra_args|--extra_args) SHORTCUT_OPTIONS+="--extra_args ${2} " extra_args+="${2}" shift 2;; -sound-card|--sound-card) SHORTCUT_OPTIONS+="--sound-card ${2} " sound_card="${2}" shift 2;; -sound-duplex|--sound-duplex) SHORTCUT_OPTIONS+="--sound-duplex ${2} " sound_duplex="${2}" shift 2;; -version|--version) echo "${VERSION}" exit;; -h|--h|-help|--help) usage exit 0;; *) echo "ERROR! \"${1}\" is not a supported parameter." usage exit 1;; esac done fi if [ -n "${VM}" ] && [ -e "${VM}" ]; then # shellcheck source=/dev/null source "${VM}" PUBLIC="${public_dir:-${PUBLIC}}" # Re-detect architecture and QEMU binary after sourcing config # Config file can set arch="aarch64" to override the default ARCH_VM="${arch:-x86_64}" QEMU=$(command -v "qemu-system-${ARCH_VM}") if [ ! -x "${QEMU}" ]; then echo "ERROR! qemu-system-${ARCH_VM} not found." echo " Please install QEMU for ${ARCH_VM} architecture." exit 1 fi VMDIR=$(dirname "${disk_img}") # directory the VM disk and state files are stored VMNAME=$(basename "${VM}" .conf) # name of the VM VMPATH=$(realpath "$(dirname "${VM}")") # path to the top-level VM directory SOCKET_MONITOR="${VMDIR}/${VMNAME}-monitor.socket" SOCKET_SERIAL="${VMDIR}/${VMNAME}-serial.socket" # Set serial default based on guest_os if not explicitly configured. # macOS and Windows guests don't output anything useful to serial by default, # so disable it to reduce clutter. Users can still override with --serial. if [ -z "${serial}" ]; then case "${guest_os}" in macos|windows|windows-server) serial="none" ;; *) serial="socket" ;; esac fi # if disk_img is not configured, do the right thing. if [ -z "${disk_img}" ]; then disk_img="${VMDIR}/disk.${disk_format}" fi # Fixes running VMs when PWD is not relative to the VM directory # https://github.com/quickemu-project/quickemu/pull/875 if [ ! -f "${disk_img}" ]; then pushd "${VMPATH}" >/dev/null || exit fi # Check if VM is already running VM_PID="" if [ -r "${VMDIR}/${VMNAME}.pid" ]; then VM_PID=$(head -n 1 "${VMDIR}/${VMNAME}.pid") if ! kill -0 "${VM_PID}" > /dev/null 2>&1; then #VM is not running, cleaning up. VM_PID="" rm -f "${VMDIR}/${VMNAME}.pid" rm -f "${VMDIR}/${VMNAME}.spice" rm -f "${VMDIR}/${VMNAME}.sock" fi fi # Iterate over any actions and exit. if [ ${#ACTIONS[@]} -ge 1 ]; then for ACTION in "${ACTIONS[@]}"; do ${ACTION} done exit fi if [ -n "${SNAPSHOT_ACTION}" ]; then case ${SNAPSHOT_ACTION} in apply) snapshot_apply "${SNAPSHOT_TAG}" snapshot_info exit;; create) snapshot_create "${SNAPSHOT_TAG}" snapshot_info exit;; delete) snapshot_delete "${SNAPSHOT_TAG}" snapshot_info exit;; info) echo "Snapshot information ${disk_img}" snapshot_info exit;; *) echo "ERROR! \"${SNAPSHOT_ACTION}\" is not a supported snapshot action." usage exit 1;; esac fi else echo "ERROR! Virtual machine configuration not found." usage exit 1 fi display_param_check ports_param_check sound_card_param_check tpm_param_check viewer_param_check fileshare_param_check if [ -z "${VM_PID}" ]; then vm_boot start_viewer # If the VM being started is an uninstalled Windows VM then auto-skip the press-any key prompt. if [ -n "${iso}" ] && [[ "${guest_os}" == "windows"* ]]; then # shellcheck disable=SC2034 for LOOP in {1..5}; do sleep 1 monitor_send_cmd "sendkey ret" done fi else echo "${VMNAME}" echo " - Process: Already running ${VM} as ${VMNAME} (${VM_PID})" parse_ports_from_file # Auto-detect SPICE if .spice file exists and display is a default GUI type if [ -r "${VMDIR}/${VMNAME}.spice" ]; then if [ "${display}" == "sdl" ] || [ "${display}" == "cocoa" ] || [ "${display}" == "gtk" ]; then display="spice" spice_port=$(cat "${VMDIR}/${VMNAME}.spice") fi fi start_viewer fi if [ -n "${MONITOR_CMD}" ]; then monitor_send_cmd "${MONITOR_CMD}" fi # vim:tabstop=4:shiftwidth=4:expandtab