|
|
@@ -0,0 +1,254 @@ |
|
|
#!/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. |
|
|
|
|
|
# For the time being, you should use my fork of VirtualApple here to fix a few issues until my PR (saagarjha/VirtualApple/pull/6) is merged upstream. |
|
|
# https://github.com/akemin-dayo/VirtualApple |
|
|
|
|
|
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 |
|
|
screenWidth="$(plutil -extract "Display.0.WidthPixels" raw "${utmVMInstanceConfigPlistPath}")" |
|
|
screenHeight="$(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 |
|
|
|
|
|
# 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:${COLOUR_RESET} ${screenWidth}×${screenHeight}@${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": ${screenWidth}, |
|
|
"screenHeight": ${screenHeight}, |
|
|
"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 |
|
|
} |