#!/usr/bin/env /bin/bash declare -r TOP_BASEDIR="`pwd`" # # ISO images # declare -r ISO_BASEDIR="/mnt/machines/iso" # Windows installation ISO #declare -r INSTALL_ISO="${ISO_BASEDIR}/Win11_25H2_English_x64_v2.iso" #declare -r INSTALL_ISO="${ISO_BASEDIR}/nano11-25H2-English-Pro-2026-03-20.iso" declare -r INSTALL_ISO="${TOP_BASEDIR}/output/install-builder-amd64-20260601.iso" # Unattended installation add-on ISO #declare -r UNATTEND_ISO="${TOP_BASEDIR}/output/unattend.iso" declare -r VIRTIO_ISO="${ISO_BASEDIR}/virtio-win-0.1.285.iso" # # Disk images # declare -r DISK_BASEDIR="/mnt/machines/qemu-machines" #declare -r DISK_IMG="${DISK_BASEDIR}/win11-builder-amd64.qcow2" declare -r DISK_SIZE="32G" # # x86-64-v3: Haswell w/o intel-specific instructions (TSX, IBRS) # # x86-64-v1: +cmov,+cx8,+fpu,+fxsr,+mmx,+osfxsr,+sce,+sse,+sse2 (aka "kvm64") # x86-64-v2: +cx16,+lahf-lm,+popcnt,+sse3,+sse4.1,+sse4.2,+ssse3 # x86-64-v3: +avx,+avx2,+bmi1,+bmi2,+f16c,+fma,+movbe,+xsave,+aes # x86-64-v4: +avx512f,+avx512bw,+avx512cd,+avx512dq,+avx512vl # declare -r CPU_X64_V2="kvm64,+cx16,+lahf-lm,+popcnt,+sse3,+sse4.1,+sse4.2,+ssse3" declare -r CPU_X64_V3="${CPU_X64_V2},+avx,+avx2,+bmi1,+bmi2,+f16c,+fma,+movbe,+xsave,+aes" declare -r CPU_X64_V4="${CPU_X64_V3},+avx512f,+avx512bw,+avx512cd,+avx512dq,+avx512vl" # AMD64: Minimum requirement is X86_64-V3 declare -r CPU_OPTIONS_AMD64="-cpu host -smp cpus=4,cores=4,sockets=1" #declare -r CPU_OPTIONS_AMD64="-cpu ${CPU_X64_V3} -smp cpus=4,cores=4,sockets=1" # ARM64: Minimum requirement for Windows 11 is Cortext A72? declare -r CPU_OPTIONS_ARM64="-cpu host -smp 4" #declare -r CPU_OPTIONS_ARM64="-cpu max,pauth-impdef=on -smp cpus=4,cores=4,sockets=1" #declare -r CPU_OPTIONS_ARM64="-cpu neoverse-n1 -smp cpus=4,cores=4,sockets=1" #declare -r CPU_OPTIONS_ARM64="-cpu cortext-a76 -smp cpus=4,cores=4,sockets=1" # # Memory config # declare -r MEM_SIZE="8G" declare -r MEM_OPTIONS="-m ${MEM_SIZE}" # # # declare -r ARM64_EFI_CODE_IMG="/usr/share/edk2/aarch64/QEMU_EFI.fd" declare -r ARM64_EFI_VARS_IMG="" # Optional: required EFI ROM size declare -r ARM64_EFI_CODE_SIZE="64M" ################################################################# # No user-serviceable parts below this line ################################################################# # Get builder command declare -r COMMAND="${1:-build}" shift function detect_iso_arch { local iso_file="${1}" local fallback_arch="${2}" case "${iso_file@L}" in *-amd64-*|*-x64-*) echo "amd64" ;; *-arm64-*|*-aarch64-*) echo "arm64" ;; *) # Unknown use host arch as fallback echo "NOTICE: Cannot detect architecture from ISO name, using host" >&2 echo "${fallback_arch}" ;; esac } function detect_host_arch { case "$(uname -m | tr '[:upper:]' '[:lower:]')" in "x86_64") echo "amd64" ;; "aarch64") echo "arm64" ;; *) return 1 ;; esac } # Detect host and guest architecture declare -r HOST_ARCH="$(detect_host_arch)" [[ -n "${HOST_ARCH}" ]] || { echo "Failed to detect host arch" >&2; exit 1; } declare -r GUEST_ARCH="$(detect_iso_arch "${INSTALL_ISO}" "${HOST_ARCH}")" [[ -n "${GUEST_ARCH}" ]] || { echo "Failed to detect guest arch" >&2; exit 1; } # Setup locations declare -r BASEDIR="${TOP_BASEDIR}/win11-builder-${GUEST_ARCH}" declare -r CACHEDIR="${TOP_BASEDIR}/cache" mkdir -p "${BASEDIR}" || exit $? mkdir -p "${CACHEDIR}" || exit $? declare -r DISK_IMG="${DISK_BASEDIR:-${BASEDIR}}/win11-builder-${GUEST_ARCH}.qcow2" [[ -n "${DISK_IMG}" ]] || { echo "Mandatory disk image configuration missing" >&2 exit 1 } # # Common options used by all configurations # QEMU_DYN_OPTIONS=( # Use host's /dev/urandom "-object rng-random,filename=/dev/urandom,id=rng0" "-device virtio-rng-pci,rng=rng0" # virtio-fs "-chardev socket,id=char0,path=${BASEDIR}/virtiofsd.sock" "-device vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=sharedfs" "-object memory-backend-memfd,id=mem,size=${MEM_SIZE},share=on" "-numa node,memdev=mem" ) # # virtiofs # mkdir -p "${BASEDIR}/shared" || exit $? echo "This directory is shared between host and guest" \ > "${BASEDIR}/shared/README.md" || exit $? # # Randomized user credentials? # #declare -r USER_NAME="User" || exit $? #declare -r USER_PASSWORD="$(pwgen -cns 30 1)" || exit $? #QEMU_DYN_OPTIONS+=( # "-smbios type=11,value=AutoLogon\\${USER_NAME}\\${USER_PASSWORD}" #) function start_viofsd { echo "Starting virtiofsd..." >&2 /usr/libexec/virtiofsd --sandbox=none --tag sharedfs \ --shared-dir "${CACHEDIR}" \ --socket-path "${BASEDIR}/virtiofsd.sock" \ --socket-group `id -gn` \ --translate-uid squash-guest:0:`id -u`:1000000 \ --translate-gid squash-guest:0:`id -g`:1000000 & echo $! > "${BASEDIR}/virtiofsd.pid" || exit $? } function stop_viofsd { local VIOFSD_PID="$(cat "${BASEDIR}/virtiofsd.pid")" || return $? [[ -n "${VIOFSD_PID}" ]] || return 1 pgrep -p "${VIOFSD_PID}" || return 1 echo "Stopping virtiofsd..." >&2 kill -TERM "${VIOFSD_PID}" } trap "stop_viofsd" EXIT start_viofsd # # Qemu # function qemu_image_type_from_fileext { local img_file="${1@L}" case "${img_file##*.}" in "qcow2") echo "qcow2" ;; *) echo "raw" ;; esac } function qemu_image_size { local img_file="${1}" local img_type="$(qemu_image_type_from_fileext "${1}")" case "${img_type}" in "qcow2") echo "$(qemu-img info --output json "${img_file}" | jq '.["virtual-size"]')" || return $? ;; "raw") echo "$(stat -c '%s' "${img_file}")" || return $? ;; *) echo "Unknown image type '${img_type}', cannot detect size" >&2 return 1 ;; esac } function fileext_from_qemu_image_type { local img_type="${1@L}" case "${img_type}" in *) echo "${img_type}" ;; esac } function expand_size { local value="${1}" [[ "${value}" =~ ^[0-9]+$ ]] && { echo "${value}" return 0 } local unit="${value:$(( ${#value} - 1 ))}" case "${unit@U}" in "B") echo "$(( ${value%${unit}} * 1 ))" ;; "K") echo "$(( ${value%${unit}} * 2**10 ))" ;; "M") echo "$(( ${value%${unit}} * 2**20 ))" ;; "G") echo "$(( ${value%${unit}} * 2**30 ))" ;; "T") echo "$(( ${value%${unit}} * 2**40 ))" ;; *) echo "Invalid unit suffix in value: '${value}'" >&2 return 1 ;; esac } function do_resize_image { local img_file="${1}" local img_type="${2}" local img_cur_size="${3}" local img_req_size="${4}" [[ "${img_cur_size}" -eq "${img_req_size}" ]] && return 0 [[ "${img_cur_size}" -gt "${img_req_size}" ]] && { echo "Shrinking ${img_type} images is not supported" >&2 return 1 } case "${img_type}" in "raw") echo "Resizing raw image '$(basename "${img_file}")'" >&2 truncate --size="${img_req_size}" "${img_file}" || return $? ;; "qcow2") echo "Resizing qcow2 image '$(basename "${img_file}")'" >&2 qemu-img resize -f qcow2 "${img_file}" "${img_req_size}" || return $? ;; *) echo "Resizing ${img_type} images is currently not supported" >&2 return 1 ;; esac } function resize_image { local img_file="${1}" local img_type="$(qemu_image_type_from_fileext "${img_file}")" local img_cur_size="$(qemu_image_size "${1}")" local img_req_size="$(expand_size "${2:-${img_cur_size}}")" do_resize_image "${img_file}" "${img_type}" "${img_cur_size}" "${img_req_size}" } # Drivers for the WinPE early installation environment declare -r VIRTIO_WINPE_DRIVERS=( "NetKVM" "vioscsi" "viostor" "viogpudo" ) function has { local val="${1}"; shift for x in $@; do [[ "${x}" == "${val}" ]] && return 0 done return 1 } # # # function qemu_fill_unattend_fs { local destdir="${1}" [[ -d "${destdir}" ]] || { echo "ERROR: Virtual VFAT target directory '${destdir}' does not exist" >&2 return 1 } local unattend_xml="${2}" [[ -f "${unattend_xml}" ]] || { echo "ERROR: Unattended XML installation file '${unattend_xml}' does not exist" >&2 return 1 } # Copy autounattend.xml sed -e "s|processorArchitecture=\"[^\"]\+\"|processorArchitecture=\"${GUEST_ARCH}\"|g" \ < "${unattend_xml}" > "${destdir}/autounattend.xml" || return $? # local virtio_arch="${GUEST_ARCH@L}" case "${GUEST_ARCH@L}" in "arm64") virtio_arch="${GUEST_ARCH@U}" ;; "amd64") virtio_arch="${GUEST_ARCH@L}" ;; *) echo "ERROR: " >&2 return 1 ;; esac # Copy virtio drivers local virtio_iso="${3}" [[ -f "${virtio_iso}" ]] || { echo "ERROR: VirtIO ISO image '${virtio_iso}' does not exist" >&2 return 1 } local virtio_drivers_list="$(7z l -slt "${virtio_iso}" | sed -ne "/^Path = [^/]\+\/w11\/${virtio_arch}\$/{ s|^Path = \([^/]\+\)/.*|\1|; p }")" [[ -n "${virtio_drivers_list}" ]] || { echo "ERROR: No drivers found in VirtIO ISO image '${virtio_iso}'" >&2 return 1 } echo "Copying virtio drivers '${virtio_drivers_list//$'\n'/,}' into unattend VFS..." >&2 mkdir -p "${destdir}/drivers" "${destdir}/\$WinPEDriver\$" || return $? for x in ${virtio_drivers_list}; do mkdir -p "${destdir}/drivers/${x}" || return $? 7z e -y -bd -bb0 -o"${destdir}/drivers/${x}" -- "${virtio_iso}" "${x}/w11/${virtio_arch}/*" >/dev/null || return $? has "${x}" "${VIRTIO_WINPE_DRIVERS[@]}" && { echo "Copying ${x} into \$WinPEDriver\$..." >&2 find "${destdir}/drivers/${x}/" -maxdepth 1 -type f -exec cp '{}' "${destdir}/\$WinPEDriver\$/" \; || return $? } done } function qemu_generate_unattend_iso { local destfile="${1}" local destdir="${BASEDIR}/unattend.tmp" mkdir -p "${destdir}" || return $? qemu_fill_unattend_fs "${destdir}" "${2}" "${3}" || return $? if type -p "mkisofs" &>/dev/null; then mkisofs \ -iso-level 3 -J -l -D -N -U -joliet-long -relaxed-filenames -rational-rock \ -V "win11-builder-unattend" -o "${destfile}" -quiet \ "${destdir}" || return $? elif type -p "genisoimage" &>/dev/null; then genisoimage \ -iso-level 3 -J -l -D -N -U -joliet-long -allow-limited-size -relaxed-filenames -rock \ -V "win11-builder-unattend" -o "${destfile}" -quiet \ "${destdir}" || return $? elif type -p "xorriso" &>/dev/null; then echo "ERROR: xorriso support is pending" >&2 return 1 else echo "ERROR: Failed to find one of mkisofs | genisofs | xorriso" >&2 return 1 fi # cleanup find "${destdir}" -delete || return $? } function qemu_prepare_disk { local QEMU_DISK_FILE="${1}" local QEMU_DISK_SIZE="${2:-32G}" # Remove leftover temp files rm -f "${QEMU_DISK_FILE}.tmp" 2>/dev/null if [[ ! -f "${QEMU_DISK_FILE}" || "${FORCE_RESET}" -eq 1 ]]; then echo "Creating QEMU disk image..." >&2 qemu-img create -f qcow2 "${QEMU_DISK_FILE}" "${QEMU_DISK_SIZE}" >/dev/null || return $? elif [[ -f "${QEMU_DISK_FILE}" && "${COMMAND}" != "build" ]]; then local QEMU_COW_FILE="${QEMU_DISK_FILE}.cow" local QEMU_COW_SIZE="$(qemu_image_size "${QEMU_DISK_FILE}")" || return $? echo "Creating QEMU CoW disk image for '$(basename "${QEMU_DISK_FILE}")' ..." >&2 qemu-img create -f qcow2 -B qcow2 -b "${QEMU_DISK_FILE}" "${QEMU_COW_FILE}" "${QEMU_COW_SIZE}" >/dev/null || return $? fi QEMU_DYN_OPTIONS+=( "-drive if=none,format=qcow2,file=${QEMU_COW_FILE:-${QEMU_DISK_FILE}},discard=unmap,id=hd0,index=0" "-device virtio-blk-pci,drive=hd0,bootindex=1" ) } function qemu_prepare_cdrom_amd64 { local QEMU_BOOTINDEX=2 # First non-HD drive bootindex local QEMU_DRIVEINDEX=1 # First non-HD drive index # Only assign cdrom drives for build command (TODO: clean this up) [[ "${COMMAND}" == "build" ]] || return 0; if [[ -n "${UNATTEND_CONFIG}" ]]; then local unattend_iso="${BASEDIR}/unattend.iso" qemu_generate_unattend_iso "${unattend_iso}" "${UNATTEND_CONFIG}" "${VIRTIO_ISO}" || return $? QEMU_DYN_OPTIONS+=( "-drive if=none,file=${unattend_iso},media=cdrom,id=cd2,index=$(( QEMU_DRIVEINDEX++ ))" "-device ide-cd,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ )),bus=ahci.2" ) #local unattend_tmp="${BASEDIR}/unattend" #mkdir -p "${unattend_tmp}" || return $? #qemu_fill_unattend_fs "${unattend_tmp}" "${UNATTEND_CONFIG}" "${VIRTIO_ISO}" || return $? #QEMU_DYN_OPTIONS+=( # "-drive if=none,file=fat:rw:${unattend_tmp},format=raw,id=cd2,index=$(( QEMU_DRIVEINDEX++ ))" # "-device usb-storage,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ )),removable=on" #) elif [[ -n "${UNATTEND_ISO}" ]]; then QEMU_DYN_OPTIONS+=( "-drive if=none,file=${UNATTEND_ISO},media=cdrom,id=cd2,index=$(( QEMU_DRIVEINDEX++ ))" "-device ide-cd,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ )),bus=ahci.2" ) fi [[ -n "${INSTALL_ISO}" ]] && { QEMU_DYN_OPTIONS+=( "-drive if=none,file=${INSTALL_ISO},media=cdrom,id=cd0,index=$(( QEMU_DRIVEINDEX++ ))" "-device ide-cd,drive=cd0,bootindex=$(( QEMU_BOOTINDEX++ )),bus=ahci.0" ) } [[ -n "${VIRTIO_ISO}" ]] && { QEMU_DYN_OPTIONS+=( "-drive if=none,file=${VIRTIO_ISO},media=cdrom,id=cd1,index=$(( QEMU_DRIVEINDEX++ ))" "-device ide-cd,drive=cd1,bootindex=$(( QEMU_BOOTINDEX++ )),bus=ahci.1" ) } } function qemu_prepare_cdrom_arm64 { local QEMU_BOOTINDEX=2 # First non-HD drive bootindex local QEMU_DRIVEINDEX=1 # First non-HD drive index # Only assign cdrom drives for build command (TODO: clean this up) [[ "${COMMAND}" == "build" ]] || return 0; if [[ -n "${UNATTEND_CONFIG}" ]]; then local unattend_iso="${BASEDIR}/unattend.iso" qemu_generate_unattend_iso "${unattend_iso}" "${UNATTEND_CONFIG}" "${VIRTIO_ISO}" || return $? QEMU_DYN_OPTIONS+=( "-drive if=none,file=${unattend_iso},media=cdrom,id=cd2,index=$(( QEMU_DRIVEINDEX++ ))" "-device usb-storage,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ ))" ) # Using Qemu's VVFAT support to create an unattend drive (NOK: blocks C: / Disk #0) #local unattend_tmp="${BASEDIR}/unattend" #mkdir -p "${unattend_tmp}" || return $? #qemu_fill_unattend_fs "${unattend_tmp}" "${UNATTEND_CONFIG}" "${VIRTIO_ISO}" || return $? #QEMU_DYN_OPTIONS+=( # "-drive if=none,file=fat:${unattend_tmp},media=disk,format=raw,id=cd2,index=$(( QEMU_DRIVEINDEX++ ))" # "-device usb-storage,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ )),removable=on" #) elif [[ -n "${UNATTEND_ISO}" ]]; then QEMU_DYN_OPTIONS+=( "-drive if=none,file=${UNATTEND_ISO},media=cdrom,id=cd2,index=$(( QEMU_DRIVEINDEX++ ))" "-device usb-storage,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ ))" ) fi [[ -n "${INSTALL_ISO}" ]] && { QEMU_DYN_OPTIONS+=( "-drive if=none,file=${INSTALL_ISO},media=cdrom,id=cd0,index=$(( QEMU_DRIVEINDEX++ ))" "-device usb-storage,drive=cd0,bootindex=$(( QEMU_BOOTINDEX++ ))" ) } [[ -n "${VIRTIO_ISO}" ]] && { QEMU_DYN_OPTIONS+=( "-drive if=none,file=${VIRTIO_ISO},media=cdrom,id=cd1,index=$(( QEMU_DRIVEINDEX++ ))" "-device usb-storage,drive=cd1,bootindex=$(( QEMU_BOOTINDEX++ ))" ) } } function qemu_prepare_efi_code_and_vars { local SRC_EFI_CODE_IMG="${1}" # Required: EFI code image path local SRC_EFI_VARS_IMG="${2}" # Optional: EFI vars image path local SRC_EFI_CODE_SIZE="${3}" # Optional: EFI code image size # Code image must exist [[ -f "${SRC_EFI_CODE_IMG}" ]] || { echo "EFI code ROM image '${SRC_EFI_CODE_IMG}' does not exist" >&2 return 1 } local QEMU_EFI_CODE_SIZE="$(qemu_image_size "${SRC_EFI_CODE_IMG}")" local QEMU_EFI_CODE_TYPE="$(qemu_image_type_from_fileext "${SRC_EFI_CODE_IMG}")" local QEMU_EFI_CODE="${BASEDIR}/QEMU_EFI_CODE.$(fileext_from_qemu_image_type "${QEMU_EFI_CODE_TYPE}")" if [[ ! -f "${QEMU_EFI_CODE}" || "${FORCE_RESET}" -eq 1 ]]; then echo "Copying EFI code ROM image '$(basename "${SRC_EFI_CODE_IMG}")'..." >&2 cp -f "${SRC_EFI_CODE_IMG}" "${QEMU_EFI_CODE}" || return $? resize_image "${QEMU_EFI_CODE}" "${SRC_EFI_CODE_SIZE:-}" || return $? else local src_checksum="$(sha256sum -b "${SRC_EFI_CODE_IMG}" | cut -d ' ' -f1)" || return $? local dst_checksum="$(sha256sum -b "${QEMU_EFI_CODE}" | cut -d ' ' -f1)" || return $? if [[ "${src_checksum}" != "${dst_checksum}" ]]; then echo "Copying EFI code ROM image '$(basename "${SRC_EFI_CODE_IMG}")' (changed)..." >&2 cp -f "${SRC_EFI_CODE_IMG}" "${QEMU_EFI_CODE}" || return $? resize_image "${QEMU_EFI_CODE}" "${SRC_EFI_CODE_SIZE:-}" || return $? fi fi # # UEFI variables flash rom # Intialize a fresh new raw image # if [[ -n "${SRC_EFI_VARS_IMG}" ]]; then # Vars image must exist [[ -f "${SRC_EFI_VARS_IMG}" ]] || { echo "EFI variables ROM image '${SRC_EFI_VARS_IMG}' does not exist" >&2 return 1 } local QEMU_EFI_VARS_TYPE="$(qemu_image_type_from_fileext "${SRC_EFI_VARS_IMG}")" || return $? local QEMU_EFI_VARS="${BASEDIR}/QEMU_EFI_VARS.$(fileext_from_qemu_image_type "${QEMU_EFI_VARS_TYPE}")" local QEMU_EFI_VARS_SIZE="$(qemu_image_size "${QEMU_EFI_VARS}")" if [[ ! -f "${QEMU_EFI_VARS}" || "${FORCE_RESET}" -eq 1 ]]; then echo "Copying EFI variables ROM image '$(basename "${SRC_EFI_VARS_IMG}")'..." >&2 cp -f "${SRC_EFI_VARS_IMG}" "${QEMU_EFI_VARS}" || return $? else local src_checksum="$(sha256sum -b "${SRC_EFI_CODE_IMG}" | cut -d ' ' -f1)" || return $? local dst_checksum="$(sha256sum -b "${QEMU_EFI_VARS}" | cut -d ' ' -f1)" || return $? if [[ "${src_checksum}" != "${dst_checksum}" ]]; then echo "Copying EFI variables ROM image '$(basename "${SRC_EFI_VARS_IMG}")' (changed)..." >&2 cp -f "${SRC_EFI_VARS_IMG}" "${QEMU_EFI_VARS}" || return $? fi fi else local QEMU_EFI_VARS="${BASEDIR}/QEMU_EFI_VARS.fd" local QEMU_EFI_VARS_TYPE="$(qemu_image_type_from_fileext "${QEMU_EFI_VARS}")" || return $? local QEMU_EFI_VARS_SIZE="$(qemu_image_size "${QEMU_EFI_CODE}")" # Match code image size echo "Initializing UEFI variables flash ROM..." >&2 case "${QEMU_EFI_VARS_TYPE}" in "qcow2") qemu-img create -t qcow2 "${QEMU_EFI_VARS}" "${QEMU_EFI_VARS_SIZE}" >/dev/null || return $? ;; "raw") truncate --size=${QEMU_EFI_VARS_SIZE} "${QEMU_EFI_VARS}" || return $? ;; *) echo "Unknown image type '${QEMU_EFI_VARS_TYPE}'" >&2; return 1 ;; esac fi # Append architecture specific configuration QEMU_DYN_OPTIONS+=( "-drive if=pflash,format=${QEMU_EFI_CODE_TYPE},unit=0,file=${QEMU_EFI_CODE},readonly=on" "-drive if=pflash,format=${QEMU_EFI_VARS_TYPE},unit=1,file=${QEMU_EFI_VARS}" ) } function qemu_prepare_amd64 { # Source images for EFI code and variables local AMD64_EFI_CODE_IMG="/usr/share/edk2/OvmfX64/OVMF_CODE_4M.qcow2" local AMD64_EFI_VARS_IMG="" qemu_prepare_efi_code_and_vars "${AMD64_EFI_CODE_IMG}" "${AMD64_EFI_VARS_IMG}" || return $? qemu_prepare_cdrom_amd64 "${INSTALL_ISO}" "${VIRTIO_ISO}" "${UNATTEND_ISO}" || return $? } function qemu_prepare_arm64 { # Source images for EFI code and variables qemu_prepare_efi_code_and_vars "${ARM64_EFI_CODE_IMG}" "${ARM64_EFI_VARS_IMG}" "${ARM64_EFI_CODE_SIZE}" || return $? qemu_prepare_cdrom_arm64 "${INSTALL_ISO}" "${VIRTIO_ISO}" "${UNATTEND_ISO}" || return $? } function qemu_run_amd64_native { echo "Starting X64 QEMU KVM..." >&2 qemu-system-x86_64 \ -machine type=q35,usb=on,acpi=on,hpet=off -accel kvm -boot menu=off \ ${CPU_OPTIONS_AMD64} ${MEM_OPTIONS} \ -device virtio-gpu-pci,edid=on,xres=1280,yres=800 -vga virtio \ -device qemu-xhci -device usb-kbd -device usb-tablet \ -device ich9-ahci,id=ahci \ ${QEMU_DYN_OPTIONS[@]} \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0,mac=2A:50:A7:4E:D9:C5 \ -display gtk,show-tabs=on,show-menubar=on,zoom-to-fit=off \ -monitor unix:${BASEDIR}/monitor.sock,server,nowait \ -vnc unix:${BASEDIR}/vnc.sock,password=on \ -nodefaults } function qemu_run_arm64_native { # Return list of the big CPU cores on the system (TODO: Check for Cortex-A72+ before attempting to start) local CPU_CONFIG="$(lscpu -J -b -e=CPU,MODELNAME)" [[ -n "${CPU_CONFIG}" ]] || { echo "Failed to retrieve CPU configuration" >&2 return 1 } local CPU_MODELS="$(jq -r '.cpus | map(.modelname) | unique | map(select(. | test("A72|A76"))) | join("\n")' <<< "${CPU_CONFIG}")" [[ -n "${CPU_MODELS}" ]] || { echo "No supported CPU models found" >&2 return 1 } local SELECTED_CPU_CORES="$(jq -r '.cpus | group_by(.modelname) | sort_by(.[].modelname) | last | map(.cpu) | join(",")' <<< "${CPU_CONFIG}")" [[ -n "${SELECTED_CPU_CORES}" ]] || { echo "No supported CPU cores found" >&2 return 1 } echo "Starting ARM64 QEMU KVM (on cores ${SELECTED_CPU_CORES})..." >&2 taskset -c "${SELECTED_CPU_CORES}" \ qemu-system-aarch64 \ -machine type=virt,virtualization=off,acpi=on -accel kvm -boot menu=off \ ${CPU_OPTIONS_ARM64} ${MEM_OPTIONS} \ -device virtio-gpu-pci,edid=on,xres=1280,yres=800 -device ramfb \ -device qemu-xhci -device usb-kbd -device usb-tablet \ ${QEMU_DYN_OPTIONS[@]} \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0,mac=2A:50:A7:4E:D9:C5 \ -display gtk,show-tabs=on,show-menubar=on,zoom-to-fit=off \ -monitor unix:${BASEDIR}/monitor.sock,server,nowait \ -vnc unix:${BASEDIR}/vnc.sock,password=on \ -nodefaults } function qemu_run_arm64_emulated { echo "Starting ARM64 QEMU TCG..." >&2 qemu-system-aarch64 \ -machine type=virt,virtualization=off,acpi=on -accel tcg,thread=multi -boot menu=off \ ${CPU_OPTIONS_ARM64} ${MEM_OPTIONS} \ -device virtio-gpu-pci,edid=on,xres=1280,yres=800 -device ramfb \ -device qemu-xhci -device usb-kbd -device usb-tablet \ ${QEMU_DYN_OPTIONS[@]} \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0,mac=2A:50:A7:4E:D9:C5 \ -display gtk,show-tabs=on,show-menubar=on,zoom-to-fit=off \ -monitor unix:${BASEDIR}/monitor.sock,server,nowait \ -vnc unix:${BASEDIR}/vnc.sock,password=on \ -nodefaults } function qemu_prepare_and_run { # # Prepare QEMU VM resources # case "${GUEST_ARCH}" in "amd64") qemu_prepare_amd64 || return $? ;; "arm64") qemu_prepare_arm64 || return $? ;; *) echo "Unsupported guest architecture '${GUEST_ARCH}'" >&2 return 1 ;; esac qemu_prepare_disk "${DISK_IMG}" "${DISK_SIZE}" || return $? # # Run QEMU VM # case "${HOST_ARCH}:${GUEST_ARCH}" in "amd64:amd64") # Native: AMD64 on AMD64 qemu_run_amd64_native ;; "amd64:arm64") # Emulated: ARM64 on AMD64 echo 'NOTICE: Running ARM64 emulated on AMD64, this will be slow' >&2 qemu_run_arm64_emulated ;; "arm64:arm64") # Native: ARM64 on ARM64 qemu_run_arm64_native ;; *) echo "Unsupported host + guest architecture combination: '${GUEST_ARCH}' on '${HOST_ARCH}'" >&2 return 1 ;; esac } function recompress_disk_image { local IMG_FILE="${1}" local IMG_SIZE="$(stat -c '%s' "${IMG_FILE}")" || return $? [[ ${IMG_SIZE} -lt $(( 2 ** 30 )) ]] && { echo 'Disk is smaller than 1 GiB, skipping recompression' >&2 return 0 } echo "Optimizing QEMU disk image..." >&2 qemu-img convert -p -c -W -f qcow2 -O qcow2 "${IMG_FILE}" "${IMG_FILE}.tmp" || { rm -f "${IMG_FILE}.tmp" 2>/dev/null return 1 } local IMG_SIZE_ORG="$(stat -c 'scale=2; %s / 2^30' "${IMG_FILE}" | bc)" local IMG_SIZE_OPT="$(stat -c 'scale=2; %s / 2^30' "${IMG_FILE}.tmp" | bc)" local PERCENT="$(bc <<< "scale=2; (${IMG_SIZE_OPT} * 100) / ${IMG_SIZE_ORG}")" echo "'$(basename "${IMG_FILE}")': ${IMG_SIZE_ORG} GiB -> ${IMG_SIZE_OPT} GiB (${PERCENT} %)" >&2 mv -f "${IMG_FILE}.tmp" "${IMG_FILE}" || return $? } function handle_build_command { qemu_prepare_and_run || return $? recompress_disk_image "${DISK_IMG}" || return $? } function handle_run_command { qemu_prepare_and_run || return $? } # # Handle user-supplied command # case "${COMMAND@L}" in "build") handle_build_command || exit $? ;; "run") handle_run_command || exit $? ;; esac