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
This commit is contained in:
Martin Wimpress 2026-01-24 12:31:15 +00:00 committed by Martin Wimpress
parent 2fe51d5671
commit 1783381e29
4 changed files with 288 additions and 15 deletions

View File

@ -16,7 +16,9 @@ mkShell {
gawk
gnugrep
gnused
gptfdisk
jq
mtools
pciutils
procps
python3

View File

@ -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

View File

@ -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

233
quickget
View File

@ -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 <output_disk> <disk_size> <opencore_dir>
# 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 <destination_dir>
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
}