Last active
April 24, 2024 18:34
-
-
Save akemin-dayo/8337d8274deddfefae5d1543420ca0b1 to your computer and use it in GitHub Desktop.
A cleaned up version of an internal script that I've been using while working on TotalFinder to create VirtualApple virtual machine instances that are hardlinked to a UTM virtual machine instance. It's particularly useful for entering One True recoveryOS (1TR) as well as using the other features found only in VirtualApple.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# virtualapple-utm-link | |
# Karen/あけみ (akemin_dayo) | |
# https://gist.github.com/akemin-dayo/8337d8274deddfefae5d1543420ca0b1.git | |
# This is a cleaned up version of an internal script that I've been using while working on TotalFinder to create VirtualApple virtual machine instances that are hardlinked to a UTM virtual machine instance. | |
# It's particularly useful for entering One True recoveryOS (1TR) as well as using the other features found only in VirtualApple. | |
# UTM does not support entering 1TR for macOS 12 hosts (yet?), though it is now possible on macOS 13 hosts (utmapp/UTM/issues/3526). | |
# Other VirtualApple features like booting into DFU mode, halting on panic / iBoot stage 1 / iBoot stage 2, or the GDB stub are also not supported on UTM, regardless of the macOS host version. | |
# Also, hardlinking is used because Apple's Virtualization.framework refuses to boot virtual machines from symlinked virtual disks for some reason. | |
COLOUR_CYANBOLD="\033[36;1m" | |
COLOUR_REDBOLD="\033[31;1m" | |
COLOUR_GREENBOLD="\033[32;1m" | |
COLOUR_YELLOWBOLD="\033[33;1m" | |
COLOUR_BOLD="\033[1m" | |
COLOUR_RESET="\033[0m" | |
KarenLog() { | |
echo -e "${COLOUR_CYANBOLD}[🍍 virtualapple-utm-link]${COLOUR_RESET} $@" | |
} | |
KarenError() { | |
echo -e "${COLOUR_CYANBOLD}[🍍 virtualapple-utm-link]${COLOUR_RESET} ${COLOUR_REDBOLD}[ERROR]${COLOUR_RESET} $@" | |
} | |
KarenWarning() { | |
echo -e "${COLOUR_CYANBOLD}[🍍 virtualapple-utm-link]${COLOUR_RESET} ${COLOUR_YELLOWBOLD}[WARNING]${COLOUR_RESET} $@" | |
} | |
get_git_commit_sha1() { | |
SCRIPT_DIR="$(dirname -- "${BASH_SOURCE[0]}")" | |
if [[ -d "${SCRIPT_DIR}/.git" ]]; then | |
GIT_SHA1="$(git -C "${SCRIPT_DIR}" describe --always --long)" | |
if [[ $? == 0 ]]; then | |
GIT_STRING="git-${GIT_SHA1}" | |
fi | |
fi | |
} | |
print_usage() { | |
echo -e "${COLOUR_CYANBOLD}🍍 virtualapple-utm-link${COLOUR_RESET} - Creates a VirtualApple virtual machine instance using hardlinks to a UTM virtual machine instance" | |
echo "(C) 2022-2023 Karen/あけみ (akemin_dayo)" | |
if [[ ! -z "${GIT_STRING}" ]]; then | |
echo "Version ${GIT_STRING}" | |
fi | |
echo "https://gist.github.com/akemin-dayo/8337d8274deddfefae5d1543420ca0b1.git" | |
echo "" | |
echo -e "${COLOUR_CYANBOLD}🍍 Usage:${COLOUR_RESET} virtualapple-utm-link ${COLOUR_GREENBOLD}[Optional: path/to/UTMVirtualMachine.utm]${COLOUR_RESET}" | |
} | |
{ | |
get_git_commit_sha1 | |
if [[ ${1} == "-h" ]] || [[ ${1} == "--help" ]] || [[ ${1} == "help" ]] || [[ ${1} == "?" ]]; then | |
print_usage | |
exit 0 | |
fi | |
######## Derive UTM VM instance path ######## | |
if [[ -f "${1}/config.plist" ]]; then | |
utmVMInstancePath="${1}" | |
elif [[ ! -z "${1}" ]]; then | |
KarenError "The path you provided (${COLOUR_GREENBOLD}${1}${COLOUR_RESET}) does not appear to be a UTM virtual machine instance!" | |
exit 1 | |
fi | |
if [[ -z "${utmVMInstancePath}" ]]; then | |
######## Check for UTM VMs in the default sandboxed path ######## | |
utmSandboxBundleID="com.utmapp.UTM" | |
utmVMInstanceRootPath="${HOME}/Library/Containers/${utmSandboxBundleID}/Data/Documents" | |
compgen -G "${utmVMInstanceRootPath}/"*".utm" > /dev/null 2>&1 | |
if [[ $? != 0 ]]; then | |
KarenError "You don't seem to have any UTM virtual machine instances in the default sandboxed path! (${COLOUR_GREENBOLD}${utmVMInstanceRootPath}${COLOUR_RESET})" | |
exit 1 | |
fi | |
######## Ask user to choose a UTM VM and derive the full path to it ######## | |
PS3="Please enter the number corresponding to the UTM virtual machine instance that you would like to use to create a hardlinked VirtualApple virtual machine instance for: " | |
select utmVMInstancePath in "${utmVMInstanceRootPath}/"*".utm"; do | |
if [[ ! -z ${utmVMInstancePath} ]]; then | |
break | |
else | |
KarenError "\"${REPLY}\" is not a valid choice! Please try again." | |
fi | |
done | |
unset PS3 | |
echo "" | |
fi | |
######## Define UTM config.plist path ######## | |
utmVMInstanceConfigPlistPath="${utmVMInstancePath}/config.plist" | |
######## Verify UTM configuration version ######## | |
utmVMInstanceConfigurationVersion="$(plutil -extract "ConfigurationVersion" raw "${utmVMInstanceConfigPlistPath}")" | |
if [[ "${utmVMInstanceConfigurationVersion}" -gt "4" ]]; then | |
KarenWarning "The UTM virtual machine instance that you have selected is using a newer configuration format (version ${utmVMInstanceConfigurationVersion}) than what this script was tested with (version 4)!" | |
KarenWarning "Things may or may not work correctly, depending on whether or not UTM made any breaking changes to the configuration format!" | |
echo "" | |
fi | |
######## Verify virtualisation backend ######## | |
utmVMInstanceBackend="$(plutil -extract "Backend" raw "${utmVMInstanceConfigPlistPath}")" | |
if [[ "${utmVMInstanceBackend}" != "Apple" ]]; then | |
KarenError "The UTM virtual machine instance that you have selected is not using the Apple Virtualization.framework backend!" | |
exit 1 | |
fi | |
######## Extract values ######## | |
# Instance name | |
utmVMInstanceNameFromConfigPlist="$(plutil -extract "Information.Name" raw "${utmVMInstanceConfigPlistPath}")" | |
utmVMInstanceNameFromFilesystemPath="$(basename "${utmVMInstancePath}")" | |
utmVMInstanceNameFromFilesystemPath="${utmVMInstanceNameFromFilesystemPath%.*}" | |
# CPU core count, memory size | |
cpuCount="$(plutil -extract "System.CPUCount" raw "${utmVMInstanceConfigPlistPath}")" | |
memorySizeMiB="$(plutil -extract "System.MemorySize" raw "${utmVMInstanceConfigPlistPath}")" | |
memorySizeBytes="$((memorySizeMiB * 1048576))" | |
# Virtual disks | |
utmVMInstanceDisk0ImageName="$(plutil -extract "Drive.0.ImageName" raw "${utmVMInstanceConfigPlistPath}")" | |
utmVMInstanceAuxiliaryStorageImageName="$(plutil -extract "System.MacPlatform.AuxiliaryStoragePath" raw "${utmVMInstanceConfigPlistPath}")" | |
# Check for the existence of multiple disks | |
plutil -extract "Drive.1.ImageName" raw "${utmVMInstanceConfigPlistPath}" > /dev/null 2>&1 | |
if [[ $? == 0 ]]; then | |
KarenWarning "Multiple virtual disks appear to be attached to the UTM virtual machine instance that you have selected!" | |
KarenWarning "Please note that only the ${COLOUR_BOLD}first${COLOUR_RESET} disk (${COLOUR_GREENBOLD}${utmVMInstanceDisk0ImageName}${COLOUR_RESET}) will be attached to and available for use in VirtualApple." | |
echo "" | |
fi | |
# Display mode | |
screenWidthPixels="$(plutil -extract "Display.0.WidthPixels" raw "${utmVMInstanceConfigPlistPath}")" | |
screenHeightPixels="$(plutil -extract "Display.0.HeightPixels" raw "${utmVMInstanceConfigPlistPath}")" | |
# UI scaling factor | |
screenScalePPI="$(plutil -extract "Display.0.PixelsPerInch" raw "${utmVMInstanceConfigPlistPath}")" | |
screenScaleFactor=1 | |
if [[ "${screenScalePPI}" -gt 80 ]]; then | |
# For some reason, UTM seems to hardcode @1x as 80 PPI, and @2x as… 226 PPI. What? | |
# We'll just treat everything above 80 as the user having HiDPI enabled in UTM prefs. | |
screenScaleFactor=2 | |
fi | |
# Convert display mode from pixels to points based on UI scaling factor | |
screenWidthPoints="$((screenWidthPixels / screenScaleFactor))" | |
screenHeightPoints="$((screenHeightPixels / screenScaleFactor))" | |
# Hardware model, machine identifier | |
hardwareModel="$(plutil -extract "System.MacPlatform.HardwareModel" raw "${utmVMInstanceConfigPlistPath}")" | |
machineIdentifier="$(plutil -extract "System.MacPlatform.MachineIdentifier" raw "${utmVMInstanceConfigPlistPath}")" | |
######## Print all collected info to stdout ######## | |
KarenLog "${COLOUR_BOLD}UTM virtual machine configuration version:${COLOUR_RESET} ${utmVMInstanceConfigurationVersion}" | |
KarenLog "${COLOUR_BOLD}Virtual machine instance name (from configuration):${COLOUR_RESET} ${utmVMInstanceNameFromConfigPlist}" | |
KarenLog "${COLOUR_BOLD}Virtual machine instance name (from filesystem path):${COLOUR_RESET} ${utmVMInstanceNameFromFilesystemPath}" | |
KarenLog "${COLOUR_BOLD}CPU core count:${COLOUR_RESET} ${cpuCount}" | |
KarenLog "${COLOUR_BOLD}Memory size:${COLOUR_RESET} ${memorySizeMiB} MiB (${memorySizeBytes} bytes)" | |
KarenLog "${COLOUR_BOLD}Virtual disk 0 filename:${COLOUR_RESET} ${utmVMInstanceDisk0ImageName}" | |
KarenLog "${COLOUR_BOLD}Virtual auxiliary storage filename:${COLOUR_RESET} ${utmVMInstanceAuxiliaryStorageImageName}" | |
KarenLog "${COLOUR_BOLD}Display mode (pixels):${COLOUR_RESET} ${screenWidthPixels}×${screenHeightPixels}@${screenScaleFactor}x" | |
KarenLog "${COLOUR_BOLD}Display mode (points):${COLOUR_RESET} ${screenWidthPoints}×${screenHeightPoints}@${screenScaleFactor}x" | |
KarenLog "${COLOUR_BOLD}Hardware model (base64-encoded bplist):${COLOUR_RESET} ${hardwareModel}" | |
KarenLog "${COLOUR_BOLD}Hardware model (decoded):${COLOUR_RESET} $(echo ${hardwareModel} | base64 -d | plutil -p -)" | |
KarenLog "${COLOUR_BOLD}Machine identifier (base64-encoded bplist):${COLOUR_RESET} ${machineIdentifier}" | |
KarenLog "${COLOUR_BOLD}Machine identifier (decoded):${COLOUR_RESET} $(echo ${machineIdentifier} | base64 -d | plutil -p -)" | |
echo "" | |
######## Generate configuration ######## | |
generatedVirtualAppleConfig=$(cat <<EOF | |
{ | |
"configuration": | |
{ | |
"bootIntoMacOSRecovery": true, | |
"bootIntoDFU": false, | |
"haltInIBoot1": false, | |
"haltInIBoot2": false, | |
"haltOnPanic": false, | |
"cpuCount": ${cpuCount}, | |
"memorySize": ${memorySizeBytes}, | |
"screenWidth": ${screenWidthPoints}, | |
"screenHeight": ${screenHeightPoints}, | |
"screenScale": ${screenScaleFactor} | |
}, | |
"installed": true, | |
"hardwareModel": "${hardwareModel}", | |
"machineIdentifier": "${machineIdentifier}" | |
} | |
EOF | |
) | |
KarenLog "${COLOUR_BOLD}Generated VirtualApple configuration:${COLOUR_RESET} ${generatedVirtualAppleConfig}" | |
echo "" | |
######## Create or update VirtualApple virtual machine instance and write configuration data to disk ######## | |
virtualAppleVMInstanceWithExtension="${utmVMInstanceNameFromFilesystemPath}.vmapple" | |
if [[ -d "${virtualAppleVMInstanceWithExtension}" ]]; then | |
KarenLog "There already appears to be a VirtualApple virtual machine with the same name in the current directory. (${COLOUR_GREENBOLD}${virtualAppleVMInstanceWithExtension}${COLOUR_RESET})" | |
KarenLog "Continuing beyond this point will overwrite its configuration data, virtual disk, and virtual auxiliary storage." | |
KarenLog "※ If this is already a hardlinked VirtualApple virtual machine instance and you're running this script to update the configuration, feel free to continue." | |
# TODO: Consider creating an empty file or something so we can automatically tell whether or not a given vmapple instance was generated by this tool | |
# That takes effort, though. And this is basically just a slightly cleaned up version of a minimum-effort script I originally wrote only for internal use anyway, so… ┐(🍍 ̄ー ̄)┌ | |
while true; do | |
read -p "Are you sure you want to continue? (enter y/n) " -n 1 -r input | |
if [[ "${input}" =~ ^[Yy]$ ]]; then | |
echo "" | |
KarenLog "Proceeding using the existing VirtualApple virtual machine instance in the current directory… (${COLOUR_GREENBOLD}${virtualAppleVMInstanceWithExtension}${COLOUR_RESET})" | |
break | |
elif [[ "${input}" =~ ^[Nn]$ ]]; then | |
echo "" | |
exit 0 | |
else | |
echo "" | |
KarenError "Invalid input received." | |
fi | |
done | |
else | |
KarenLog "Creating a new VirtualApple virtual machine instance in the current directory…" | |
mkdir -pv "${virtualAppleVMInstanceWithExtension}" | |
fi | |
KarenLog "Writing VirtualApple configuration data to disk…" | |
echo "${generatedVirtualAppleConfig}" > "${virtualAppleVMInstanceWithExtension}/metadata.json" | |
######## Create hardlinks ######## | |
KarenLog "Hardlinking virtual disk 0…" | |
ln -f "${utmVMInstancePath}/Data/${utmVMInstanceDisk0ImageName}" "${virtualAppleVMInstanceWithExtension}/disk.img" | |
KarenLog "Hardlinking virtual auxiliary storage…" | |
ln -f "${utmVMInstancePath}/Data/${utmVMInstanceAuxiliaryStorageImageName}" "${virtualAppleVMInstanceWithExtension}/aux.img" | |
echo "" | |
######## Print information ######## | |
KarenLog "${COLOUR_GREENBOLD}All operations complete!${COLOUR_RESET}" | |
echo "" | |
KarenLog "${COLOUR_BOLD}※ NOTE:${COLOUR_RESET} The newly-generated VirtualApple virtual machine will boot by default into the paired One True recoveryOS (1TR)." | |
KarenLog "If this is undesired behaviour for your use case, simply disable the option from the VirtualApple preferences." | |
echo "" | |
KarenWarning "${COLOUR_BOLD}※ IMPORTANT:${COLOUR_RESET} Both UTM and VirtualApple will read from and write to the same disks, as they are hardlinked together." | |
KarenWarning "As a result, please make sure to ${COLOUR_REDBOLD}never simultaneously run the same virtual machine in both UTM and VirtualApple!${COLOUR_RESET}" | |
KarenWarning "While there ${COLOUR_BOLD}are${COLOUR_RESET} safeguards against this (filesystem locks that only allow one virtual machine instance to access a given virtual disk at a time), in the event that said safeguards fail, data corruption will almost certainly occur." | |
exit | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment