Skip to content

Instantly share code, notes, and snippets.

@PicoMitchell
Last active May 22, 2024 02:49
Show Gist options
  • Save PicoMitchell/877b645b113c9a5db95248ed1d496243 to your computer and use it in GitHub Desktop.
Save PicoMitchell/877b645b113c9a5db95248ed1d496243 to your computer and use it in GitHub Desktop.
Scripts to get compatible macOS versions from Apple Software Lookup Service (gdmf.apple.com/v2/pmv) or Software Update Catalog (swscan.apple.com[...]sucatalog).
#!/bin/bash
#
# Created by Pico Mitchell (of Random Applications) on 1/5/23
#
# https://gist.github.com/PicoMitchell/877b645b113c9a5db95248ed1d496243#file-get_compatible_macos_versions-asls-sh
#
# MIT License
#
# Copyright (c) 2023 Pico Mitchell (Random Applications)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
readonly SCRIPT_VERSION='2023.4.16-1'
PATH='/usr/bin:/bin:/usr/sbin:/sbin'
current_macos_version="$(sw_vers -productVersion)"
echo "Running \"$(basename "${BASH_SOURCE[0]}")\" version ${SCRIPT_VERSION} on macOS ${current_macos_version} $(sw_vers -buildVersion)..."
mac_board_id=''
mac_device_id=''
mac_is_apple_silicon="$([[ "$(sysctl -in hw.optional.arm64)" == '1' ]] && echo 'true' || echo 'false')"
if [[ " $(sysctl -in machdep.cpu.features) " == *' VMM '* || "$(sysctl -in kern.hv_vmm_present)" == '1' ]]; then
# "machdep.cpu.features" is always EMPTY on Apple Silicon (whether or not it's a VM) so it cannot be used to check for the "VMM" feature flag when the system is a VM,
# but I examined the full "sysctl -a" output when running a VM on Apple Silicon and found that "kern.hv_vmm_present" is set to "1" when running a VM and "0" when not.
# Through testing, I found the "kern.hv_vmm_present" key is also the present on Intel Macs starting with macOS 11 Big Sur and gets properly set to "1"
# on Intel VMs, but still check for either since "kern.hv_vmm_present" is not available on every version of macOS that this script may be run on.
mac_device_id="$($mac_is_apple_silicon && echo 'VMA2MACOSAP' || echo 'VMM-x86_64')" # These are the Device IDs listed in the "Apple Software Lookup Service" JSON for Apple Silicon and Intel VMs.
else
if ! $mac_is_apple_silicon; then # Only Intel Macs have a Board ID. T2 Macs (which are Intel) will have both a Board ID and Device ID while Apple Silicon Macs only have a Device ID.
mac_board_id="$(/usr/libexec/PlistBuddy -c 'Print 0:board-id' /dev/stdin <<< "$(ioreg -arc IOPlatformExpertDevice -k board-id -d 1)" 2> /dev/null | tr -d '[:cntrl:]')" # Remove control characters because this decoded value could end with a NUL char.
if [[ "${mac_board_id}" != 'Mac-'* ]]; then
>&2 echo 'ERROR: Failed to retrieve Board ID for Intel Mac.'
exit 2
fi
fi
if [[ -n "$(ioreg -rc AppleSEPManager)" ]]; then # The Device ID only exists for T2 and Apple Silicon Macs, both of which have a Secure Enclave (SEP).
if $mac_is_apple_silicon; then # For Apple Silicon Macs, the Device ID is the first element of the "compatible" array in "ioreg -rc IOPlatformExpertDevice -d 1"
# Annoyingly, the "compatible" array is easier to get elements out of from the plain text output rather than the plist output since the value is not actually a proper plist array.
# NOTE: This "compatible" array will also exist on Intel Macs, but it will only contain the Model ID which was already retrieved above.
mac_device_id="$(ioreg -rc IOPlatformExpertDevice -k compatible -d 1 | awk -F '"' '($2 == "compatible") { print $4; exit }')"
else
mac_device_id="$(/usr/libexec/remotectl get-property localbridge HWModel 2> /dev/null)" # For T2 Macs, this is the T2 chip Device ID.
fi
if [[ "${mac_device_id}" != *'AP' ]]; then
>&2 echo 'ERROR: Failed to retrieve Device ID for T2 or Apple Silicon Mac.'
exit 3
fi
fi
fi
asls_url='https://gdmf.apple.com/v2/pmv'
# About "Apple Software Lookup Service" (from https://support.apple.com/guide/deployment/use-mdm-to-deploy-software-updates-depafd2fad80/1/web/1.0#dep0b094c8d3 & https://developer.apple.com/business/documentation/MDM-Protocol-Reference.pdf):
# Use the service at https://gdmf.apple.com/v2/pmv to obtain a list of available updates.
# The JSON response contains two lists of available software releases. The "AssetSets" list contains all the releases available for MDMs to push to their supervised devices.
# The other list, "PublicAssetSets" contains the latest releases available to the general public (non-supervised devices) if they try to upgrade. The "PublicAssetSets" is a subset of the "AssetSets" list.
# Each element in the list contains the product version number of the OS, the posting date, the expiration date, and a list of supported devices for that release.
if ! asls_json="$(curl -m 5 -sfL "${asls_url}")" || [[ "${asls_json}" != *'"PublicAssetSets"'* ]]; then
>&2 echo 'ERROR: Failed to download Apple Software Lookup Service JSON.'
exit 4
fi
compatible_supported_macos_versions="$(osascript -l JavaScript -e '
"use strict"
function run(argv) {
const macBoardID = argv[0], macDeviceID = argv[1], supportedVersion = []
JSON.parse(argv[2]).PublicAssetSets.macOS.forEach(thisVersionDict => {
const thisVersionSupportedDevices = thisVersionDict.SupportedDevices
if ((macBoardID && thisVersionSupportedDevices.includes(macBoardID)) || (macDeviceID && thisVersionSupportedDevices.includes(macDeviceID)))
supportedVersion.push(thisVersionDict.ProductVersion)
})
return supportedVersion.sort((thisVersion, thatVersion) => ObjC.wrap(thatVersion).compareOptions(thisVersion, $.NSNumericSearch)).join("\n")
}
' -- "${mac_board_id}" "${mac_device_id}" "${asls_json}" 2> /dev/null)"
echo -e "\nLatest Compatible macOS Version: $(echo "${compatible_supported_macos_versions}" | head -1)"
current_macos_major_version="${current_macos_version%%.*}"
echo -e "\nLatest Version of macOS ${current_macos_major_version} (Running Version): $(echo "${compatible_supported_macos_versions}" | grep -m 1 "^${current_macos_major_version}")"
echo -e "\nAll Compatible Supported macOS Versions:\n${compatible_supported_macos_versions}"
#!/bin/bash
#
# Created by Pico Mitchell (of Random Applications) on 4/5/22
#
# https://gist.github.com/PicoMitchell/877b645b113c9a5db95248ed1d496243#file-get_compatible_macos_versions-sucatalog-sh
#
# MIT License
#
# Copyright (c) 2022 Pico Mitchell (Random Applications)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
readonly SCRIPT_VERSION='2023.4.16-1'
PATH='/usr/bin:/bin:/usr/sbin:/sbin'
current_macos_version="$(sw_vers -productVersion)"
echo "Running \"$(basename "${BASH_SOURCE[0]}")\" version ${SCRIPT_VERSION} on macOS ${current_macos_version} $(sw_vers -buildVersion)..."
mac_model_id="$(sysctl -n hw.model)"
mac_board_id=''
mac_device_id=''
mac_is_virtual_machine="$([[ " $(sysctl -in machdep.cpu.features) " == *' VMM '* || "$(sysctl -in kern.hv_vmm_present)" == '1' ]] && echo 'true' || echo 'false')"
# "machdep.cpu.features" is always EMPTY on Apple Silicon (whether or not it's a VM) so it cannot be used to check for the "VMM" feature flag when the system is a VM,
# but I examined the full "sysctl -a" output when running a VM on Apple Silicon and found that "kern.hv_vmm_present" is set to "1" when running a VM and "0" when not.
# Through testing, I found the "kern.hv_vmm_present" key is also the present on Intel Macs starting with macOS 11 Big Sur and gets properly set to "1"
# on Intel VMs, but still check for either since "kern.hv_vmm_present" is not available on every version of macOS that this script may be run on.
if ! $mac_is_virtual_machine; then # VMs will just show all versions of macOS as compatible.
if [[ "${mac_model_id}" != *'Mac'* ]]; then
>&2 echo 'ERROR: Failed to retrieve Model ID.'
exit 1
fi
mac_is_apple_silicon="$([[ "$(sysctl -in hw.optional.arm64)" == '1' ]] && echo 'true' || echo 'false')"
if ! $mac_is_apple_silicon; then # Only Intel Macs have a Board ID. T2 Macs (which are Intel) will have both a Board ID and Device ID while Apple Silicon Macs only have a Device ID.
mac_board_id="$(/usr/libexec/PlistBuddy -c 'Print 0:board-id' /dev/stdin <<< "$(ioreg -arc IOPlatformExpertDevice -k board-id -d 1)" 2> /dev/null | tr -d '[:cntrl:]')" # Remove control characters because this decoded value could end with a NUL char.
if [[ "${mac_board_id}" != 'Mac-'* ]]; then
>&2 echo 'ERROR: Failed to retrieve Board ID for Intel Mac.'
exit 2
fi
fi
if [[ -n "$(ioreg -rc AppleSEPManager)" ]]; then # The Device ID only exists for T2 and Apple Silicon Macs, both of which have a Secure Enclave (SEP).
if $mac_is_apple_silicon; then # For Apple Silicon Macs, the Device ID is the first element of the "compatible" array in "ioreg -rc IOPlatformExpertDevice -d 1"
# Annoyingly, the "compatible" array is easier to get elements out of from the plain text output rather than the plist output since the value is not actually a proper plist array.
# NOTE: This "compatible" array will also exist on Intel Macs, but it will only contain the Model ID which was already retrieved above.
mac_device_id="$(ioreg -rc IOPlatformExpertDevice -k compatible -d 1 | awk -F '"' '($2 == "compatible") { print $4; exit }')"
else
mac_device_id="$(/usr/libexec/remotectl get-property localbridge HWModel 2> /dev/null)" # For T2 Macs, this is the T2 chip Device ID.
fi
if [[ "${mac_device_id}" != *'AP' ]]; then
>&2 echo 'ERROR: Failed to retrieve Device ID for T2 or Apple Silicon Mac.'
exit 3
fi
fi
fi
sucatalog_url='https://swscan.apple.com/content/catalogs/others/index-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz'
if ! sucatalog_plist="$(curl -m 10 -sfL --compressed "${sucatalog_url}")" || [[ "${sucatalog_plist}" != *'<plist'* ]]; then
>&2 echo 'ERROR: Failed to download Software Update Catalog.'
exit 4
fi
# Use Objective-C bridge of JavaScript for Automation (JXA) to get the English ".dist" file URLs since it requires semi-complex
# plist traversal which is much simpler using a JavaScript dictionary rather than multiple "PlistBuddy" commands in the shell.
IFS=$'\n' read -rd '' -a english_dist_urls < <(printf '%s\n' "${sucatalog_plist}" | osascript -l JavaScript -e '
"use strict"
const stdinFileHandle = $.NSFileHandle.fileHandleWithStandardInput
const sucatalogPlist = $.NSString.alloc.initWithDataEncoding((stdinFileHandle.respondsToSelector("readDataToEndOfFileAndReturnError:") ? stdinFileHandle.readDataToEndOfFileAndReturnError(ObjC.wrap()) : stdinFileHandle.readDataToEndOfFile), $.NSUTF8StringEncoding)
const englishDistURLs = []
Object.values(ObjC.deepUnwrap($.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(ObjC.wrap(sucatalogPlist).dataUsingEncoding($.NSUTF8StringEncoding), $.NSPropertyListImmutable, null, ObjC.wrap())).Products).forEach(thisProductDict => {
const thisProductExtendedMetaInfo = thisProductDict.ExtendedMetaInfo
if (thisProductExtendedMetaInfo && thisProductExtendedMetaInfo.InstallAssistantPackageIdentifiers)
englishDistURLs.push(thisProductDict.Distributions.English)
})
englishDistURLs.join("\n")
' 2> /dev/null)
if (( ${#english_dist_urls[@]} == 0 )) || [[ "${english_dist_urls[0]}" != 'https://'* ]]; then
>&2 echo 'ERROR: Failed to retrieve Distribution XML URLs.'
exit 5
fi
compatible_versions_output=''
for this_dist_url in "${english_dist_urls[@]}"; do
this_dist_product_id="${this_dist_url##*/}"
this_dist_product_id="${this_dist_product_id%%.*}"
if ! this_dist_xml="$(curl -m 5 -sfL "${this_dist_url}")" || [[ "${this_dist_xml}" != *'<?xml'* ]]; then
>&2 echo "ERROR: Failed to download Distribution XML for Product ID \"${this_dist_product_id}\"."
exit 6
fi
# The ".dist" files have some plist-like sections, but are actually Distribution XML files so must use "xmllint" instead of "PlistBuddy".
# https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Introduction.html
this_dist_build="$(printf '%s\n' "${this_dist_xml}" | xmllint --xpath '//key[text()="BUILD"]/following-sibling::string[1]/text()' - 2> /dev/null)"
this_dist_version="$(printf '%s\n' "${this_dist_xml}" | xmllint --xpath '//key[text()="VERSION"]/following-sibling::string[1]/text()' - 2> /dev/null)"
if [[ -z "${this_dist_build}" || -z "${this_dist_version}" ]]; then
>&2 echo "ERROR: Failed to retrieve macOS version or build for Product ID \"${this_dist_product_id}\"."
exit 7
elif [[ "${this_dist_build}" != *[[:lower:]] ]]; then # Ignore beta builds which end in lowercase letters.
# The section of the Distribution XML containing the supported IDs is Installer JS (JavaScript) code (https://developer.apple.com/documentation/installer_js),
# so just "grep" the JavaScript variable assignments to be able to check for the single quoted IDs within the JavaScript arrays.
this_dist_supported_board_ids=''
this_dist_supported_device_ids=''
this_dist_non_supported_model_ids=''
this_dist_is_compatible=false
if $mac_is_virtual_machine; then
this_dist_is_compatible=true
elif this_dist_supported_board_ids="$(printf '%s\n' "${this_dist_xml}" | grep -F 'var supportedBoardIDs = [' 2> /dev/null)"; then
if this_dist_supported_device_ids="$(printf '%s\n' "${this_dist_xml}" | grep -F 'var supportedDeviceIDs = [' 2> /dev/null)"; then
# For macOS 11 Big Sur and newer, the ".dist" files contain JavaScript arrays of supported Board IDs and Device IDs
if [[ "${this_dist_supported_board_ids}" == *"'${mac_board_id}'"* || "${this_dist_supported_device_ids}" == *"'${mac_device_id}'"* ]]; then # NOTICE: Checking for single quoted IDs like they are in the JavaScript arrays.
this_dist_is_compatible=true
fi
else
>&2 echo "ERROR: Failed to retrieve supportedDeviceIDs for Product ID \"${this_dist_product_id}\"."
exit 8
fi
elif this_dist_supported_board_ids="$(printf '%s\n' "${this_dist_xml}" | grep -F 'var boardIds = [' 2> /dev/null)"; then
if this_dist_non_supported_model_ids="$(printf '%s\n' "${this_dist_xml}" | grep -F 'var nonSupportedModels = [' 2> /dev/null)"; then
# For macOS 10.15 Catalina and older, the ".dist" files contain JavaScript arrays of supported Board IDs and NON-supported Model IDs.
if [[ "${this_dist_supported_board_ids}" == *"'${mac_board_id}'"* && "${this_dist_non_supported_model_ids}" != *"'${mac_model_id}'"* ]]; then # NOTICE: Checking for single quoted IDs like they are in the JavaScript arrays.
this_dist_is_compatible=true
fi
else
>&2 echo "ERROR: Failed to retrieve nonSupportedModels for Product ID \"${this_dist_product_id}\"."
exit 9
fi
else
>&2 echo "ERROR: Failed to retrieve supportedBoardIDs or boardIds for Product ID \"${this_dist_product_id}\"."
exit 10
fi
if $this_dist_is_compatible; then
compatible_versions_output+=$'\n'"${this_dist_version}"$'\t'"${this_dist_build}"$'\t'"${this_dist_product_id}"
fi
fi
done
if [[ -z "${compatible_versions_output}" ]]; then
>&2 echo "ERROR: Failed to detect any compatible macOS versions for this Mac."
exit 11
fi
sorted_compatible_versions_output="$(echo "${compatible_versions_output}" | sort -rV)"
echo -e "\nLatest Compatible macOS Version: $(echo "${sorted_compatible_versions_output}" | awk '{ print $1; exit }')"
current_macos_major_version="${current_macos_version%%.*}"
if (( current_macos_major_version == 10 )); then
current_macos_major_version="$(echo "${current_macos_version}" | cut -d '.' -f '1,2')"
fi
echo -e "\nLatest Version of macOS ${current_macos_major_version} (Running Version): $(echo "${sorted_compatible_versions_output}" | AWK_ENV_CURRENT_MACOS_MAJOR_VERSION_REGEX="^${current_macos_major_version//./\\.}" awk '($1 ~ ENVIRON["AWK_ENV_CURRENT_MACOS_MAJOR_VERSION_REGEX"]) { print $1; exit }')"
echo -e "\nAll Compatible macOS Versions:\nVersion\tBuild\tProduct ID\n${sorted_compatible_versions_output}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment