Merge branch 'main' into sequoia-development

This commit is contained in:
Mykola Grymalyuk
2024-07-21 12:18:01 -06:00
committed by GitHub
11 changed files with 795 additions and 417 deletions

View File

@@ -1,20 +1,17 @@
"""
macos_installer_handler.py: Handler for macOS installers, both local and remote
macos_installer_handler.py: Handler for local macOS installers
"""
import enum
import logging
import plistlib
import tempfile
import subprocess
import applescript
from pathlib import Path
from ..datasets import os_data
from . import (
network_handler,
utilities,
subprocess_wrapper
)
@@ -22,26 +19,6 @@ from . import (
APPLICATION_SEARCH_PATH: str = "/Applications"
SFR_SOFTWARE_UPDATE_PATH: str = "SFR/com_apple_MobileAsset_SFRSoftwareUpdate/com_apple_MobileAsset_SFRSoftwareUpdate.xml"
CATALOG_URL_BASE: str = "https://swscan.apple.com/content/catalogs/others/index"
CATALOG_URL_EXTENSION: str = ".merged-1.sucatalog"
CATALOG_URL_VARIANTS: list = [
"15",
"14",
"13",
"12",
"10.16",
"10.15",
"10.14",
"10.13",
"10.12",
"10.11",
"10.10",
"10.9",
"mountainlion",
"lion",
"snowleopard",
"leopard",
]
tmp_dir = tempfile.TemporaryDirectory()
@@ -221,359 +198,6 @@ fi
return list_disks
class SeedType(enum.IntEnum):
"""
Enum for catalog types
Variants:
DeveloperSeed: Developer Beta (Part of the Apple Developer Program)
PublicSeed: Public Beta
CustomerSeed: AppleSeed Program (Generally mirrors DeveloperSeed)
PublicRelease: Public Release
"""
DeveloperSeed: int = 0
PublicSeed: int = 1
CustomerSeed: int = 2
PublicRelease: int = 3
class RemoteInstallerCatalog:
"""
Parses Apple's Software Update catalog and finds all macOS installers.
"""
def __init__(self, seed_override: SeedType = SeedType.PublicRelease, os_override: int = os_data.os_data.sequoia) -> None:
self.catalog_url: str = self._construct_catalog_url(seed_override, os_override)
self.available_apps: dict = self._parse_catalog()
self.available_apps_latest: dict = self._list_newest_installers_only()
def _construct_catalog_url(self, seed_type: SeedType, os_kernel: int) -> str:
"""
Constructs the catalog URL based on the seed type
Parameters:
seed_type (SeedType): The seed type to use
Returns:
str: The catalog URL
"""
url: str = CATALOG_URL_BASE
os_version: str = os_data.os_conversion.kernel_to_os(os_kernel)
os_version = "10.16" if os_version == "11" else os_version
if os_version not in CATALOG_URL_VARIANTS:
logging.error(f"OS version {os_version} is not supported, defaulting to latest")
os_version = CATALOG_URL_VARIANTS[0]
url += f"-{os_version}"
if seed_type == SeedType.DeveloperSeed:
url += f"seed"
elif seed_type == SeedType.PublicSeed:
url += f"beta"
elif seed_type == SeedType.CustomerSeed:
url += f"customerseed"
did_find_variant: bool = False
for variant in CATALOG_URL_VARIANTS:
if variant in url:
did_find_variant = True
if did_find_variant:
url += f"-{variant}"
url += f"{CATALOG_URL_EXTENSION}"
return url
def _fetch_catalog(self) -> dict:
"""
Fetches the catalog from Apple's servers
Returns:
dict: The catalog as a dictionary
"""
catalog: dict = {}
if network_handler.NetworkUtilities(self.catalog_url).verify_network_connection() is False:
return catalog
try:
catalog = plistlib.loads(network_handler.NetworkUtilities().get(self.catalog_url).content)
except plistlib.InvalidFileException:
return {}
return catalog
def _parse_catalog(self) -> dict:
"""
Parses the catalog and returns a dictionary of available installers
Returns:
dict: Dictionary of available installers
"""
available_apps: dict = {}
catalog: dict = self._fetch_catalog()
if not catalog:
return available_apps
if "Products" not in catalog:
return available_apps
for product in catalog["Products"]:
if "ExtendedMetaInfo" not in catalog["Products"][product]:
continue
if "Packages" not in catalog["Products"][product]:
continue
if "InstallAssistantPackageIdentifiers" not in catalog["Products"][product]["ExtendedMetaInfo"]:
continue
if "SharedSupport" not in catalog["Products"][product]["ExtendedMetaInfo"]["InstallAssistantPackageIdentifiers"]:
continue
for bm_package in catalog["Products"][product]["Packages"]:
if Path(bm_package["URL"]).name not in ["Info.plist", "com_apple_MobileAsset_MacSoftwareUpdate.plist"]:
continue
try:
build_plist = plistlib.loads(network_handler.NetworkUtilities().get(bm_package["URL"]).content)
except plistlib.InvalidFileException:
continue
result = {}
if Path(bm_package["URL"]).name == "com_apple_MobileAsset_MacSoftwareUpdate.plist":
result = self._parse_mobile_asset_plist(build_plist)
else:
result = self._legacy_parse_info_plist(build_plist)
if result == {}:
continue
download_link = None
integrity = None
size = None
date = catalog["Products"][product]["PostDate"]
for ia_package in catalog["Products"][product]["Packages"]:
if "InstallAssistant.pkg" not in ia_package["URL"]:
continue
if "URL" not in ia_package:
continue
if "IntegrityDataURL" not in ia_package:
continue
download_link = ia_package["URL"]
integrity = ia_package["IntegrityDataURL"]
size = ia_package["Size"] if ia_package["Size"] else 0
if any([download_link, size, integrity]) is None:
continue
available_apps.update({
product: {
"Version": result["Version"],
"Build": result["Build"],
"Link": download_link,
"Size": size,
"integrity": integrity,
"Source": "Apple Inc.",
"Variant": result["Variant"],
"OS": result["OS"],
"Models": result["Models"],
"Date": date
}
})
available_apps = {k: v for k, v in sorted(available_apps.items(), key=lambda x: x[1]['Version'])}
return available_apps
def _legacy_parse_info_plist(self, data: dict) -> dict:
"""
Legacy version of parsing for installer details through Info.plist
"""
if "MobileAssetProperties" not in data:
return {}
if "SupportedDeviceModels" not in data["MobileAssetProperties"]:
return {}
if "OSVersion" not in data["MobileAssetProperties"]:
return {}
if "Build" not in data["MobileAssetProperties"]:
return {}
# Ensure Apple Silicon specific Installers are not listed
if "VMM-x86_64" not in data["MobileAssetProperties"]["SupportedDeviceModels"]:
return {}
version = data["MobileAssetProperties"]["OSVersion"]
build = data["MobileAssetProperties"]["Build"]
catalog = ""
try:
catalog = data["MobileAssetProperties"]["BridgeVersionInfo"]["CatalogURL"]
except KeyError:
pass
if any([version, build]) is None:
return {}
return {
"Version": version,
"Build": build,
"Source": "Apple Inc.",
"Variant": self._catalog_to_variant(catalog),
"OS": os_data.os_conversion.os_to_kernel(version),
"Models": data["MobileAssetProperties"]["SupportedDeviceModels"],
}
def _parse_mobile_asset_plist(self, data: dict) -> dict:
"""
Parses the MobileAsset plist for installer details
With macOS Sequoia beta 1, the Info.plist was missing and as such this method was introduced
"""
for entry in data["Assets"]:
if "SupportedDeviceModels" not in entry:
continue
if "VMM-x86_64" not in entry["SupportedDeviceModels"]:
continue
if "OSVersion" not in entry:
continue
if "Build" not in entry:
continue
build = entry["Build"]
version = entry["OSVersion"]
catalog_url = ""
try:
catalog_url = entry["BridgeVersionInfo"]["CatalogURL"]
except KeyError:
pass
return {
"Version": version,
"Build": build,
"Source": "Apple Inc.",
"Variant": self._catalog_to_variant(catalog_url),
"OS": os_data.os_conversion.os_to_kernel(version),
"Models": entry["SupportedDeviceModels"],
}
return {}
def _catalog_to_variant(self, catalog: str) -> SeedType:
"""
Converts the Catalog URL to a SeedType
"""
if "beta" in catalog:
return SeedType.PublicSeed
elif "customerseed" in catalog:
return SeedType.CustomerSeed
elif "seed" in catalog:
return SeedType.DeveloperSeed
return SeedType.PublicRelease
def _list_newest_installers_only(self) -> dict:
"""
Returns a dictionary of the newest macOS installers only.
Primarily used to avoid overwhelming the user with a list of
installers that are not the newest version.
Returns:
dict: A dictionary of the newest macOS installers only.
"""
if self.available_apps is None:
return {}
newest_apps: dict = self.available_apps.copy()
supported_versions = ["10.13", "10.14", "10.15", "11", "12", "13", "14"]
for version in supported_versions:
remote_version_minor = 0
remote_version_security = 0
os_builds = []
# First determine the largest version
for ia in newest_apps:
if newest_apps[ia]["Version"].startswith(version):
if newest_apps[ia]["Variant"] not in [SeedType.CustomerSeed, SeedType.DeveloperSeed, SeedType.PublicSeed]:
remote_version = newest_apps[ia]["Version"].split(".")
if remote_version[0] == "10":
remote_version.pop(0)
remote_version.pop(0)
else:
remote_version.pop(0)
if int(remote_version[0]) > remote_version_minor:
remote_version_minor = int(remote_version[0])
remote_version_security = 0 # Reset as new minor version found
if len(remote_version) > 1:
if int(remote_version[1]) > remote_version_security:
remote_version_security = int(remote_version[1])
# Now remove all versions that are not the largest
for ia in list(newest_apps):
# Don't use Beta builds to determine latest version
if newest_apps[ia]["Variant"] in [SeedType.CustomerSeed, SeedType.DeveloperSeed, SeedType.PublicSeed]:
continue
if newest_apps[ia]["Version"].startswith(version):
remote_version = newest_apps[ia]["Version"].split(".")
if remote_version[0] == "10":
remote_version.pop(0)
remote_version.pop(0)
else:
remote_version.pop(0)
if int(remote_version[0]) < remote_version_minor:
newest_apps.pop(ia)
continue
if int(remote_version[0]) == remote_version_minor:
if len(remote_version) > 1:
if int(remote_version[1]) < remote_version_security:
newest_apps.pop(ia)
continue
else:
if remote_version_security > 0:
newest_apps.pop(ia)
continue
# Remove duplicate builds
# ex. macOS 12.5.1 has 2 builds in the Software Update Catalog
# ref: https://twitter.com/classicii_mrmac/status/1560357471654379522
if newest_apps[ia]["Build"] in os_builds:
newest_apps.pop(ia)
continue
os_builds.append(newest_apps[ia]["Build"])
# Remove Betas if there's a non-beta version available
for ia in list(newest_apps):
if newest_apps[ia]["Variant"] in [SeedType.CustomerSeed, SeedType.DeveloperSeed, SeedType.PublicSeed]:
for ia2 in newest_apps:
if newest_apps[ia2]["Version"].split(".")[0] == newest_apps[ia]["Version"].split(".")[0] and newest_apps[ia2]["Variant"] not in [SeedType.CustomerSeed, SeedType.DeveloperSeed, SeedType.PublicSeed]:
newest_apps.pop(ia)
break
# Remove EOL versions (n-3)
for ia in list(newest_apps):
if newest_apps[ia]["Version"].split('.')[0] < supported_versions[-3]:
newest_apps.pop(ia)
return newest_apps
class LocalInstallerCatalog:
"""
Finds all macOS installers on the local machine.