e577b48c6c
Signed-off-by: Stefan Knoblich <stkn@bitplumber.de>
555 lines
18 KiB
Bash
Executable File
555 lines
18 KiB
Bash
Executable File
#!/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 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}" ]]; 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"
|
|
"-device virtio-blk-pci,drive=hd0,bootindex=1"
|
|
)
|
|
}
|
|
|
|
function qemu_prepare_cdrom_amd64 {
|
|
local QEMU_BOOTINDEX=2 # First non-HD drive bootindex
|
|
|
|
# Only assign cdrom drives for build command (TODO: clean this up)
|
|
[[ "${COMMAND}" == "build" ]] || return 0;
|
|
|
|
[[ -n "${UNATTEND_ISO}" ]] && {
|
|
QEMU_DYN_OPTIONS+=(
|
|
"-drive if=none,file=${UNATTEND_ISO},media=cdrom,id=cd2"
|
|
"-device ide-cd,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ )),bus=ahci.2"
|
|
)
|
|
}
|
|
|
|
[[ -n "${INSTALL_ISO}" ]] && {
|
|
QEMU_DYN_OPTIONS+=(
|
|
"-drive if=none,file=${INSTALL_ISO},media=cdrom,id=cd0"
|
|
"-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"
|
|
"-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
|
|
|
|
# Only assign cdrom drives for build command (TODO: clean this up)
|
|
[[ "${COMMAND}" == "build" ]] || return 0;
|
|
|
|
[[ -n "${UNATTEND_ISO}" ]] && {
|
|
QEMU_DYN_OPTIONS+=(
|
|
"-drive if=none,file=${UNATTEND_ISO},media=cdrom,id=cd2"
|
|
"-device usb-storage,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ ))"
|
|
)
|
|
}
|
|
|
|
[[ -n "${INSTALL_ISO}" ]] && {
|
|
QEMU_DYN_OPTIONS+=(
|
|
"-drive if=none,file=${INSTALL_ISO},media=cdrom,id=cd0"
|
|
"-device usb-storage,drive=cd0,bootindex=$(( QEMU_BOOTINDEX++ ))"
|
|
)
|
|
}
|
|
|
|
[[ -n "${VIRTIO_ISO}" ]] && {
|
|
QEMU_DYN_OPTIONS+=(
|
|
"-drive if=none,file=${VIRTIO_ISO},media=cdrom,id=cd1"
|
|
"-device usb-storage,drive=cd1,bootindex=$(( QEMU_BOOTINDEX++ ))"
|
|
)
|
|
}
|
|
}
|
|
|
|
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 resize_image {
|
|
local img_file="${1}"
|
|
|
|
local img_cur_size="$(qemu_image_size "${1}")"
|
|
local img_req_size="$(expand_size "${2:-${img_cur_size}}")"
|
|
[[ "${img_cur_size}" -eq "${img_req_size}" ]] && return 0;
|
|
[[ "${img_cur_size}" -lt "${img_req_size}" ]] || return 1;
|
|
|
|
local img_type="$(qemu_image_type_from_fileext "${img_file}")"
|
|
case "${img_type}" in
|
|
raw)
|
|
echo "Resizing raw image '$(basename "${img_file}")'" >&2
|
|
truncate --size="${img_req_size}" "${img_file}" || return $? ;;
|
|
*)
|
|
echo "Qcow2 image resizing is currently not implemented" >&2
|
|
return 1 ;;
|
|
esac
|
|
}
|
|
|
|
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
|