From 1783381e298f95a662361cae06005a5242eb0a54 Mon Sep 17 00:00:00 2001 From: Martin Wimpress Date: Sat, 24 Jan 2026 12:31:15 +0000 Subject: [PATCH] feat(quickget): integrate OpenCore into macOS disk image by default Create macOS VMs with OpenCore embedded in the EFI partition of disk.qcow2 instead of using a separate OpenCore.qcow2 file. This simplifies VM management by reducing from two disk images to one. Implementation: - Add create_macos_disk_with_opencore() using mtools/sgdisk for cross-platform EFI partition creation without mounting or root privileges - Add download_opencore() to extract OpenCore files from OSX-KVM image - Use LC_ALL='' with mcopy to prevent FAT directory name mangling - Adjust disk size threshold for macOS integrated mode (1GB vs 1.5MB) Backwards compatibility: - If OpenCore.qcow2 exists, use legacy two-disk boot method - If mtools/sgdisk unavailable, fall back to legacy method automatically New dependencies: mtools, gptfdisk (added to devshell.nix and package.nix) Closes #1720 --- devshell.nix | 2 + package.nix | 4 + quickemu | 64 +++++++++++--- quickget | 233 ++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 288 insertions(+), 15 deletions(-) diff --git a/devshell.nix b/devshell.nix index e3b161d..aa27075 100644 --- a/devshell.nix +++ b/devshell.nix @@ -16,7 +16,9 @@ mkShell { gawk gnugrep gnused + gptfdisk jq + mtools pciutils procps python3 diff --git a/package.nix b/package.nix index 51d2008..444e494 100644 --- a/package.nix +++ b/package.nix @@ -10,8 +10,10 @@ gawk, gnugrep, gnused, + gptfdisk, jq, mesa-demos, + mtools, pciutils, procps, python3, @@ -37,7 +39,9 @@ let gawk gnugrep gnused + gptfdisk jq + mtools pciutils procps python3 diff --git a/quickemu b/quickemu index f01bad1..8b58b16 100755 --- a/quickemu +++ b/quickemu @@ -666,13 +666,21 @@ function configure_bios() { MAC_MISSING="Firmware" fi + # Check for OpenCore bootloader + # Backwards compatibility: If OpenCore.qcow2 exists, use legacy two-disk boot method + # If OpenCore.qcow2 is absent, assume OpenCore is integrated in the main disk if [ -e "${VMDIR}/OpenCore.qcow2" ]; then MAC_BOOTLOADER="${VMDIR}/OpenCore.qcow2" + MAC_BOOT_MODE="legacy" elif [ -e "${VMDIR}/ESP.qcow2" ]; then # Backwards compatibility for Clover MAC_BOOTLOADER="${VMDIR}/ESP.qcow2" + MAC_BOOT_MODE="legacy" else - MAC_MISSING="Bootloader" + # New method: OpenCore is integrated in the main disk's EFI partition + # No separate bootloader file needed + MAC_BOOTLOADER="" + MAC_BOOT_MODE="integrated" fi if [ -n "${MAC_MISSING}" ]; then @@ -680,7 +688,11 @@ function configure_bios() { echo " Use 'quickget' to download the required files." exit 1 fi - BOOT_STATUS="EFI (macOS), OVMF ($(basename "${EFI_CODE}")), SecureBoot (${secureboot})." + if [ "${MAC_BOOT_MODE}" == "integrated" ]; then + BOOT_STATUS="EFI (macOS), OVMF ($(basename "${EFI_CODE}")), OpenCore (integrated), SecureBoot (${secureboot})." + else + BOOT_STATUS="EFI (macOS), OVMF ($(basename "${EFI_CODE}")), SecureBoot (${secureboot})." + fi elif [[ "${boot}" == *"efi"* ]]; then EFI_VARS="${VMDIR}/OVMF_VARS.fd" @@ -902,7 +914,15 @@ function configure_storage() { # 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 + # For macOS with integrated OpenCore, the disk is pre-created with an EFI + # partition containing OpenCore (~500MB). Use a higher threshold (1GB) to + # distinguish between "just EFI" and "EFI + installed macOS". + if [ "${guest_os}" == "macos" ] && [ "${MAC_BOOT_MODE}" == "integrated" ]; then + DISK_MIN_SIZE_CHECK=$((1024 * 1024 * 1024)) + else + DISK_MIN_SIZE_CHECK="${DISK_MIN_SIZE}" + fi + if [ "${DISK_CURR_SIZE}" -le "${DISK_MIN_SIZE_CHECK}" ]; 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." @@ -1244,6 +1264,7 @@ function vm_boot() { OS_RELEASE="Unknown OS" MACHINE_TYPE="${MACHINE_TYPE:-q35}" MAC_BOOTLOADER="" + MAC_BOOT_MODE="" MAC_MISSING="" MAC_DISK_DEV="${MAC_DISK_DEV:-ide-hd,bus=ahci.2}" NET_DEVICE="${NET_DEVICE:-virtio-net-pci}" @@ -1519,19 +1540,36 @@ function vm_boot() { 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}") + args+=(-device ahci,id=ahci) - if [ -n "${img}" ]; then + if [ -n "${MAC_BOOTLOADER}" ]; then + # Legacy mode: boot from separate OpenCore.qcow2 # shellcheck disable=SC2054 - args+=(-device ide-hd,bus=ahci.1,drive=RecoveryImage - -drive id=RecoveryImage,if=none,format=raw,file="${img}") - fi + args+=(-device ide-hd,bus=ahci.0,drive=BootLoader,bootindex=0 + -drive id=BootLoader,if=none,format=qcow2,file="${MAC_BOOTLOADER}") - # shellcheck disable=SC2054,SC2206 - args+=(-device ${MAC_DISK_DEV},drive=SystemDisk - -drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}" ${STATUS_QUO}) + 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}" ${STATUS_QUO}) + else + # Integrated mode: OpenCore is in the main disk's EFI partition + # Boot directly from the main disk + # shellcheck disable=SC2054,SC2206 + args+=(-device ${MAC_DISK_DEV},drive=SystemDisk,bootindex=0 + -drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}" ${STATUS_QUO}) + + if [ -n "${img}" ]; then + # shellcheck disable=SC2054 + args+=(-device ide-hd,bus=ahci.0,drive=RecoveryImage + -drive id=RecoveryImage,if=none,format=raw,file="${img}") + fi + fi elif [ "${guest_os}" == "kolibrios" ]; then # shellcheck disable=SC2054,SC2206 args+=(-device ahci,id=ahci diff --git a/quickget b/quickget index 6e16b80..7ac1038 100755 --- a/quickget +++ b/quickget @@ -215,6 +215,192 @@ function require_qemu_img() { fi } +function require_mtools() { + local MFORMAT="" + local MCOPY="" + local MMD="" + MFORMAT=$(command -v mformat) + MCOPY=$(command -v mcopy) + MMD=$(command -v mmd) + if [ ! -x "${MFORMAT}" ] || [ ! -x "${MCOPY}" ] || [ ! -x "${MMD}" ]; then + echo "ERROR! mtools not found. Please install mtools (mformat, mcopy, mmd)." + exit 1 + fi +} + +function require_sgdisk() { + local SGDISK="" + SGDISK=$(command -v sgdisk) + if [ ! -x "${SGDISK}" ]; then + echo "ERROR! sgdisk not found. Please install gptfdisk (gdisk package)." + exit 1 + fi +} + +# Create a raw disk image with GPT partition table and EFI partition containing OpenCore +# Usage: create_macos_disk_with_opencore +# The disk will have: +# - Partition 1: EFI System Partition (200MB FAT32) containing OpenCore +# - Remaining space: unallocated (macOS will create its partitions during install) +function create_macos_disk_with_opencore() { + local DISK_PATH="${1}" + local DISK_SIZE="${2}" + local OPENCORE_DIR="${3}" + local TEMP_DISK="" + local EFI_OFFSET_BYTES="" + local SGDISK="" + local MFORMAT="" + local MMD="" + local MCOPY="" + + require_sgdisk + require_mtools + require_qemu_img + + SGDISK=$(command -v sgdisk) + MFORMAT=$(command -v mformat) + MMD=$(command -v mmd) + MCOPY=$(command -v mcopy) + + echo " - Creating macOS disk with integrated OpenCore..." + + # Create a temporary raw disk image + TEMP_DISK="${DISK_PATH}.raw" + + # Create the raw disk image + if ! ${QEMU_IMG} create -f raw "${TEMP_DISK}" "${DISK_SIZE}" >/dev/null 2>&1; then + echo "ERROR! Failed to create raw disk image." + rm -f "${TEMP_DISK}" + return 1 + fi + + # Create GPT partition table with EFI partition + # Partition 1: EFI System Partition, 200MB starting at sector 2048 (1MiB offset) + # Using type EF00 (EFI System) + if ! ${SGDISK} --clear \ + --new=1:2048:+200M --typecode=1:EF00 --change-name=1:"EFI" \ + "${TEMP_DISK}" >/dev/null 2>&1; then + echo "ERROR! Failed to create GPT partition table." + rm -f "${TEMP_DISK}" + return 1 + fi + + # Calculate EFI partition offset in bytes (sector 2048 * 512 bytes/sector = 1MiB) + EFI_OFFSET_BYTES=$((2048 * 512)) + + # Format the EFI partition as FAT32 using mtools + # The -i option with @@offset allows operating on a partition within an image + if ! ${MFORMAT} -i "${TEMP_DISK}@@${EFI_OFFSET_BYTES}" -F -v "EFI" :: 2>/dev/null; then + echo "ERROR! Failed to format EFI partition." + rm -f "${TEMP_DISK}" + return 1 + fi + + # Create the EFI directory structure + ${MMD} -i "${TEMP_DISK}@@${EFI_OFFSET_BYTES}" ::/EFI 2>/dev/null + # Note: Only create the top-level EFI directory; mcopy -s will create subdirectories + + # Copy OpenCore files to the EFI partition + # mcopy -s recursively copies directories including their contents + # This preserves .kext bundle structure (directories with Contents/Info.plist) + if [ -d "${OPENCORE_DIR}/EFI/BOOT" ]; then + if ! ${MCOPY} -i "${TEMP_DISK}@@${EFI_OFFSET_BYTES}" -s "${OPENCORE_DIR}/EFI/BOOT" ::/EFI/; then + echo "ERROR! Failed to copy EFI/BOOT to disk." + rm -f "${TEMP_DISK}" + return 1 + fi + fi + if [ -d "${OPENCORE_DIR}/EFI/OC" ]; then + if ! ${MCOPY} -i "${TEMP_DISK}@@${EFI_OFFSET_BYTES}" -s "${OPENCORE_DIR}/EFI/OC" ::/EFI/; then + echo "ERROR! Failed to copy EFI/OC to disk." + rm -f "${TEMP_DISK}" + return 1 + fi + fi + + # Convert the raw image to qcow2 format + if ! ${QEMU_IMG} convert -f raw -O qcow2 "${TEMP_DISK}" "${DISK_PATH}" >/dev/null 2>&1; then + echo "ERROR! Failed to convert disk to qcow2 format." + rm -f "${TEMP_DISK}" + return 1 + fi + + # Clean up temporary raw image + rm -f "${TEMP_DISK}" + + echo " - macOS disk with integrated OpenCore created successfully." + return 0 +} + +# Download and extract OpenCore files from OSX-KVM repository +# Usage: download_opencore +function download_opencore() { + local DEST_DIR="${1}" + local OPENCORE_URL="https://github.com/kholia/OSX-KVM/raw/master/OpenCore/OpenCore.qcow2" + local TEMP_QCOW2="" + local TEMP_RAW="" + local MCOPY="" + + require_qemu_img + require_mtools + + MCOPY=$(command -v mcopy) + TEMP_QCOW2="${DEST_DIR}/OpenCore_temp.qcow2" + TEMP_RAW="${DEST_DIR}/OpenCore_temp.raw" + + echo " - Downloading OpenCore bootloader..." + if ! web_get "${OPENCORE_URL}" "${DEST_DIR}" "OpenCore_temp.qcow2"; then + echo "ERROR! Failed to download OpenCore." + return 1 + fi + + # Convert qcow2 to raw so we can extract files with mtools + echo " - Extracting OpenCore files..." + if ! ${QEMU_IMG} convert -f qcow2 -O raw "${TEMP_QCOW2}" "${TEMP_RAW}" >/dev/null 2>&1; then + echo "ERROR! Failed to convert OpenCore image." + rm -f "${TEMP_QCOW2}" + return 1 + fi + + # Create destination directory (mcopy will create BOOT and OC subdirs) + mkdir -p "${DEST_DIR}/EFI" + + # The OpenCore.qcow2 from OSX-KVM is a disk image with an EFI partition + # The EFI partition starts at sector 2048 (1MiB offset) + local EFI_OFFSET=$((2048 * 512)) + + # Extract files using mtools + # Note: mcopy -s recursively copies directories, preserving the full structure + # including .kext bundles (which are directories containing Contents/Info.plist) + # LC_ALL='' prevents mtools from mangling directory names (BOOT -> BOOT_) + if ! LC_ALL='' ${MCOPY} -i "${TEMP_RAW}@@${EFI_OFFSET}" -s ::/EFI/BOOT "${DEST_DIR}/EFI/"; then + echo "ERROR! Failed to extract EFI/BOOT from OpenCore image." + rm -f "${TEMP_QCOW2}" "${TEMP_RAW}" + return 1 + fi + if ! LC_ALL='' ${MCOPY} -i "${TEMP_RAW}@@${EFI_OFFSET}" -s ::/EFI/OC "${DEST_DIR}/EFI/"; then + echo "ERROR! Failed to extract EFI/OC from OpenCore image." + rm -f "${TEMP_QCOW2}" "${TEMP_RAW}" + return 1 + fi + + # Clean up temporary files + rm -f "${TEMP_QCOW2}" "${TEMP_RAW}" + + # Verify extraction was successful - check both boot file and a kext plist + if [ ! -f "${DEST_DIR}/EFI/BOOT/BOOTx64.efi" ]; then + echo "ERROR! Failed to extract OpenCore boot file." + return 1 + fi + if [ ! -f "${DEST_DIR}/EFI/OC/Kexts/Lilu.kext/Contents/Info.plist" ]; then + echo "ERROR! Failed to extract OpenCore kexts (Lilu.kext/Contents/Info.plist missing)." + return 1 + fi + + echo " - OpenCore files extracted successfully." + return 0 +} + function is_valid_language() { local I18N="" local PASSED_I18N="${1}" @@ -2054,6 +2240,9 @@ function get_macos() { local CHUNKCHECK="" local MLB="00000000000000000" local OS_TYPE="default" + local USE_INTEGRATED_OPENCORE=1 + local OPENCORE_TEMP_DIR="" + local MCOPY="" case ${RELEASE} in lion|10.7) @@ -2109,6 +2298,13 @@ function get_macos() { CHUNKCHECK="$(command -v chunkcheck)" fi + # Check if mtools and sgdisk are available for integrated OpenCore + MCOPY=$(command -v mcopy) + if [ ! -x "${MCOPY}" ] || [ ! -x "$(command -v sgdisk)" ]; then + echo " - NOTE: mtools or sgdisk not found, using legacy OpenCore.qcow2 method." + USE_INTEGRATED_OPENCORE=0 + fi + appleSession=$(curl --disable -v -H "Host: osrecovery.apple.com" \ -H "Connection: close" \ -A "InternetRecovery/1.0" https://osrecovery.apple.com/ 2>&1 | tr ';' '\n' | awk -F'session=|;' '{print $2}' | grep 1) @@ -2157,12 +2353,45 @@ function get_macos() { rm "${VM_PATH}/RecoveryImage.dmg" "${VM_PATH}/RecoveryImage.chunklist" echo " - RecoveryImage.img is ready." fi - echo "Downloading OpenCore & UEFI firmware" - web_get "https://github.com/kholia/OSX-KVM/raw/master/OpenCore/OpenCore.qcow2" "${VM_PATH}" + + echo "Downloading UEFI firmware" web_get "https://github.com/kholia/OSX-KVM/raw/master/OVMF_CODE.fd" "${VM_PATH}" if [ ! -e "${VM_PATH}/OVMF_VARS-1920x1080.fd" ]; then web_get "https://github.com/kholia/OSX-KVM/raw/master/OVMF_VARS-1920x1080.fd" "${VM_PATH}" fi + + if [ "${USE_INTEGRATED_OPENCORE}" -eq 1 ]; then + # Create disk with integrated OpenCore (new method) + echo "Creating disk with integrated OpenCore bootloader" + + # Create temporary directory for OpenCore extraction + OPENCORE_TEMP_DIR="${VM_PATH}/.opencore_temp" + mkdir -p "${OPENCORE_TEMP_DIR}" + + # Download and extract OpenCore files + if download_opencore "${OPENCORE_TEMP_DIR}"; then + # Create the main disk with integrated OpenCore EFI partition + # Default size is 128G (can be overridden in config) + if create_macos_disk_with_opencore "${VM_PATH}/disk.qcow2" "128G" "${OPENCORE_TEMP_DIR}"; then + echo " - Integrated OpenCore disk created successfully." + else + echo " - WARNING: Failed to create integrated OpenCore disk, falling back to legacy method." + USE_INTEGRATED_OPENCORE=0 + fi + else + echo " - WARNING: Failed to download OpenCore, falling back to legacy method." + USE_INTEGRATED_OPENCORE=0 + fi + + # Clean up temporary directory + rm -rf "${OPENCORE_TEMP_DIR}" + fi + + if [ "${USE_INTEGRATED_OPENCORE}" -eq 0 ]; then + # Legacy method: separate OpenCore.qcow2 file + echo "Downloading OpenCore bootloader (legacy method)" + web_get "https://github.com/kholia/OSX-KVM/raw/master/OpenCore/OpenCore.qcow2" "${VM_PATH}" + fi fi make_vm_config RecoveryImage.img }