Skip to content

Instantly share code, notes, and snippets.

@dariaphoebe
Forked from akemin-dayo/virtualapple-utm-link
Created April 24, 2024 18:34
Show Gist options
  • Select an option

  • Save dariaphoebe/07e5fa889bb252f19493697dece0bc80 to your computer and use it in GitHub Desktop.

Select an option

Save dariaphoebe/07e5fa889bb252f19493697dece0bc80 to your computer and use it in GitHub Desktop.

Revisions

  1. @akemin-dayo akemin-dayo revised this gist Jan 18, 2023. 1 changed file with 10 additions and 8 deletions.
    18 changes: 10 additions & 8 deletions virtualapple-utm-link
    Original file line number Diff line number Diff line change
    @@ -12,9 +12,6 @@

    # 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"
    @@ -137,8 +134,8 @@ print_usage() {
    fi

    # Display mode
    screenWidth="$(plutil -extract "Display.0.WidthPixels" raw "${utmVMInstanceConfigPlistPath}")"
    screenHeight="$(plutil -extract "Display.0.HeightPixels" raw "${utmVMInstanceConfigPlistPath}")"
    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}")"
    @@ -149,6 +146,10 @@ print_usage() {
    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}")"
    @@ -164,7 +165,8 @@ print_usage() {
    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}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 -)"
    @@ -185,8 +187,8 @@ print_usage() {
    "haltOnPanic": false,
    "cpuCount": ${cpuCount},
    "memorySize": ${memorySizeBytes},
    "screenWidth": ${screenWidth},
    "screenHeight": ${screenHeight},
    "screenWidth": ${screenWidthPoints},
    "screenHeight": ${screenHeightPoints},
    "screenScale": ${screenScaleFactor}
    },
    "installed": true,
  2. @akemin-dayo akemin-dayo created this gist Jan 2, 2023.
    254 changes: 254 additions & 0 deletions virtualapple-utm-link
    Original file line number Diff line number Diff line change
    @@ -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
    }