Last active
May 22, 2024 02:49
-
-
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).
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
#!/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}" |
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
#!/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