mirror of
https://github.com/dortania/OpenCore-Legacy-Patcher.git
synced 2026-04-24 20:10:14 +10:00
macos_installer_handler.py: Reworked from installer.py module
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
- Allows for more reliable network calls and downloads
|
||||
- Better supports network timeouts and disconnects
|
||||
- Dramatically less noise in console during downloads
|
||||
- Implemented new macOS Installer handler
|
||||
- Removed unused modules:
|
||||
- sys_patch_downloader.py
|
||||
- run.py
|
||||
|
||||
@@ -31,14 +31,14 @@ from resources import (
|
||||
constants,
|
||||
defaults,
|
||||
install,
|
||||
installer,
|
||||
utilities,
|
||||
generate_smbios,
|
||||
updates,
|
||||
integrity_verification,
|
||||
global_settings,
|
||||
kdk_handler,
|
||||
network_handler
|
||||
network_handler,
|
||||
macos_installer_handler
|
||||
)
|
||||
|
||||
from resources.sys_patch import sys_patch_detect, sys_patch
|
||||
@@ -1652,7 +1652,9 @@ class wx_python_gui:
|
||||
# Download installer catalog
|
||||
if ias is None:
|
||||
def ia():
|
||||
self.available_installers = installer.list_downloadable_macOS_installers(self.constants.payload_path, "DeveloperSeed")
|
||||
remote_obj = macos_installer_handler.RemoteInstallerCatalog(seed_override=macos_installer_handler.SeedType.DeveloperSeed)
|
||||
self.available_installers = remote_obj.available_apps
|
||||
self.available_installers_latest = remote_obj.available_apps_latest
|
||||
|
||||
logging.info("- Downloading installer catalog...")
|
||||
thread_ia = threading.Thread(target=ia)
|
||||
@@ -1690,7 +1692,7 @@ class wx_python_gui:
|
||||
i = -20
|
||||
if available_installers:
|
||||
if ias is None:
|
||||
available_installers = installer.only_list_newest_installers(available_installers)
|
||||
available_installers = self.available_installers_latest
|
||||
for app in available_installers:
|
||||
logging.info(f"macOS {available_installers[app]['Version']} ({available_installers[app]['Build']}):\n - Size: {utilities.human_fmt(available_installers[app]['Size'])}\n - Source: {available_installers[app]['Source']}\n - Variant: {available_installers[app]['Variant']}\n - Link: {available_installers[app]['Link']}\n")
|
||||
if available_installers[app]['Variant'] in ["DeveloperSeed" , "PublicSeed"]:
|
||||
@@ -1964,7 +1966,7 @@ class wx_python_gui:
|
||||
self.header.Centre(wx.HORIZONTAL)
|
||||
self.verifying_chunk_label.SetLabel("Installing into Applications folder")
|
||||
self.verifying_chunk_label.Centre(wx.HORIZONTAL)
|
||||
thread_install = threading.Thread(target=installer.install_macOS_installer, args=(self.constants.payload_path,))
|
||||
thread_install = threading.Thread(target=macos_installer_handler.InstallerCreation().install_macOS_installer, args=(self.constants.payload_path,))
|
||||
thread_install.start()
|
||||
self.progress_bar.Pulse()
|
||||
while thread_install.is_alive():
|
||||
@@ -2001,7 +2003,7 @@ class wx_python_gui:
|
||||
|
||||
# Spawn thread to get list of installers
|
||||
def get_installers():
|
||||
self.available_installers = installer.list_local_macOS_installers()
|
||||
self.available_installers = macos_installer_handler.LocalInstallerCatalog().available_apps
|
||||
|
||||
thread_get_installers = threading.Thread(target=get_installers)
|
||||
thread_get_installers.start()
|
||||
@@ -2105,7 +2107,7 @@ class wx_python_gui:
|
||||
self.usb_selection_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
i = -15
|
||||
available_disks = installer.list_disk_to_format()
|
||||
available_disks = macos_installer_handler.InstallerCreation().list_disk_to_format()
|
||||
if available_disks:
|
||||
logging.info("Disks found")
|
||||
for disk in available_disks:
|
||||
@@ -2305,7 +2307,7 @@ class wx_python_gui:
|
||||
self.return_to_main_menu.Enable()
|
||||
|
||||
def prepare_script(self, installer_path, disk):
|
||||
self.prepare_result = installer.generate_installer_creation_script(self.constants.payload_path, installer_path, disk)
|
||||
self.prepare_result = macos_installer_handler.InstallerCreation().generate_installer_creation_script(self.constants.payload_path, installer_path, disk)
|
||||
|
||||
def start_script(self):
|
||||
utilities.disable_sleep_while_running()
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
# Creates a macOS Installer
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import logging
|
||||
from resources import utilities, network_handler
|
||||
|
||||
def list_local_macOS_installers():
|
||||
# Finds all applicable macOS installers
|
||||
# within a user's /Applications folder
|
||||
# Returns a list of installers
|
||||
application_list = {}
|
||||
|
||||
for application in Path("/Applications").iterdir():
|
||||
# Verify whether application has createinstallmedia
|
||||
try:
|
||||
if (Path("/Applications") / Path(application) / Path("Contents/Resources/createinstallmedia")).exists():
|
||||
plist = plistlib.load((Path("/Applications") / Path(application) / Path("Contents/Info.plist")).open("rb"))
|
||||
try:
|
||||
# Doesn't reflect true OS build, but best to report SDK in the event multiple installers are found with same version
|
||||
app_version = plist["DTPlatformVersion"]
|
||||
clean_name = plist["CFBundleDisplayName"]
|
||||
try:
|
||||
app_sdk = plist["DTSDKBuild"]
|
||||
except KeyError:
|
||||
app_sdk = "Unknown"
|
||||
|
||||
# app_version can sometimes report GM instead of the actual version
|
||||
# This is a workaround to get the actual version
|
||||
if app_version.startswith("GM"):
|
||||
try:
|
||||
app_version = int(app_sdk[:2])
|
||||
if app_version < 20:
|
||||
app_version = f"10.{app_version - 4}"
|
||||
else:
|
||||
app_version = f"{app_version - 9}.0"
|
||||
except ValueError:
|
||||
app_version = "Unknown"
|
||||
# Check if App Version is High Sierra or newer
|
||||
can_add = False
|
||||
if app_version.startswith("10."):
|
||||
app_sub_version = app_version.split(".")[1]
|
||||
if int(app_sub_version) >= 13:
|
||||
can_add = True
|
||||
else:
|
||||
can_add = False
|
||||
else:
|
||||
can_add = True
|
||||
|
||||
# Check SharedSupport.dmg's data
|
||||
results = parse_sharedsupport_version(Path("/Applications") / Path(application)/ Path("Contents/SharedSupport/SharedSupport.dmg"))
|
||||
if results[0] is not None:
|
||||
app_sdk = results[0]
|
||||
if results[1] is not None:
|
||||
app_version = results[1]
|
||||
|
||||
if can_add is True:
|
||||
application_list.update({
|
||||
application: {
|
||||
"Short Name": clean_name,
|
||||
"Version": app_version,
|
||||
"Build": app_sdk,
|
||||
"Path": application,
|
||||
}
|
||||
})
|
||||
except KeyError:
|
||||
pass
|
||||
except PermissionError:
|
||||
pass
|
||||
# Sort Applications by version
|
||||
application_list = {k: v for k, v in sorted(application_list.items(), key=lambda item: item[1]["Version"])}
|
||||
return application_list
|
||||
|
||||
def parse_sharedsupport_version(sharedsupport_path):
|
||||
detected_build = None
|
||||
detected_os = None
|
||||
sharedsupport_path = Path(sharedsupport_path)
|
||||
|
||||
if not sharedsupport_path.exists():
|
||||
return (detected_build, detected_os)
|
||||
|
||||
if not sharedsupport_path.name.endswith(".dmg"):
|
||||
return (detected_build, detected_os)
|
||||
|
||||
|
||||
# Create temporary directory to extract SharedSupport.dmg to
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output = subprocess.run(
|
||||
[
|
||||
"hdiutil", "attach", "-noverify", sharedsupport_path,
|
||||
"-mountpoint", tmpdir,
|
||||
"-nobrowse",
|
||||
],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
if output.returncode != 0:
|
||||
return (detected_build, detected_os)
|
||||
|
||||
ss_info = Path("SFR/com_apple_MobileAsset_SFRSoftwareUpdate/com_apple_MobileAsset_SFRSoftwareUpdate.xml")
|
||||
|
||||
if Path(tmpdir / ss_info).exists():
|
||||
plist = plistlib.load((tmpdir / ss_info).open("rb"))
|
||||
if "Build" in plist["Assets"][0]:
|
||||
detected_build = plist["Assets"][0]["Build"]
|
||||
if "OSVersion" in plist["Assets"][0]:
|
||||
detected_os = plist["Assets"][0]["OSVersion"]
|
||||
|
||||
# Unmount SharedSupport.dmg
|
||||
output = subprocess.run(["hdiutil", "detach", tmpdir], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
return (detected_build, detected_os)
|
||||
|
||||
|
||||
def create_installer(installer_path, volume_name):
|
||||
# Creates a macOS installer
|
||||
# Takes a path to the installer and the Volume
|
||||
# Returns boolean on success status
|
||||
|
||||
createinstallmedia_path = Path("/Applications") / Path(installer_path) / Path("Contents/Resources/createinstallmedia")
|
||||
|
||||
# Sanity check in the event the user somehow deleted it between the time we found it and now
|
||||
if (createinstallmedia_path).exists():
|
||||
utilities.cls()
|
||||
utilities.header(["Starting createinstallmedia"])
|
||||
logging.info("This will take some time, recommend making some coffee while you wait\n")
|
||||
utilities.elevated([createinstallmedia_path, "--volume", f"/Volumes/{volume_name}", "--nointeraction"])
|
||||
return True
|
||||
else:
|
||||
logging.info("- Failed to find createinstallmedia")
|
||||
return False
|
||||
|
||||
def download_install_assistant(download_path, ia_link):
|
||||
# Downloads InstallAssistant.pkg
|
||||
ia_download = network_handler.DownloadObject(ia_link, (Path(download_path) / Path("InstallAssistant.pkg")))
|
||||
ia_download.download(display_progress=True, spawn_thread=False)
|
||||
|
||||
if ia_download.download_complete is True:
|
||||
return True
|
||||
return False
|
||||
|
||||
def install_macOS_installer(download_path):
|
||||
logging.info("- Extracting macOS installer from InstallAssistant.pkg\n This may take some time")
|
||||
args = [
|
||||
"osascript",
|
||||
"-e",
|
||||
f'''do shell script "installer -pkg {Path(download_path)}/InstallAssistant.pkg -target /"'''
|
||||
' with prompt "OpenCore Legacy Patcher needs administrator privileges to add InstallAssistant."'
|
||||
" with administrator privileges"
|
||||
" without altering line endings",
|
||||
]
|
||||
|
||||
result = subprocess.run(args,stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if result.returncode == 0:
|
||||
logging.info("- InstallAssistant installed")
|
||||
return True
|
||||
else:
|
||||
logging.info("- Failed to install InstallAssistant")
|
||||
logging.info(f" Error Code: {result.returncode}")
|
||||
return False
|
||||
|
||||
def list_downloadable_macOS_installers(download_path, catalog):
|
||||
available_apps = {}
|
||||
if catalog == "DeveloperSeed":
|
||||
link = "https://swscan.apple.com/content/catalogs/others/index-13seed-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"
|
||||
elif catalog == "PublicSeed":
|
||||
link = "https://swscan.apple.com/content/catalogs/others/index-13beta-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"
|
||||
else:
|
||||
link = "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"
|
||||
|
||||
if network_handler.NetworkUtilities(link).verify_network_connection() is True:
|
||||
try:
|
||||
catalog_plist = plistlib.loads(network_handler.SESSION.get(link).content)
|
||||
except plistlib.InvalidFileException:
|
||||
return available_apps
|
||||
|
||||
for item in catalog_plist["Products"]:
|
||||
try:
|
||||
# Check if entry has SharedSupport and BuildManifest
|
||||
# Ensures only Big Sur and newer Installers are listed
|
||||
catalog_plist["Products"][item]["ExtendedMetaInfo"]["InstallAssistantPackageIdentifiers"]["SharedSupport"]
|
||||
catalog_plist["Products"][item]["ExtendedMetaInfo"]["InstallAssistantPackageIdentifiers"]["BuildManifest"]
|
||||
|
||||
for bm_package in catalog_plist["Products"][item]["Packages"]:
|
||||
if "Info.plist" in bm_package["URL"] and "InstallInfo.plist" not in bm_package["URL"]:
|
||||
try:
|
||||
build_plist = plistlib.loads(network_handler.SESSION.get(bm_package["URL"]).content)
|
||||
except plistlib.InvalidFileException:
|
||||
continue
|
||||
# Ensure Apple Silicon specific Installers are not listed
|
||||
if "VMM-x86_64" not in build_plist["MobileAssetProperties"]["SupportedDeviceModels"]:
|
||||
continue
|
||||
version = build_plist["MobileAssetProperties"]["OSVersion"]
|
||||
build = build_plist["MobileAssetProperties"]["Build"]
|
||||
try:
|
||||
catalog_url = build_plist["MobileAssetProperties"]["BridgeVersionInfo"]["CatalogURL"]
|
||||
if "beta" in catalog_url:
|
||||
catalog_url = "PublicSeed"
|
||||
elif "customerseed" in catalog_url:
|
||||
catalog_url = "CustomerSeed"
|
||||
elif "seed" in catalog_url:
|
||||
catalog_url = "DeveloperSeed"
|
||||
else:
|
||||
catalog_url = "Public"
|
||||
except KeyError:
|
||||
# Assume Public if no catalog URL is found
|
||||
catalog_url = "Public"
|
||||
for ia_package in catalog_plist["Products"][item]["Packages"]:
|
||||
if "InstallAssistant.pkg" in ia_package["URL"]:
|
||||
download_link = ia_package["URL"]
|
||||
size = ia_package["Size"]
|
||||
integrity = ia_package["IntegrityDataURL"]
|
||||
|
||||
available_apps.update({
|
||||
item: {
|
||||
"Version": version,
|
||||
"Build": build,
|
||||
"Link": download_link,
|
||||
"Size": size,
|
||||
"integrity": integrity,
|
||||
"Source": "Apple Inc.",
|
||||
"Variant": catalog_url,
|
||||
}
|
||||
})
|
||||
except KeyError:
|
||||
pass
|
||||
available_apps = {k: v for k, v in sorted(available_apps.items(), key=lambda x: x[1]['Version'])}
|
||||
return available_apps
|
||||
|
||||
def only_list_newest_installers(available_apps):
|
||||
# Takes a dictionary of available installers
|
||||
# Returns a dictionary of only the newest installers
|
||||
# This is used to avoid overwhelming the user with installer options
|
||||
|
||||
# Only strip OSes that we know are supported
|
||||
supported_versions = ["10.13", "10.14", "10.15", "11", "12", "13"]
|
||||
|
||||
for version in supported_versions:
|
||||
remote_version_minor = 0
|
||||
remote_version_security = 0
|
||||
os_builds = []
|
||||
|
||||
# First determine the largest version
|
||||
for ia in available_apps:
|
||||
if available_apps[ia]["Version"].startswith(version):
|
||||
if available_apps[ia]["Variant"] not in ["CustomerSeed", "DeveloperSeed", "PublicSeed"]:
|
||||
remote_version = available_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(available_apps):
|
||||
# Don't use Beta builds to determine latest version
|
||||
if available_apps[ia]["Variant"] in ["CustomerSeed", "DeveloperSeed", "PublicSeed"]:
|
||||
continue
|
||||
|
||||
if available_apps[ia]["Version"].startswith(version):
|
||||
remote_version = available_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:
|
||||
available_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:
|
||||
available_apps.pop(ia)
|
||||
continue
|
||||
else:
|
||||
if remote_version_security > 0:
|
||||
available_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 available_apps[ia]["Build"] in os_builds:
|
||||
available_apps.pop(ia)
|
||||
continue
|
||||
|
||||
os_builds.append(available_apps[ia]["Build"])
|
||||
|
||||
# Final passthrough
|
||||
# Remove Betas if there's a non-beta version available
|
||||
for ia in list(available_apps):
|
||||
if available_apps[ia]["Variant"] in ["CustomerSeed", "DeveloperSeed", "PublicSeed"]:
|
||||
for ia2 in available_apps:
|
||||
if available_apps[ia2]["Version"].split(".")[0] == available_apps[ia]["Version"].split(".")[0] and available_apps[ia2]["Variant"] not in ["CustomerSeed", "DeveloperSeed", "PublicSeed"]:
|
||||
available_apps.pop(ia)
|
||||
break
|
||||
|
||||
return available_apps
|
||||
|
||||
def format_drive(disk_id):
|
||||
# Formats a disk for macOS install
|
||||
# Takes a disk ID
|
||||
# Returns boolean on success status
|
||||
header = f"# Formatting disk{disk_id} for macOS installer #"
|
||||
box_length = len(header)
|
||||
utilities.cls()
|
||||
logging.info("#" * box_length)
|
||||
logging.info(header)
|
||||
logging.info("#" * box_length)
|
||||
logging.info("")
|
||||
#logging.info(f"- Formatting disk{disk_id} for macOS installer")
|
||||
format_process = utilities.elevated(["diskutil", "eraseDisk", "HFS+", "OCLP-Installer", f"disk{disk_id}"])
|
||||
if format_process.returncode == 0:
|
||||
logging.info("- Disk formatted")
|
||||
return True
|
||||
else:
|
||||
logging.info("- Failed to format disk")
|
||||
logging.info(f" Error Code: {format_process.returncode}")
|
||||
input("\nPress Enter to exit")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def list_disk_to_format():
|
||||
all_disks = {}
|
||||
list_disks = {}
|
||||
# TODO: AllDisksAndPartitions is not supported in Snow Leopard and older
|
||||
try:
|
||||
# High Sierra and newer
|
||||
disks = plistlib.loads(subprocess.run("diskutil list -plist physical".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
except ValueError:
|
||||
# Sierra and older
|
||||
disks = plistlib.loads(subprocess.run("diskutil list -plist".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
for disk in disks["AllDisksAndPartitions"]:
|
||||
disk_info = plistlib.loads(subprocess.run(f"diskutil info -plist {disk['DeviceIdentifier']}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
try:
|
||||
all_disks[disk["DeviceIdentifier"]] = {"identifier": disk_info["DeviceNode"], "name": disk_info["MediaName"], "size": disk_info["TotalSize"], "removable": disk_info["Internal"], "partitions": {}}
|
||||
except KeyError:
|
||||
# Avoid crashing with CDs installed
|
||||
continue
|
||||
for disk in all_disks:
|
||||
# Strip disks that are under 14GB (15,032,385,536 bytes)
|
||||
# createinstallmedia isn't great at detecting if a disk has enough space
|
||||
if not any(all_disks[disk]['size'] > 15032385536 for partition in all_disks[disk]):
|
||||
continue
|
||||
# Strip internal disks as well (avoid user formatting their SSD/HDD)
|
||||
# Ensure user doesn't format their boot drive
|
||||
if not any(all_disks[disk]['removable'] is False for partition in all_disks[disk]):
|
||||
continue
|
||||
logging.info(f"disk {disk}: {all_disks[disk]['name']} ({utilities.human_fmt(all_disks[disk]['size'])})")
|
||||
list_disks.update({
|
||||
disk: {
|
||||
"identifier": all_disks[disk]["identifier"],
|
||||
"name": all_disks[disk]["name"],
|
||||
"size": all_disks[disk]["size"],
|
||||
}
|
||||
})
|
||||
return list_disks
|
||||
|
||||
# Create global tmp directory
|
||||
tmp_dir = tempfile.TemporaryDirectory()
|
||||
|
||||
def generate_installer_creation_script(tmp_location, installer_path, disk):
|
||||
# Creates installer.sh to be piped to OCLP-Helper and run as admin
|
||||
# Goals:
|
||||
# - Format provided disk as HFS+ GPT
|
||||
# - Run createinstallmedia on provided disk
|
||||
# Implementing this into a single installer.sh script allows us to only call
|
||||
# OCLP-Helper once to avoid nagging the user about permissions
|
||||
|
||||
additional_args = ""
|
||||
script_location = Path(tmp_location) / Path("Installer.sh")
|
||||
|
||||
# Due to a bug in createinstallmedia, running from '/Applications' may sometimes error:
|
||||
# 'Failed to extract AssetData/boot/Firmware/Manifests/InstallerBoot/*'
|
||||
# This affects native Macs as well even when manually invoking createinstallmedia
|
||||
|
||||
# To resolve, we'll copy into our temp directory and run from there
|
||||
|
||||
# Create a new tmp directory
|
||||
# Our current one is a disk image, thus CoW will not work
|
||||
global tmp_dir
|
||||
ia_tmp = tmp_dir.name
|
||||
|
||||
logging.info(f"Creating temporary directory at {ia_tmp}")
|
||||
# Delete all files in tmp_dir
|
||||
for file in Path(ia_tmp).glob("*"):
|
||||
subprocess.run(["rm", "-rf", str(file)])
|
||||
|
||||
# Copy installer to tmp (use CoW to avoid extra disk writes)
|
||||
args = ["cp", "-cR", installer_path, ia_tmp]
|
||||
if utilities.check_filesystem_type() != "apfs":
|
||||
# HFS+ disks do not support CoW
|
||||
args[1] = "-R"
|
||||
# Ensure we have enough space for the duplication
|
||||
space_available = utilities.get_free_space()
|
||||
space_needed = Path(ia_tmp).stat().st_size
|
||||
if space_available < space_needed:
|
||||
logging.info("Not enough free space to create installer.sh")
|
||||
logging.info(f"{utilities.human_fmt(space_available)} available, {utilities.human_fmt(space_needed)} required")
|
||||
return False
|
||||
subprocess.run(args)
|
||||
|
||||
# Adjust installer_path to point to the copied installer
|
||||
installer_path = Path(ia_tmp) / Path(Path(installer_path).name)
|
||||
if not Path(installer_path).exists():
|
||||
logging.info(f"Failed to copy installer to {ia_tmp}")
|
||||
return False
|
||||
|
||||
createinstallmedia_path = str(Path(installer_path) / Path("Contents/Resources/createinstallmedia"))
|
||||
plist_path = str(Path(installer_path) / Path("Contents/Info.plist"))
|
||||
if Path(plist_path).exists():
|
||||
plist = plistlib.load(Path(plist_path).open("rb"))
|
||||
if "DTPlatformVersion" in plist:
|
||||
platform_version = plist["DTPlatformVersion"]
|
||||
platform_version = platform_version.split(".")[0]
|
||||
if platform_version[0] == "10":
|
||||
if int(platform_version[1]) < 13:
|
||||
additional_args = f" --applicationpath '{installer_path}'"
|
||||
|
||||
if script_location.exists():
|
||||
script_location.unlink()
|
||||
script_location.touch()
|
||||
|
||||
with script_location.open("w") as script:
|
||||
script.write(f'''#!/bin/bash
|
||||
erase_disk='diskutil eraseDisk HFS+ OCLP-Installer {disk}'
|
||||
if $erase_disk; then
|
||||
"{createinstallmedia_path}" --volume /Volumes/OCLP-Installer --nointeraction{additional_args}
|
||||
fi
|
||||
''')
|
||||
if Path(script_location).exists():
|
||||
return True
|
||||
return False
|
||||
590
resources/macos_installer_handler.py
Normal file
590
resources/macos_installer_handler.py
Normal file
@@ -0,0 +1,590 @@
|
||||
# Handler for macOS installers, both local and remote
|
||||
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import enum
|
||||
import logging
|
||||
|
||||
from data import os_data
|
||||
from resources import network_handler, utilities
|
||||
|
||||
|
||||
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 = "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"
|
||||
CATALOG_URL_VERSION: str = "13"
|
||||
|
||||
tmp_dir = tempfile.TemporaryDirectory()
|
||||
|
||||
|
||||
class InstallerCreation():
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
def install_macOS_installer(self, download_path: str):
|
||||
"""
|
||||
Installs InstallAssistant.pkg
|
||||
"""
|
||||
|
||||
logging.info("- Extracting macOS installer from InstallAssistant.pkg\n This may take some time")
|
||||
args = [
|
||||
"osascript",
|
||||
"-e",
|
||||
f'''do shell script "installer -pkg {Path(download_path)}/InstallAssistant.pkg -target /"'''
|
||||
' with prompt "OpenCore Legacy Patcher needs administrator privileges to add InstallAssistant."'
|
||||
" with administrator privileges"
|
||||
" without altering line endings",
|
||||
]
|
||||
|
||||
result = subprocess.run(args,stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to install InstallAssistant")
|
||||
logging.info(f" Error Code: {result.returncode}")
|
||||
return False
|
||||
|
||||
logging.info("- InstallAssistant installed")
|
||||
return True
|
||||
|
||||
|
||||
def generate_installer_creation_script(self, tmp_location, installer_path, disk):
|
||||
"""
|
||||
Creates installer.sh to be piped to OCLP-Helper and run as admin
|
||||
|
||||
Script includes:
|
||||
- Format provided disk as HFS+ GPT
|
||||
- Run createinstallmedia on provided disk
|
||||
|
||||
Implementing this into a single installer.sh script allows us to only call
|
||||
OCLP-Helper once to avoid nagging the user about permissions
|
||||
|
||||
Parameters:
|
||||
tmp_location (str): Path to temporary directory
|
||||
installer_path (str): Path to InstallAssistant.pkg
|
||||
disk (str): Disk to install to
|
||||
"""
|
||||
|
||||
additional_args = ""
|
||||
script_location = Path(tmp_location) / Path("Installer.sh")
|
||||
|
||||
# Due to a bug in createinstallmedia, running from '/Applications' may sometimes error:
|
||||
# 'Failed to extract AssetData/boot/Firmware/Manifests/InstallerBoot/*'
|
||||
# This affects native Macs as well even when manually invoking createinstallmedia
|
||||
|
||||
# To resolve, we'll copy into our temp directory and run from there
|
||||
|
||||
# Create a new tmp directory
|
||||
# Our current one is a disk image, thus CoW will not work
|
||||
global tmp_dir
|
||||
ia_tmp = tmp_dir.name
|
||||
|
||||
logging.info(f"Creating temporary directory at {ia_tmp}")
|
||||
# Delete all files in tmp_dir
|
||||
for file in Path(ia_tmp).glob("*"):
|
||||
subprocess.run(["rm", "-rf", str(file)])
|
||||
|
||||
# Copy installer to tmp (use CoW to avoid extra disk writes)
|
||||
args = ["cp", "-cR", installer_path, ia_tmp]
|
||||
if utilities.check_filesystem_type() != "apfs":
|
||||
# HFS+ disks do not support CoW
|
||||
args[1] = "-R"
|
||||
|
||||
# Ensure we have enough space for the duplication
|
||||
space_available = utilities.get_free_space()
|
||||
space_needed = Path(ia_tmp).stat().st_size
|
||||
if space_available < space_needed:
|
||||
logging.info("Not enough free space to create installer.sh")
|
||||
logging.info(f"{utilities.human_fmt(space_available)} available, {utilities.human_fmt(space_needed)} required")
|
||||
return False
|
||||
|
||||
subprocess.run(args)
|
||||
|
||||
# Adjust installer_path to point to the copied installer
|
||||
installer_path = Path(ia_tmp) / Path(Path(installer_path).name)
|
||||
if not Path(installer_path).exists():
|
||||
logging.info(f"Failed to copy installer to {ia_tmp}")
|
||||
return False
|
||||
|
||||
createinstallmedia_path = str(Path(installer_path) / Path("Contents/Resources/createinstallmedia"))
|
||||
plist_path = str(Path(installer_path) / Path("Contents/Info.plist"))
|
||||
if Path(plist_path).exists():
|
||||
plist = plistlib.load(Path(plist_path).open("rb"))
|
||||
if "DTPlatformVersion" in plist:
|
||||
platform_version = plist["DTPlatformVersion"]
|
||||
platform_version = platform_version.split(".")[0]
|
||||
if platform_version[0] == "10":
|
||||
if int(platform_version[1]) < 13:
|
||||
additional_args = f" --applicationpath '{installer_path}'"
|
||||
|
||||
if script_location.exists():
|
||||
script_location.unlink()
|
||||
script_location.touch()
|
||||
|
||||
with script_location.open("w") as script:
|
||||
script.write(f'''#!/bin/bash
|
||||
erase_disk='diskutil eraseDisk HFS+ OCLP-Installer {disk}'
|
||||
if $erase_disk; then
|
||||
"{createinstallmedia_path}" --volume /Volumes/OCLP-Installer --nointeraction{additional_args}
|
||||
fi
|
||||
''')
|
||||
if Path(script_location).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_disk_to_format(self):
|
||||
"""
|
||||
List applicable disks for macOS installer creation
|
||||
Only lists disks that are:
|
||||
- 14GB or larger
|
||||
- External
|
||||
|
||||
Current limitations:
|
||||
- Does not support PCIe based SD cards readers
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of disks
|
||||
"""
|
||||
|
||||
all_disks: dict = {}
|
||||
list_disks: dict = {}
|
||||
|
||||
# TODO: AllDisksAndPartitions is not supported in Snow Leopard and older
|
||||
try:
|
||||
# High Sierra and newer
|
||||
disks = plistlib.loads(subprocess.run("diskutil list -plist physical".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
except ValueError:
|
||||
# Sierra and older
|
||||
disks = plistlib.loads(subprocess.run("diskutil list -plist".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
|
||||
for disk in disks["AllDisksAndPartitions"]:
|
||||
disk_info = plistlib.loads(subprocess.run(f"diskutil info -plist {disk['DeviceIdentifier']}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
try:
|
||||
all_disks[disk["DeviceIdentifier"]] = {"identifier": disk_info["DeviceNode"], "name": disk_info["MediaName"], "size": disk_info["TotalSize"], "removable": disk_info["Internal"], "partitions": {}}
|
||||
except KeyError:
|
||||
# Avoid crashing with CDs installed
|
||||
continue
|
||||
|
||||
for disk in all_disks:
|
||||
# Strip disks that are under 14GB (15,032,385,536 bytes)
|
||||
# createinstallmedia isn't great at detecting if a disk has enough space
|
||||
if not any(all_disks[disk]['size'] > 15032385536 for partition in all_disks[disk]):
|
||||
continue
|
||||
# Strip internal disks as well (avoid user formatting their SSD/HDD)
|
||||
# Ensure user doesn't format their boot drive
|
||||
if not any(all_disks[disk]['removable'] is False for partition in all_disks[disk]):
|
||||
continue
|
||||
|
||||
logging.info(f"disk {disk}: {all_disks[disk]['name']} ({utilities.human_fmt(all_disks[disk]['size'])})")
|
||||
list_disks.update({
|
||||
disk: {
|
||||
"identifier": all_disks[disk]["identifier"],
|
||||
"name": all_disks[disk]["name"],
|
||||
"size": all_disks[disk]["size"],
|
||||
}
|
||||
})
|
||||
|
||||
return list_disks
|
||||
|
||||
|
||||
class SeedType(enum.Enum):
|
||||
"""
|
||||
Enum for catalog types
|
||||
"""
|
||||
DeveloperSeed = 0
|
||||
PublicSeed = 1
|
||||
CustomerSeed = 2
|
||||
PublicRelease = 3
|
||||
|
||||
|
||||
class RemoteInstallerCatalog:
|
||||
"""
|
||||
Parses Apple's Software Update catalog and finds all macOS installers.
|
||||
"""
|
||||
|
||||
def __init__(self, seed_override: SeedType = SeedType.PublicRelease):
|
||||
|
||||
self.catalog_url: str = self._construct_catalog_url(seed_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):
|
||||
"""
|
||||
Constructs the catalog URL based on the seed type
|
||||
|
||||
Args:
|
||||
seed_type (SeedType): The seed type to use
|
||||
"""
|
||||
|
||||
|
||||
url: str = ""
|
||||
|
||||
if seed_type == SeedType.DeveloperSeed:
|
||||
url = f"{CATALOG_URL_BASE}-{CATALOG_URL_VERSION}seed-{CATALOG_URL_EXTENSION}"
|
||||
elif seed_type == SeedType.PublicSeed:
|
||||
url = f"{CATALOG_URL_BASE}-{CATALOG_URL_VERSION}beta-{CATALOG_URL_EXTENSION}"
|
||||
elif seed_type == SeedType.CustomerSeed:
|
||||
url = f"{CATALOG_URL_BASE}-{CATALOG_URL_VERSION}customerseed-{CATALOG_URL_EXTENSION}"
|
||||
else:
|
||||
url = f"{CATALOG_URL_BASE}-{CATALOG_URL_EXTENSION}"
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def _fetch_catalog(self):
|
||||
"""
|
||||
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.SESSION.get(self.catalog_url).content)
|
||||
except plistlib.InvalidFileException:
|
||||
return {}
|
||||
|
||||
return catalog
|
||||
|
||||
def _parse_catalog(self):
|
||||
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
|
||||
if "BuildManifest" not in catalog["Products"][product]["ExtendedMetaInfo"]["InstallAssistantPackageIdentifiers"]:
|
||||
continue
|
||||
|
||||
for bm_package in catalog["Products"][product]["Packages"]:
|
||||
if "Info.plist" not in bm_package["URL"]:
|
||||
continue
|
||||
if "InstallInfo.plist" in bm_package["URL"]:
|
||||
continue
|
||||
|
||||
try:
|
||||
build_plist = plistlib.loads(network_handler.SESSION.get(bm_package["URL"]).content)
|
||||
except plistlib.InvalidFileException:
|
||||
continue
|
||||
|
||||
if "MobileAssetProperties" not in build_plist:
|
||||
continue
|
||||
if "SupportedDeviceModels" not in build_plist["MobileAssetProperties"]:
|
||||
continue
|
||||
if "OSVersion" not in build_plist["MobileAssetProperties"]:
|
||||
continue
|
||||
if "Build" not in build_plist["MobileAssetProperties"]:
|
||||
continue
|
||||
|
||||
# Ensure Apple Silicon specific Installers are not listed
|
||||
if "VMM-x86_64" not in build_plist["MobileAssetProperties"]["SupportedDeviceModels"]:
|
||||
continue
|
||||
|
||||
version = build_plist["MobileAssetProperties"]["OSVersion"]
|
||||
build = build_plist["MobileAssetProperties"]["Build"]
|
||||
|
||||
try:
|
||||
catalog_url = build_plist["MobileAssetProperties"]["BridgeVersionInfo"]["CatalogURL"]
|
||||
if "beta" in catalog_url:
|
||||
catalog_url = "PublicSeed"
|
||||
elif "customerseed" in catalog_url:
|
||||
catalog_url = "CustomerSeed"
|
||||
elif "seed" in catalog_url:
|
||||
catalog_url = "DeveloperSeed"
|
||||
else:
|
||||
catalog_url = "Public"
|
||||
except KeyError:
|
||||
# Assume Public if no catalog URL is found
|
||||
catalog_url = "Public"
|
||||
|
||||
download_link = None
|
||||
integrity = None
|
||||
size = None
|
||||
|
||||
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
|
||||
if "Size" not in ia_package:
|
||||
size = 0
|
||||
|
||||
download_link = ia_package["URL"]
|
||||
integrity = ia_package["IntegrityDataURL"]
|
||||
size = ia_package["Size"]
|
||||
|
||||
|
||||
if any([version, build, download_link, size, integrity]) is None:
|
||||
continue
|
||||
|
||||
available_apps.update({
|
||||
product: {
|
||||
"Version": version,
|
||||
"Build": build,
|
||||
"Link": download_link,
|
||||
"Size": size,
|
||||
"integrity": integrity,
|
||||
"Source": "Apple Inc.",
|
||||
"Variant": catalog_url,
|
||||
}
|
||||
})
|
||||
|
||||
available_apps = {k: v for k, v in sorted(available_apps.items(), key=lambda x: x[1]['Version'])}
|
||||
return available_apps
|
||||
|
||||
|
||||
def _list_newest_installers_only(self):
|
||||
"""
|
||||
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"]
|
||||
|
||||
|
||||
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 ["CustomerSeed", "DeveloperSeed", "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 ["CustomerSeed", "DeveloperSeed", "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"])
|
||||
|
||||
# Final passthrough
|
||||
# Remove Betas if there's a non-beta version available
|
||||
for ia in list(newest_apps):
|
||||
if newest_apps[ia]["Variant"] in ["CustomerSeed", "DeveloperSeed", "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 ["CustomerSeed", "DeveloperSeed", "PublicSeed"]:
|
||||
newest_apps.pop(ia)
|
||||
break
|
||||
|
||||
return newest_apps
|
||||
|
||||
|
||||
class LocalInstallerCatalog:
|
||||
"""
|
||||
Finds all macOS installers on the local machine.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.available_apps: dict = self._list_local_macOS_installers()
|
||||
|
||||
|
||||
def _list_local_macOS_installers(self):
|
||||
"""
|
||||
Searches for macOS installers in /Applications
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of macOS installers found on the local machine.
|
||||
|
||||
Example:
|
||||
"Install macOS Big Sur Beta.app": {
|
||||
"Short Name": "Big Sur Beta",
|
||||
"Version": "11.0",
|
||||
"Build": "20A5343i",
|
||||
"Path": "/Applications/Install macOS Big Sur Beta.app",
|
||||
},
|
||||
etc...
|
||||
"""
|
||||
|
||||
application_list: dict = {}
|
||||
|
||||
for application in Path(APPLICATION_SEARCH_PATH).iterdir():
|
||||
# Certain Microsoft Applications have strange permissions disabling us from reading them
|
||||
try:
|
||||
if not (Path(APPLICATION_SEARCH_PATH) / Path(application) / Path("Contents/Resources/createinstallmedia")).exists():
|
||||
continue
|
||||
|
||||
if not (Path(APPLICATION_SEARCH_PATH) / Path(application) / Path("Contents/Info.plist")).exists():
|
||||
continue
|
||||
except PermissionError:
|
||||
continue
|
||||
|
||||
try:
|
||||
application_info_plist = plistlib.load((Path(APPLICATION_SEARCH_PATH) / Path(application) / Path("Contents/Info.plist")).open("rb"))
|
||||
except (PermissionError, TypeError, plistlib.InvalidFileException):
|
||||
continue
|
||||
|
||||
if "DTPlatformVersion" not in application_info_plist:
|
||||
continue
|
||||
if "CFBundleDisplayName" not in application_info_plist:
|
||||
continue
|
||||
|
||||
app_version = application_info_plist["DTPlatformVersion"]
|
||||
clean_name = application_info_plist["CFBundleDisplayName"]
|
||||
|
||||
if "DTSDKBuild" in application_info_plist:
|
||||
app_sdk = application_info_plist["DTSDKBuild"]
|
||||
else:
|
||||
app_sdk = "Unknown"
|
||||
|
||||
# app_version can sometimes report GM instead of the actual version
|
||||
# This is a workaround to get the actual version
|
||||
if app_version.startswith("GM"):
|
||||
try:
|
||||
app_version = int(app_sdk[:2])
|
||||
if app_version < 20:
|
||||
app_version = f"10.{app_version - 4}"
|
||||
else:
|
||||
app_version = f"{app_version - 9}.0"
|
||||
except ValueError:
|
||||
app_version = "Unknown"
|
||||
|
||||
# Check if App Version is High Sierra or newer
|
||||
if os_data.os_conversion.os_to_kernel(app_version) < os_data.os_data.high_sierra:
|
||||
continue
|
||||
|
||||
results = self._parse_sharedsupport_version(Path(APPLICATION_SEARCH_PATH) / Path(application)/ Path("Contents/SharedSupport/SharedSupport.dmg"))
|
||||
if results[0] is not None:
|
||||
app_sdk = results[0]
|
||||
if results[1] is not None:
|
||||
app_version = results[1]
|
||||
|
||||
application_list.update({
|
||||
application: {
|
||||
"Short Name": clean_name,
|
||||
"Version": app_version,
|
||||
"Build": app_sdk,
|
||||
"Path": application,
|
||||
}
|
||||
})
|
||||
|
||||
# Sort Applications by version
|
||||
application_list = {k: v for k, v in sorted(application_list.items(), key=lambda item: item[1]["Version"])}
|
||||
return application_list
|
||||
|
||||
|
||||
def _parse_sharedsupport_version(self, sharedsupport_path: Path):
|
||||
"""
|
||||
Determine true version of macOS installer by parsing SharedSupport.dmg
|
||||
This is required due to Info.plist reporting the application version, not the OS version
|
||||
|
||||
Parameters:
|
||||
sharedsupport_path (Path): Path to SharedSupport.dmg
|
||||
|
||||
Returns:
|
||||
tuple: Tuple containing the build and OS version
|
||||
"""
|
||||
|
||||
detected_build: str = None
|
||||
detected_os: str = None
|
||||
|
||||
if not sharedsupport_path.exists():
|
||||
return (detected_build, detected_os)
|
||||
|
||||
if not sharedsupport_path.name.endswith(".dmg"):
|
||||
return (detected_build, detected_os)
|
||||
|
||||
|
||||
# Create temporary directory to extract SharedSupport.dmg to
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
|
||||
output = subprocess.run(
|
||||
[
|
||||
"hdiutil", "attach", "-noverify", sharedsupport_path,
|
||||
"-mountpoint", tmpdir,
|
||||
"-nobrowse",
|
||||
],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
|
||||
if output.returncode != 0:
|
||||
return (detected_build, detected_os)
|
||||
|
||||
ss_info = Path(SFR_SOFTWARE_UPDATE_PATH)
|
||||
|
||||
if Path(tmpdir / ss_info).exists():
|
||||
plist = plistlib.load((tmpdir / ss_info).open("rb"))
|
||||
if "Assets" in plist:
|
||||
if "Build" in plist["Assets"][0]:
|
||||
detected_build = plist["Assets"][0]["Build"]
|
||||
if "OSVersion" in plist["Assets"][0]:
|
||||
detected_os = plist["Assets"][0]["OSVersion"]
|
||||
|
||||
# Unmount SharedSupport.dmg
|
||||
subprocess.run(["hdiutil", "detach", tmpdir], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
return (detected_build, detected_os)
|
||||
Reference in New Issue
Block a user