1
0
Files
win11-builder/win11-builder-generic.sh
T
stkn e577b48c6c WIP
Signed-off-by: Stefan Knoblich <stkn@bitplumber.de>
2026-06-04 17:56:16 +02:00

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