mirror of
https://github.com/dortania/OpenCore-Legacy-Patcher.git
synced 2026-04-11 16:27:19 +10:00
Merge branch 'main' into sequoia-development
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
Copyright (c) 2020-2024, Dhinak G
|
||||
Copyright (c) 2020-2024, Mykola Grymalyuk
|
||||
Copyright (c) 2020-2024 Dhinak G, Mykola Grymalyuk, and individual contributors.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ class GenerateDiskImages:
|
||||
'-format', 'UDZO', '-ov',
|
||||
'-volname', 'OpenCore Patcher Resources (Base)',
|
||||
'-fs', 'HFS+',
|
||||
'-layout', 'NONE',
|
||||
'-srcfolder', './payloads',
|
||||
'-passphrase', 'password', '-encryption'
|
||||
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
@@ -50,8 +50,8 @@ The patcher is designed to target **macOS Big Sur 11.x to macOS Sonoma 14.x**.
|
||||
| MacBook Air (11-inch, Early 2015) | `MacBookAir7,1` | ^^ |
|
||||
| MacBook Air (13-inch, Early 2015)<br>MacBook Air (13-inch, 2017) | `MacBookAir7,2` | ^^ |
|
||||
| MacBook Air (Retina, 13-inch, 2018) | `MacBookAir8,1` | - Supported by Apple |
|
||||
| MacBook Air (Retina, 13-inch, 2019) | `MacBookAir9,1` | ^^ |
|
||||
| MacBook Air (Retina, 13-inch, 2020) | `MacBookAir10,1` | ^^ |
|
||||
| MacBook Air (Retina, 13-inch, 2019) | `MacBookAir8,2` | ^^ |
|
||||
| MacBook Air (Retina, 13-inch, 2020) | `MacBookAir9,1` | ^^ |
|
||||
|
||||
### MacBook Pro
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ In some cases, a following error saying "The bless of the installer disk failed"
|
||||
</div>
|
||||
|
||||
|
||||
To resolve this, you may try adding Full Disk Access permission OpenCore Legacy Patcher. To add it, first go to the settings
|
||||
To resolve this, you may try adding Full Disk Access permission for OpenCore Legacy Patcher. To add it, first go to the settings
|
||||
|
||||
* Ventura and Sonoma: Go to System Settings -> Privacy and Security -> Full Disk Access
|
||||
|
||||
|
||||
111
opencore_legacy_patcher/sucatalog/__init__.py
Normal file
111
opencore_legacy_patcher/sucatalog/__init__.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
sucatalog: Python module for querying Apple's Software Update Catalog, supporting Tiger through Sequoia.
|
||||
|
||||
-------------------
|
||||
|
||||
## Usage
|
||||
|
||||
### Get Software Update Catalog URL
|
||||
|
||||
```python
|
||||
>>> import sucatalog
|
||||
|
||||
>>> # Defaults to PublicRelease seed
|
||||
>>> url = sucatalog.CatalogURL().url
|
||||
"https://swscan.apple.com/.../index-15-14-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"
|
||||
|
||||
>>> url = sucatalog.CatalogURL(seed=sucatalog.SeedType.DeveloperSeed).url
|
||||
"https://swscan.apple.com/.../index-15seed-15-14-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"
|
||||
|
||||
>>> url = sucatalog.CatalogURL(version=sucatalog.CatalogVersion.HIGH_SIERRA).url
|
||||
"https://swscan.apple.com/.../index-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog"
|
||||
```
|
||||
|
||||
|
||||
### Parse Software Update Catalog - InstallAssistants only
|
||||
|
||||
>>> import sucatalog
|
||||
|
||||
>>> # Pass contents of URL (as dictionary)
|
||||
>>> catalog = plistlib.loads(requests.get(url).content)
|
||||
|
||||
>>> products = sucatalog.CatalogProducts(catalog).products
|
||||
[
|
||||
{
|
||||
'Build': '22G720',
|
||||
'Catalog': <SeedType.PublicRelease: ''>,
|
||||
'InstallAssistant': {
|
||||
'IntegrityDataSize': 42008,
|
||||
'IntegrityDataURL': 'https://swcdn.apple.com/.../InstallAssistant.pkg.integrityDataV1',
|
||||
'Size': 12210304673,
|
||||
'URL': 'https://swcdn.apple.com/.../InstallAssistant.pkg'
|
||||
},
|
||||
'PostDate': datetime.datetime(2024, 5, 20, 17, 18, 21),
|
||||
'ProductID': '052-96247',
|
||||
'Title': 'macOS Ventura',
|
||||
'Version': '13.6.7'
|
||||
}
|
||||
]
|
||||
|
||||
### Parse Software Update Catalog - All products
|
||||
|
||||
By default, `CatalogProducts` will only return InstallAssistants. To get all products, set `install_assistants_only=False`.
|
||||
|
||||
>>> import sucatalog
|
||||
|
||||
>>> # Pass contents of URL (as dictionary)
|
||||
>>> products = sucatalog.CatalogProducts(catalog, install_assistants_only=False).products
|
||||
[
|
||||
{
|
||||
'Build': None,
|
||||
'Catalog': None,
|
||||
'Packages': [
|
||||
{
|
||||
'MetadataURL': 'https://swdist.apple.com/.../iLifeSlideshow_v2.pkm',
|
||||
'Size': 116656956,
|
||||
'URL': 'http://swcdn.apple.com/.../iLifeSlideshow_v2.pkg'
|
||||
},
|
||||
{
|
||||
'MetadataURL': 'https://swdist.apple.com/.../iPhoto9.2.3ContentUpdate.pkm',
|
||||
'Size': 59623907,
|
||||
'URL': 'http://swcdn.apple.com/.../iPhoto9.2.3ContentUpdate.pkg'
|
||||
},
|
||||
{
|
||||
'MetadataURL': 'https://swdist.apple.com/.../iPhoto9.2.3Update.pkm',
|
||||
'Size': 197263405,
|
||||
'URL': 'http://swcdn.apple.com/.../iPhoto9.2.3Update.pkg'
|
||||
}
|
||||
],
|
||||
'PostDate': datetime.datetime(2019, 10, 23, 0, 2, 42),
|
||||
'ProductID': '041-85230',
|
||||
'Title': 'iPhoto Update',
|
||||
'Version': '9.2.3'
|
||||
},
|
||||
{
|
||||
'Build': None,
|
||||
'Catalog': None,
|
||||
'Packages': [
|
||||
{
|
||||
'Digest': '9aba109078feec7ea841529e955440b63d7755a0',
|
||||
'MetadataURL': 'https://swdist.apple.com/.../iPhoto9.4.3Update.pkm',
|
||||
'Size': 555246460,
|
||||
'URL': 'http://swcdn.apple.com/.../iPhoto9.4.3Update.pkg'
|
||||
},
|
||||
{
|
||||
'Digest': '0bb013221ca2df5e178d950cb229f41b8e680d00',
|
||||
'MetadataURL': 'https://swdist.apple.com/.../iPhoto9.4.3ContentUpdate.pkm',
|
||||
'Size': 213073666,
|
||||
'URL': 'http://swcdn.apple.com/.../iPhoto9.4.3ContentUpdate.pkg'
|
||||
}
|
||||
],
|
||||
'PostDate': datetime.datetime(2019, 10, 13, 3, 23, 14),
|
||||
'ProductID': '041-88859',
|
||||
'Title': 'iPhoto Update',
|
||||
'Version': '9.4.3'
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
from .url import CatalogURL
|
||||
from .constants import CatalogVersion, SeedType
|
||||
from .products import CatalogProducts
|
||||
57
opencore_legacy_patcher/sucatalog/constants.py
Normal file
57
opencore_legacy_patcher/sucatalog/constants.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
constants.py: Enumerations for sucatalog-py
|
||||
"""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class SeedType(StrEnum):
|
||||
"""
|
||||
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: str = "seed"
|
||||
PublicSeed: str = "beta"
|
||||
CustomerSeed: str = "customerseed"
|
||||
PublicRelease: str = ""
|
||||
|
||||
|
||||
class CatalogVersion(StrEnum):
|
||||
"""
|
||||
Enum for macOS versions
|
||||
|
||||
Used for generating sucatalog URLs
|
||||
"""
|
||||
SEQUOIA: str = "15"
|
||||
SONOMA: str = "14"
|
||||
VENTURA: str = "13"
|
||||
MONTEREY: str = "12"
|
||||
BIG_SUR: str = "11"
|
||||
BIG_SUR_LEGACY: str = "10.16"
|
||||
CATALINA: str = "10.15"
|
||||
MOJAVE: str = "10.14"
|
||||
HIGH_SIERRA: str = "10.13"
|
||||
SIERRA: str = "10.12"
|
||||
EL_CAPITAN: str = "10.11"
|
||||
YOSEMITE: str = "10.10"
|
||||
MAVERICKS: str = "10.9"
|
||||
MOUNTAIN_LION: str = "mountainlion"
|
||||
LION: str = "lion"
|
||||
SNOW_LEOPARD: str = "snowleopard"
|
||||
LEOPARD: str = "leopard"
|
||||
TIGER: str = ""
|
||||
|
||||
|
||||
class CatalogExtension(StrEnum):
|
||||
"""
|
||||
Enum for catalog extensions
|
||||
|
||||
Used for generating sucatalog URLs
|
||||
"""
|
||||
PLIST: str = ".sucatalog"
|
||||
GZIP: str = ".sucatalog.gz"
|
||||
402
opencore_legacy_patcher/sucatalog/products.py
Normal file
402
opencore_legacy_patcher/sucatalog/products.py
Normal file
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
products.py: Parse products from Software Update Catalog
|
||||
"""
|
||||
|
||||
import re
|
||||
import plistlib
|
||||
|
||||
import packaging.version
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from pathlib import Path
|
||||
from functools import cached_property
|
||||
|
||||
from .url import CatalogURL
|
||||
from .constants import CatalogVersion, SeedType
|
||||
|
||||
from ..support import network_handler
|
||||
|
||||
|
||||
class CatalogProducts:
|
||||
"""
|
||||
Args:
|
||||
catalog (dict): Software Update Catalog (contents of CatalogURL's URL)
|
||||
install_assistants_only (bool): Only list InstallAssistant products
|
||||
only_vmm_install_assistants (bool): Only list VMM-x86_64-compatible InstallAssistant products
|
||||
max_install_assistant_version (CatalogVersion): Maximum InstallAssistant version to list
|
||||
"""
|
||||
def __init__(self,
|
||||
catalog: dict,
|
||||
install_assistants_only: bool = True,
|
||||
only_vmm_install_assistants: bool = True,
|
||||
max_install_assistant_version: CatalogVersion = CatalogVersion.SONOMA
|
||||
) -> None:
|
||||
self.catalog: dict = catalog
|
||||
self.ia_only: bool = install_assistants_only
|
||||
self.vmm_only: bool = only_vmm_install_assistants
|
||||
self.max_ia_version: packaging = packaging.version.parse(f"{max_install_assistant_version.value}.99.99")
|
||||
self.max_ia_catalog: CatalogVersion = max_install_assistant_version
|
||||
|
||||
|
||||
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"]:
|
||||
if self.vmm_only:
|
||||
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,
|
||||
"Catalog": CatalogURL().catalog_url_to_seed(catalog),
|
||||
}
|
||||
|
||||
|
||||
def _parse_mobile_asset_plist(self, data: dict) -> dict:
|
||||
"""
|
||||
Parses the MobileAsset plist for installer details
|
||||
|
||||
With macOS Sequoia, the Info.plist is no longer present in the InstallAssistant's assets
|
||||
"""
|
||||
|
||||
for entry in data["Assets"]:
|
||||
if "SupportedDeviceModels" not in entry:
|
||||
continue
|
||||
if "OSVersion" not in entry:
|
||||
continue
|
||||
if "Build" not in entry:
|
||||
continue
|
||||
if "VMM-x86_64" not in entry["SupportedDeviceModels"]:
|
||||
if self.vmm_only:
|
||||
continue
|
||||
|
||||
build = entry["Build"]
|
||||
version = entry["OSVersion"]
|
||||
|
||||
catalog_url = ""
|
||||
try:
|
||||
catalog_url = entry["BridgeVersionInfo"]["CatalogURL"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"Version": version,
|
||||
"Build": build,
|
||||
"Catalog": CatalogURL().catalog_url_to_seed(catalog_url),
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _parse_english_distributions(self, data: bytes) -> dict:
|
||||
"""
|
||||
Resolve Title, Build and Version from the English distribution file
|
||||
"""
|
||||
try:
|
||||
plist_contents = plistlib.loads(data)
|
||||
except plistlib.InvalidFileException:
|
||||
plist_contents = None
|
||||
|
||||
try:
|
||||
xml_contents = ET.fromstring(data)
|
||||
except ET.ParseError:
|
||||
xml_contents = None
|
||||
|
||||
_product_map = {
|
||||
"Title": None,
|
||||
"Build": None,
|
||||
"Version": None,
|
||||
}
|
||||
|
||||
if plist_contents:
|
||||
if "macOSProductBuildVersion" in plist_contents:
|
||||
_product_map["Build"] = plist_contents["macOSProductBuildVersion"]
|
||||
if "macOSProductVersion" in plist_contents:
|
||||
_product_map["Version"] = plist_contents["macOSProductVersion"]
|
||||
if "BUILD" in plist_contents:
|
||||
_product_map["Build"] = plist_contents["BUILD"]
|
||||
if "VERSION" in plist_contents:
|
||||
_product_map["Version"] = plist_contents["VERSION"]
|
||||
|
||||
if xml_contents:
|
||||
# Fetch item title
|
||||
item_title = xml_contents.find(".//title").text
|
||||
if item_title in ["SU_TITLE", "MANUAL_TITLE", "MAN_TITLE"]:
|
||||
# regex search the contents for the title
|
||||
title_search = re.search(r'"SU_TITLE"\s*=\s*"(.*)";', data.decode("utf-8"))
|
||||
if title_search:
|
||||
item_title = title_search.group(1)
|
||||
|
||||
_product_map["Title"] = item_title
|
||||
|
||||
return _product_map
|
||||
|
||||
|
||||
def _build_installer_name(self, version: str, catalog: SeedType) -> str:
|
||||
"""
|
||||
Builds the installer name based on the version and catalog
|
||||
"""
|
||||
try:
|
||||
marketing_name = CatalogVersion(version.split(".")[0]).name
|
||||
except ValueError:
|
||||
marketing_name = "Unknown"
|
||||
|
||||
# Replace _ with space
|
||||
marketing_name = marketing_name.replace("_", " ")
|
||||
|
||||
# Convert to upper for each word
|
||||
marketing_name = "macOS " + " ".join([word.capitalize() for word in marketing_name.split()])
|
||||
|
||||
# Append Beta if needed
|
||||
if catalog in [SeedType.DeveloperSeed, SeedType.PublicSeed, SeedType.CustomerSeed]:
|
||||
marketing_name += " Beta"
|
||||
|
||||
return marketing_name
|
||||
|
||||
|
||||
def _list_latest_installers_only(self, products: list) -> list:
|
||||
"""
|
||||
List only the latest installers per macOS version
|
||||
|
||||
macOS versions capped at n-3 (n being the latest macOS version)
|
||||
"""
|
||||
|
||||
supported_versions = []
|
||||
|
||||
# Build list of supported versions (n to n-3, where n is the latest macOS version set)
|
||||
did_find_latest = False
|
||||
for version in CatalogVersion:
|
||||
if did_find_latest is False:
|
||||
if version != self.max_ia_catalog:
|
||||
continue
|
||||
did_find_latest = True
|
||||
|
||||
supported_versions.append(version)
|
||||
|
||||
if len(supported_versions) == 4:
|
||||
break
|
||||
|
||||
# invert the list
|
||||
supported_versions = supported_versions[::-1]
|
||||
|
||||
# Remove all but the newest version
|
||||
for version in supported_versions:
|
||||
_newest_version = packaging.version.parse("0.0.0")
|
||||
|
||||
# First, determine largest version
|
||||
for installer in products:
|
||||
if installer["Version"] is None:
|
||||
continue
|
||||
if not installer["Version"].startswith(version.value):
|
||||
continue
|
||||
if installer["Catalog"] in [SeedType.CustomerSeed, SeedType.DeveloperSeed, SeedType.PublicSeed]:
|
||||
continue
|
||||
try:
|
||||
if packaging.version.parse(installer["Version"]) > _newest_version:
|
||||
_newest_version = packaging.version.parse(installer["Version"])
|
||||
except packaging.version.InvalidVersion:
|
||||
pass
|
||||
|
||||
# Next, remove all installers that are not the newest version
|
||||
for installer in products:
|
||||
if installer["Version"] is None:
|
||||
continue
|
||||
if not installer["Version"].startswith(version.value):
|
||||
continue
|
||||
try:
|
||||
if packaging.version.parse(installer["Version"]) < _newest_version:
|
||||
products.remove(installer)
|
||||
except packaging.version.InvalidVersion:
|
||||
pass
|
||||
|
||||
# Remove Betas if there's a non-beta version available
|
||||
for installer in products:
|
||||
if installer["Catalog"] in [SeedType.CustomerSeed, SeedType.DeveloperSeed, SeedType.PublicSeed]:
|
||||
for installer_2 in products:
|
||||
if installer_2["Version"].split(".")[0] == installer["Version"].split(".")[0] and installer_2["Catalog"] not in [SeedType.CustomerSeed, SeedType.DeveloperSeed, SeedType.PublicSeed]:
|
||||
products.remove(installer)
|
||||
|
||||
# Remove EOL versions (older than n-3)
|
||||
for installer in products:
|
||||
if installer["Version"].split(".")[0] < supported_versions[-4].value:
|
||||
products.remove(installer)
|
||||
|
||||
return products
|
||||
|
||||
|
||||
@cached_property
|
||||
def products(self) -> None:
|
||||
"""
|
||||
Returns a list of products from the sucatalog
|
||||
"""
|
||||
|
||||
catalog = self.catalog
|
||||
|
||||
_products = []
|
||||
|
||||
for product in catalog["Products"]:
|
||||
|
||||
# InstallAssistants.pkgs (macOS Installers) will have the following keys:
|
||||
if self.ia_only:
|
||||
if "ExtendedMetaInfo" 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
|
||||
|
||||
_product_map = {
|
||||
"ProductID": product,
|
||||
"PostDate": catalog["Products"][product]["PostDate"],
|
||||
"Title": None,
|
||||
"Build": None,
|
||||
"Version": None,
|
||||
"Catalog": None,
|
||||
|
||||
# Optional keys if not InstallAssistant only:
|
||||
# "Packages": None,
|
||||
|
||||
# Optional keys if InstallAssistant found:
|
||||
# "InstallAssistant": {
|
||||
# "URL": None,
|
||||
# "Size": None,
|
||||
# "XNUMajor": None,
|
||||
# "IntegrityDataURL": None,
|
||||
# "IntegrityDataSize": None
|
||||
# },
|
||||
}
|
||||
|
||||
# InstallAssistant logic
|
||||
if "Packages" in catalog["Products"][product]:
|
||||
# Add packages to product map if not InstallAssistant only
|
||||
if self.ia_only is False:
|
||||
_product_map["Packages"] = catalog["Products"][product]["Packages"]
|
||||
for package in catalog["Products"][product]["Packages"]:
|
||||
if "URL" in package:
|
||||
if Path(package["URL"]).name == "InstallAssistant.pkg":
|
||||
_product_map["InstallAssistant"] = {
|
||||
"URL": package["URL"],
|
||||
"Size": package["Size"],
|
||||
"IntegrityDataURL": package["IntegrityDataURL"],
|
||||
"IntegrityDataSize": package["IntegrityDataSize"]
|
||||
}
|
||||
|
||||
if Path(package["URL"]).name not in ["Info.plist", "com_apple_MobileAsset_MacSoftwareUpdate.plist"]:
|
||||
continue
|
||||
|
||||
net_obj = network_handler.NetworkUtilities().get(package["URL"])
|
||||
if net_obj is None:
|
||||
continue
|
||||
|
||||
contents = net_obj.content
|
||||
try:
|
||||
plist_contents = plistlib.loads(contents)
|
||||
except plistlib.InvalidFileException:
|
||||
continue
|
||||
|
||||
if plist_contents:
|
||||
if Path(package["URL"]).name == "Info.plist":
|
||||
_product_map.update(self._legacy_parse_info_plist(plist_contents))
|
||||
else:
|
||||
_product_map.update(self._parse_mobile_asset_plist(plist_contents))
|
||||
|
||||
if _product_map["Version"] is not None:
|
||||
_product_map["Title"] = self._build_installer_name(_product_map["Version"], _product_map["Catalog"])
|
||||
|
||||
# Fall back to English distribution if no version is found
|
||||
if _product_map["Version"] is None:
|
||||
url = None
|
||||
if "Distributions" in catalog["Products"][product]:
|
||||
if "English" in catalog["Products"][product]["Distributions"]:
|
||||
url = catalog["Products"][product]["Distributions"]["English"]
|
||||
elif "en" in catalog["Products"][product]["Distributions"]:
|
||||
url = catalog["Products"][product]["Distributions"]["en"]
|
||||
|
||||
if url is None:
|
||||
continue
|
||||
|
||||
net_obj = network_handler.NetworkUtilities().get(url)
|
||||
if net_obj is None:
|
||||
continue
|
||||
|
||||
contents = net_obj.content
|
||||
|
||||
_product_map.update(self._parse_english_distributions(contents))
|
||||
|
||||
if _product_map["Version"] is None:
|
||||
if "ServerMetadataURL" in catalog["Products"][product]:
|
||||
server_metadata_url = catalog["Products"][product]["ServerMetadataURL"]
|
||||
|
||||
net_obj = network_handler.NetworkUtilities().get(server_metadata_url)
|
||||
if net_obj is None:
|
||||
continue
|
||||
|
||||
server_metadata_contents = net_obj.content
|
||||
|
||||
try:
|
||||
server_metadata_plist = plistlib.loads(server_metadata_contents)
|
||||
except plistlib.InvalidFileException:
|
||||
pass
|
||||
|
||||
if "CFBundleShortVersionString" in server_metadata_plist:
|
||||
_product_map["Version"] = server_metadata_plist["CFBundleShortVersionString"]
|
||||
|
||||
|
||||
if _product_map["Version"] is not None:
|
||||
# Check if version is newer than the max version
|
||||
if self.ia_only:
|
||||
try:
|
||||
if packaging.version.parse(_product_map["Version"]) > self.max_ia_version:
|
||||
continue
|
||||
except packaging.version.InvalidVersion:
|
||||
pass
|
||||
|
||||
if _product_map["Build"] is not None:
|
||||
if "InstallAssistant" in _product_map:
|
||||
try:
|
||||
# Grab first 2 characters of build
|
||||
_product_map["InstallAssistant"]["XNUMajor"] = int(_product_map["Build"][:2])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# If version is still None, set to 0.0.0
|
||||
if _product_map["Version"] is None:
|
||||
_product_map["Version"] = "0.0.0"
|
||||
|
||||
_products.append(_product_map)
|
||||
|
||||
_products = sorted(_products, key=lambda x: x["Version"])
|
||||
|
||||
return _products
|
||||
|
||||
|
||||
@cached_property
|
||||
def latest_products(self) -> list:
|
||||
"""
|
||||
Returns a list of the latest products from the sucatalog
|
||||
"""
|
||||
return self._list_latest_installers_only(self.products)
|
||||
175
opencore_legacy_patcher/sucatalog/url.py
Normal file
175
opencore_legacy_patcher/sucatalog/url.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""
|
||||
url.py: Generate URL for Software Update Catalog
|
||||
|
||||
Usage:
|
||||
>>> import sucatalog
|
||||
>>> catalog_url = sucatalog.CatalogURL().url
|
||||
https://swscan.apple.com/content/catalogs/others/index-15seed-15-14-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
|
||||
"""
|
||||
|
||||
import logging
|
||||
import plistlib
|
||||
|
||||
from .constants import (
|
||||
SeedType,
|
||||
CatalogVersion,
|
||||
CatalogExtension
|
||||
)
|
||||
|
||||
from ..support import network_handler
|
||||
|
||||
|
||||
class CatalogURL:
|
||||
"""
|
||||
Provides URL generation for Software Update Catalog
|
||||
|
||||
Args:
|
||||
version (CatalogVersion): Version of macOS
|
||||
seed (SeedType): Seed type
|
||||
extension (CatalogExtension): Extension for the catalog URL
|
||||
"""
|
||||
def __init__(self,
|
||||
version: CatalogVersion = CatalogVersion.SONOMA,
|
||||
seed: SeedType = SeedType.PublicRelease,
|
||||
extension: CatalogExtension = CatalogExtension.PLIST
|
||||
) -> None:
|
||||
self.version = version
|
||||
self.seed = seed
|
||||
self.extension = extension
|
||||
|
||||
self.seed = self._fix_seed_type()
|
||||
self.version = self._fix_version()
|
||||
|
||||
|
||||
def _fix_seed_type(self) -> SeedType:
|
||||
"""
|
||||
Fixes seed type for URL generation
|
||||
"""
|
||||
# Pre-Mountain Lion lacked seed types
|
||||
if self.version in [CatalogVersion.LION, CatalogVersion.SNOW_LEOPARD, CatalogVersion.LEOPARD, CatalogVersion.TIGER]:
|
||||
if self.seed != SeedType.PublicRelease:
|
||||
logging.warning(f"{self.seed.name} not supported for {self.version.name}, defaulting to PublicRelease")
|
||||
return SeedType.PublicRelease
|
||||
|
||||
# Pre-Yosemite lacked PublicSeed/CustomerSeed, thus override to DeveloperSeed
|
||||
if self.version in [CatalogVersion.MAVERICKS, CatalogVersion.MOUNTAIN_LION]:
|
||||
if self.seed in [SeedType.PublicSeed, SeedType.CustomerSeed]:
|
||||
logging.warning(f"{self.seed.name} not supported for {self.version.name}, defaulting to DeveloperSeed")
|
||||
return SeedType.DeveloperSeed
|
||||
|
||||
return self.seed
|
||||
|
||||
|
||||
def _fix_version(self) -> CatalogVersion:
|
||||
"""
|
||||
Fixes version for URL generation
|
||||
"""
|
||||
if self.version == CatalogVersion.BIG_SUR:
|
||||
return CatalogVersion.BIG_SUR_LEGACY
|
||||
|
||||
return self.version
|
||||
|
||||
|
||||
def _fetch_versions_for_url(self) -> list:
|
||||
"""
|
||||
Fetches versions for URL generation
|
||||
"""
|
||||
versions: list = []
|
||||
|
||||
_did_hit_variant: bool = False
|
||||
for variant in CatalogVersion:
|
||||
|
||||
# Avoid appending versions newer than the current version
|
||||
if variant == self.version:
|
||||
_did_hit_variant = True
|
||||
if _did_hit_variant is False:
|
||||
continue
|
||||
|
||||
# Skip invalid version
|
||||
if variant in [CatalogVersion.BIG_SUR, CatalogVersion.TIGER]:
|
||||
continue
|
||||
|
||||
versions.append(variant.value)
|
||||
|
||||
if self.version == CatalogVersion.SNOW_LEOPARD:
|
||||
# Reverse list pre-Lion (ie. just Snow Leopard, since Lion is a list of one)
|
||||
versions = versions[::-1]
|
||||
|
||||
return versions
|
||||
|
||||
|
||||
def _construct_catalog_url(self) -> str:
|
||||
"""
|
||||
Constructs the catalog URL based on the seed type
|
||||
"""
|
||||
|
||||
url: str = "https://swscan.apple.com/content/catalogs"
|
||||
|
||||
if self.version == CatalogVersion.TIGER:
|
||||
url += "/index"
|
||||
else:
|
||||
url += "/others/index"
|
||||
|
||||
if self.seed in [SeedType.DeveloperSeed, SeedType.PublicSeed, SeedType.CustomerSeed]:
|
||||
url += f"-{self.version.value}"
|
||||
if self.version == CatalogVersion.MAVERICKS and self.seed == SeedType.CustomerSeed:
|
||||
# Apple previously used 'publicseed' for CustomerSeed in Mavericks
|
||||
url += "publicseed"
|
||||
else:
|
||||
url += f"{self.seed.value}"
|
||||
|
||||
# 10.10 and older don't append versions for CustomerSeed
|
||||
if self.seed == SeedType.CustomerSeed and self.version in [
|
||||
CatalogVersion.YOSEMITE,
|
||||
CatalogVersion.MAVERICKS,
|
||||
CatalogVersion.MOUNTAIN_LION,
|
||||
CatalogVersion.LION,
|
||||
CatalogVersion.SNOW_LEOPARD,
|
||||
CatalogVersion.LEOPARD
|
||||
]:
|
||||
pass
|
||||
else:
|
||||
for version in self._fetch_versions_for_url():
|
||||
url += f"-{version}"
|
||||
|
||||
if self.version != CatalogVersion.TIGER:
|
||||
url += ".merged-1"
|
||||
url += self.extension.value
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def catalog_url_to_seed(self, catalog_url: str) -> SeedType:
|
||||
"""
|
||||
Converts the Catalog URL to a SeedType
|
||||
"""
|
||||
if "beta" in catalog_url:
|
||||
return SeedType.PublicSeed
|
||||
elif "customerseed" in catalog_url:
|
||||
return SeedType.CustomerSeed
|
||||
elif "seed" in catalog_url:
|
||||
return SeedType.DeveloperSeed
|
||||
return SeedType.PublicRelease
|
||||
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""
|
||||
Generate URL for Software Update Catalog
|
||||
|
||||
Returns:
|
||||
str: URL for Software Update Catalog
|
||||
"""
|
||||
return self._construct_catalog_url()
|
||||
|
||||
|
||||
@property
|
||||
def url_contents(self) -> dict:
|
||||
"""
|
||||
Return URL contents
|
||||
"""
|
||||
try:
|
||||
return plistlib.loads(network_handler.NetworkUtilities().get(self.url).content)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to fetch URL contents: {e}")
|
||||
return None
|
||||
@@ -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.
|
||||
|
||||
@@ -135,13 +135,16 @@ class PatchSysVolume:
|
||||
mounted_system_version = Path(self.mount_location) / "System/Library/CoreServices/SystemVersion.plist"
|
||||
|
||||
if not mounted_system_version.exists():
|
||||
logging.error("- Failed to find SystemVersion.plist")
|
||||
logging.error("- Failed to find SystemVersion.plist on mounted root volume")
|
||||
return False
|
||||
|
||||
try:
|
||||
mounted_data = plistlib.load(open(mounted_system_version, "rb"))
|
||||
if mounted_data["ProductBuildVersion"] != self.constants.detected_os_build:
|
||||
logging.error(f"- SystemVersion.plist build version mismatch: {mounted_data['ProductBuildVersion']} vs {self.constants.detected_os_build}")
|
||||
logging.error(
|
||||
f"- SystemVersion.plist build version mismatch: found {mounted_data['ProductVersion']} ({mounted_data['ProductBuildVersion']}), expected {self.constants.detected_os_version} ({self.constants.detected_os_build})"
|
||||
)
|
||||
logging.error("An update is in progress on your machine and patching cannot continue until it is cancelled or finished")
|
||||
return False
|
||||
except:
|
||||
logging.error("- Failed to parse SystemVersion.plist")
|
||||
|
||||
@@ -10,7 +10,10 @@ import webbrowser
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import constants
|
||||
from .. import (
|
||||
constants,
|
||||
sucatalog
|
||||
)
|
||||
|
||||
from ..datasets import (
|
||||
os_data,
|
||||
@@ -46,7 +49,7 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
self.available_installers = None
|
||||
self.available_installers_latest = None
|
||||
|
||||
self.catalog_seed: macos_installer_handler.SeedType = macos_installer_handler.SeedType.DeveloperSeed
|
||||
self.catalog_seed: sucatalog.SeedType = sucatalog.SeedType.DeveloperSeed
|
||||
|
||||
self.frame_modal = wx.Dialog(parent, title=title, size=(330, 200))
|
||||
|
||||
@@ -132,10 +135,16 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
|
||||
# Grab installer catalog
|
||||
def _fetch_installers():
|
||||
logging.info(f"Fetching installer catalog: {macos_installer_handler.SeedType(self.catalog_seed).name}")
|
||||
remote_obj = macos_installer_handler.RemoteInstallerCatalog(seed_override=self.catalog_seed)
|
||||
self.available_installers = remote_obj.available_apps
|
||||
self.available_installers_latest = remote_obj.available_apps_latest
|
||||
logging.info(f"Fetching installer catalog: {sucatalog.SeedType.DeveloperSeed.name}")
|
||||
|
||||
sucatalog_contents = sucatalog.CatalogURL(seed=sucatalog.SeedType.DeveloperSeed).url_contents
|
||||
if sucatalog_contents is None:
|
||||
logging.error("Failed to download Installer Catalog from Apple")
|
||||
return
|
||||
|
||||
self.available_installers = sucatalog.CatalogProducts(sucatalog_contents).products
|
||||
self.available_installers_latest = sucatalog.CatalogProducts(sucatalog_contents).latest_products
|
||||
|
||||
|
||||
thread = threading.Thread(target=_fetch_installers)
|
||||
thread.start()
|
||||
@@ -157,7 +166,7 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
bundles = [wx.BitmapBundle.FromBitmaps(icon) for icon in self.icons]
|
||||
|
||||
self.frame_modal.Destroy()
|
||||
self.frame_modal = wx.Dialog(self, title="Select macOS Installer", size=(460, 500))
|
||||
self.frame_modal = wx.Dialog(self, title="Select macOS Installer", size=(505, 500))
|
||||
|
||||
# Title: Select macOS Installer
|
||||
title_label = wx.StaticText(self.frame_modal, label="Select macOS Installer", pos=(-1,-1))
|
||||
@@ -169,35 +178,31 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
self.list = wx.ListCtrl(self.frame_modal, id, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_NO_HEADER | wx.BORDER_SUNKEN)
|
||||
self.list.SetSmallImages(bundles)
|
||||
|
||||
self.list.InsertColumn(0, "Version")
|
||||
self.list.InsertColumn(1, "Size")
|
||||
self.list.InsertColumn(2, "Release Date")
|
||||
self.list.InsertColumn(0, "Title", width=175)
|
||||
self.list.InsertColumn(1, "Version", width=50)
|
||||
self.list.InsertColumn(2, "Build", width=75)
|
||||
self.list.InsertColumn(3, "Size", width=75)
|
||||
self.list.InsertColumn(4, "Release Date", width=100)
|
||||
|
||||
installers = self.available_installers_latest if show_full is False else self.available_installers
|
||||
if show_full is False:
|
||||
self.frame_modal.SetSize((460, 370))
|
||||
self.frame_modal.SetSize((490, 370))
|
||||
|
||||
if installers:
|
||||
locale.setlocale(locale.LC_TIME, '')
|
||||
logging.info(f"Available installers on SUCatalog ({'All entries' if show_full else 'Latest only'}):")
|
||||
for item in installers:
|
||||
extra = " Beta" if installers[item]['Variant'] in ["DeveloperSeed" , "PublicSeed"] else ""
|
||||
logging.info(f"- macOS {installers[item]['Version']} ({installers[item]['Build']}):\n - Size: {utilities.human_fmt(installers[item]['Size'])}\n - Source: {installers[item]['Source']}\n - Variant: {installers[item]['Variant']}\n - Link: {installers[item]['Link']}\n")
|
||||
index = self.list.InsertItem(self.list.GetItemCount(), f"macOS {installers[item]['Version']} {os_data.os_conversion.convert_kernel_to_marketing_name(int(installers[item]['Build'][:2]))}{extra} ({installers[item]['Build']})")
|
||||
self.list.SetItemImage(index, self._macos_version_to_icon(int(installers[item]['Build'][:2])))
|
||||
self.list.SetItem(index, 1, utilities.human_fmt(installers[item]['Size']))
|
||||
self.list.SetItem(index, 2, installers[item]['Date'].strftime("%x"))
|
||||
logging.info(f"- {item['Title']} ({item['Version']} - {item['Build']}):\n - Size: {utilities.human_fmt(item['InstallAssistant']['Size'])}\n - Link: {item['InstallAssistant']['URL']}\n")
|
||||
index = self.list.InsertItem(self.list.GetItemCount(), f"{item['Title']}")
|
||||
self.list.SetItemImage(index, self._macos_version_to_icon(int(item['Build'][:2])))
|
||||
self.list.SetItem(index, 1, item['Version'])
|
||||
self.list.SetItem(index, 2, item['Build'])
|
||||
self.list.SetItem(index, 3, utilities.human_fmt(item['InstallAssistant']['Size']))
|
||||
self.list.SetItem(index, 4, item['PostDate'].strftime("%x"))
|
||||
else:
|
||||
logging.error("No installers found on SUCatalog")
|
||||
wx.MessageDialog(self.frame_modal, "Failed to download Installer Catalog from Apple", "Error", wx.OK | wx.ICON_ERROR).ShowModal()
|
||||
|
||||
self.list.SetColumnWidth(0, 280)
|
||||
self.list.SetColumnWidth(1, 65)
|
||||
if show_full is True:
|
||||
self.list.SetColumnWidth(2, 80)
|
||||
else:
|
||||
self.list.SetColumnWidth(2, 94) # Hack to get the highlight to fill the ListCtrl
|
||||
|
||||
if show_full is False:
|
||||
self.list.Select(-1)
|
||||
|
||||
@@ -256,7 +261,7 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
if not clipboard.IsOpened():
|
||||
clipboard.Open()
|
||||
|
||||
clipboard.SetData(wx.TextDataObject(list(installers.values())[selected_item]['Link']))
|
||||
clipboard.SetData(wx.TextDataObject(installers[selected_item]['InstallAssistant']['URL']))
|
||||
|
||||
clipboard.Close()
|
||||
|
||||
@@ -278,14 +283,15 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
|
||||
selected_item = self.list.GetFirstSelected()
|
||||
if selected_item != -1:
|
||||
selected_installer = installers[selected_item]
|
||||
|
||||
logging.info(f"Selected macOS {list(installers.values())[selected_item]['Version']} ({list(installers.values())[selected_item]['Build']})")
|
||||
logging.info(f"Selected macOS {selected_installer['Version']} ({selected_installer['Build']})")
|
||||
|
||||
# Notify user whether their model is compatible with the selected installer
|
||||
problems = []
|
||||
model = self.constants.custom_model or self.constants.computer.real_model
|
||||
if model in smbios_data.smbios_dictionary:
|
||||
if list(installers.values())[selected_item]["OS"] >= os_data.os_data.ventura:
|
||||
if selected_installer["InstallAssistant"]["XNUMajor"] >= os_data.os_data.ventura:
|
||||
if smbios_data.smbios_dictionary[model]["CPU Generation"] <= cpu_data.CPUGen.penryn or model in ["MacPro4,1", "MacPro5,1", "Xserve3,1"]:
|
||||
if model.startswith("MacBook"):
|
||||
problems.append("Lack of internal Keyboard/Trackpad in macOS installer.")
|
||||
@@ -293,7 +299,7 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
problems.append("Lack of internal Keyboard/Mouse in macOS installer.")
|
||||
|
||||
if problems:
|
||||
logging.warning(f"Potential issues with {model} and {list(installers.values())[selected_item]['Version']} ({list(installers.values())[selected_item]['Build']}): {problems}")
|
||||
logging.warning(f"Potential issues with {model} and {selected_installer['Version']} ({selected_installer['Build']}): {problems}")
|
||||
problems = "\n".join(problems)
|
||||
dlg = wx.MessageDialog(self.frame_modal, f"Your model ({model}) may not be fully supported by this installer. You may encounter the following issues:\n\n{problems}\n\nFor more information, see associated page. Otherwise, we recommend using macOS Monterey", "Potential Issues", wx.YES_NO | wx.CANCEL | wx.ICON_WARNING)
|
||||
dlg.SetYesNoCancelLabels("View Github Issue", "Download Anyways", "Cancel")
|
||||
@@ -305,7 +311,7 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
return
|
||||
|
||||
host_space = utilities.get_free_space()
|
||||
needed_space = list(installers.values())[selected_item]['Size'] * 2
|
||||
needed_space = selected_installer['InstallAssistant']['Size'] * 2
|
||||
if host_space < needed_space:
|
||||
logging.error(f"Insufficient space to download and extract: {utilities.human_fmt(host_space)} available vs {utilities.human_fmt(needed_space)} required")
|
||||
dlg = wx.MessageDialog(self.frame_modal, f"You do not have enough free space to download and extract this installer. Please free up some space and try again\n\n{utilities.human_fmt(host_space)} available vs {utilities.human_fmt(needed_space)} required", "Insufficient Space", wx.OK | wx.ICON_WARNING)
|
||||
@@ -314,22 +320,22 @@ class macOSInstallerDownloadFrame(wx.Frame):
|
||||
|
||||
self.frame_modal.Close()
|
||||
|
||||
download_obj = network_handler.DownloadObject(list(installers.values())[selected_item]['Link'], self.constants.payload_path / "InstallAssistant.pkg")
|
||||
download_obj = network_handler.DownloadObject(selected_installer['InstallAssistant']['URL'], self.constants.payload_path / "InstallAssistant.pkg")
|
||||
|
||||
gui_download.DownloadFrame(
|
||||
self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
download_obj=download_obj,
|
||||
item_name=f"macOS {list(installers.values())[selected_item]['Version']} ({list(installers.values())[selected_item]['Build']})",
|
||||
download_icon=self.constants.icons_path[self._macos_version_to_icon(int(list(installers.values())[selected_item]['Build'][:2]))]
|
||||
item_name=f"macOS {selected_installer['Version']} ({selected_installer['Build']})",
|
||||
download_icon=self.constants.icons_path[self._macos_version_to_icon(selected_installer["InstallAssistant"]["XNUMajor"])]
|
||||
)
|
||||
|
||||
if download_obj.download_complete is False:
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
self._validate_installer(list(installers.values())[selected_item]['integrity'])
|
||||
self._validate_installer(selected_installer['InstallAssistant']['IntegrityDataURL'])
|
||||
|
||||
|
||||
def _validate_installer(self, chunklist_link: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user