mirror of
https://github.com/dortania/OpenCore-Legacy-Patcher.git
synced 2026-06-19 13:50:00 +10:00
Merge branch 'main' into pullrequests/Ausdauersportler/main
This commit is contained in:
+92
-49
@@ -1,27 +1,55 @@
|
||||
# Determine AppleMobileFileIntegrity's OS configuration
|
||||
# Copyright (C) 2022-2023, Mykola Grymalyuk
|
||||
|
||||
import enum
|
||||
from resources import utilities
|
||||
from data import amfi_data
|
||||
|
||||
class amfi_configuration_detection:
|
||||
|
||||
class AmfiConfigDetectLevel(enum.IntEnum):
|
||||
"""
|
||||
Configuration levels used by AmfiConfigurationDetection
|
||||
"""
|
||||
|
||||
NO_CHECK: int = 0
|
||||
LIBRARY_VALIDATION: int = 1 # For Ventura, use LIBRARY_VALIDATION_AND_SIG
|
||||
LIBRARY_VALIDATION_AND_SIG: int = 2
|
||||
ALLOW_ALL: int = 3
|
||||
|
||||
|
||||
class AmfiConfigurationDetection:
|
||||
"""
|
||||
Detect AppleMobileFileIntegrity's OS configuration
|
||||
|
||||
Usage:
|
||||
|
||||
>>> import amfi_detect
|
||||
>>> can_patch = amfi_detect.AmfiConfigurationDetection().check_config(amfi_detect.AmfiConfigDetectLevel.ALLOW_ALL)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.AMFI_ALLOW_TASK_FOR_PID = False
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE = False
|
||||
self.AMFI_LV_ENFORCE_THIRD_PARTY = False
|
||||
self.AMFI_ALLOW_EVERYTHING = False
|
||||
self.SKIP_LIBRARY_VALIDATION = False
|
||||
self.AMFI_ALLOW_TASK_FOR_PID: bool = False
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE: bool = False
|
||||
self.AMFI_LV_ENFORCE_THIRD_PARTY: bool = False
|
||||
self.AMFI_ALLOW_EVERYTHING: bool = False
|
||||
self.SKIP_LIBRARY_VALIDATION: bool = False
|
||||
|
||||
self.boot_args = []
|
||||
self.oclp_args = []
|
||||
self.boot_args: list = []
|
||||
self.oclp_args: list = []
|
||||
|
||||
self.init_nvram_dicts()
|
||||
self._init_nvram_dicts()
|
||||
|
||||
self.parse_amfi_bitmask()
|
||||
self.parse_amfi_boot_args()
|
||||
self.parse_oclp_configuration()
|
||||
self._parse_amfi_bitmask()
|
||||
self._parse_amfi_boot_args()
|
||||
self._parse_oclp_configuration()
|
||||
|
||||
|
||||
def init_nvram_dicts(self):
|
||||
def _init_nvram_dicts(self):
|
||||
"""
|
||||
Initialize the boot-args and OCLP-Settings NVRAM dictionaries
|
||||
"""
|
||||
|
||||
boot_args = utilities.get_nvram("boot-args", decode=True)
|
||||
oclp_args = utilities.get_nvram("OCLP-Settings", "4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102", decode=True)
|
||||
|
||||
@@ -32,40 +60,47 @@ class amfi_configuration_detection:
|
||||
self.oclp_args = oclp_args.split(" ")
|
||||
|
||||
|
||||
def parse_amfi_bitmask(self):
|
||||
# See data/amfi_data.py for more information
|
||||
def _parse_amfi_bitmask(self):
|
||||
"""
|
||||
Parse the AMFI bitmask from boot-args
|
||||
See data/amfi_data.py for more information
|
||||
"""
|
||||
|
||||
amfi_value = 0
|
||||
for arg in self.boot_args:
|
||||
if arg.startswith("amfi="):
|
||||
try:
|
||||
amfi_value = arg.split("=")
|
||||
if len(amfi_value) != 2:
|
||||
return
|
||||
amfi_value = amfi_value[1]
|
||||
if amfi_value.startswith("0x"):
|
||||
amfi_value = int(amfi_value, 16)
|
||||
else:
|
||||
amfi_value = int(amfi_value)
|
||||
except:
|
||||
if not arg.startswith("amfi="):
|
||||
continue
|
||||
try:
|
||||
amfi_value = arg.split("=")
|
||||
if len(amfi_value) != 2:
|
||||
return
|
||||
break
|
||||
amfi_value = amfi_value[1]
|
||||
if amfi_value.startswith("0x"):
|
||||
amfi_value = int(amfi_value, 16)
|
||||
else:
|
||||
amfi_value = int(amfi_value)
|
||||
except:
|
||||
return
|
||||
break
|
||||
|
||||
if amfi_value == 0:
|
||||
return
|
||||
|
||||
if amfi_value & 0x1:
|
||||
self.AMFI_ALLOW_TASK_FOR_PID = True
|
||||
if amfi_value & 0x2:
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE = True
|
||||
if amfi_value & 0x4:
|
||||
self.AMFI_LV_ENFORCE_THIRD_PARTY = True
|
||||
if amfi_value & 0x80:
|
||||
self.AMFI_ALLOW_EVERYTHING = True
|
||||
self.SKIP_LIBRARY_VALIDATION = True
|
||||
self.AMFI_ALLOW_TASK_FOR_PID: bool = amfi_value & amfi_data.AppleMobileFileIntegrity.AMFI_ALLOW_TASK_FOR_PID
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE: bool = amfi_value & amfi_data.AppleMobileFileIntegrity.AMFI_ALLOW_INVALID_SIGNATURE
|
||||
self.AMFI_LV_ENFORCE_THIRD_PARTY: bool = amfi_value & amfi_data.AppleMobileFileIntegrity.AMFI_LV_ENFORCE_THIRD_PARTY
|
||||
|
||||
if amfi_value & amfi_data.AppleMobileFileIntegrity.AMFI_ALLOW_EVERYTHING:
|
||||
self.AMFI_ALLOW_EVERYTHING = True
|
||||
self.SKIP_LIBRARY_VALIDATION = True
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE = True
|
||||
|
||||
|
||||
def parse_amfi_boot_args(self):
|
||||
def _parse_amfi_boot_args(self):
|
||||
"""
|
||||
Parse the AMFI boot-args
|
||||
"""
|
||||
|
||||
for arg in self.boot_args:
|
||||
if arg.startswith("amfi_unrestrict_task_for_pid"):
|
||||
value = arg.split("=")
|
||||
@@ -86,26 +121,34 @@ class amfi_configuration_detection:
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE = True
|
||||
|
||||
|
||||
def parse_oclp_configuration(self):
|
||||
def _parse_oclp_configuration(self):
|
||||
"""
|
||||
Parse the OCLP configuration
|
||||
"""
|
||||
|
||||
if "-allow_amfi" in self.oclp_args:
|
||||
self.SKIP_LIBRARY_VALIDATION = True
|
||||
|
||||
|
||||
def check_config(self, level):
|
||||
# Levels:
|
||||
# - 0: No checks
|
||||
# - 1. Library Validation (Monterey and Older)
|
||||
# - 2. Library Validation and Signature Checks (Ventura and Newer)
|
||||
# - 3. Disable all AMFI checks
|
||||
def check_config(self, level: int):
|
||||
"""
|
||||
Check the AMFI configuration based on provided AMFI level
|
||||
See AmfiConfigLevel enum for valid levels
|
||||
|
||||
if level == 0:
|
||||
Parameters:
|
||||
level (int): The level of AMFI checks to check for
|
||||
|
||||
Returns:
|
||||
bool: True if the AMFI configuration matches the level, False otherwise
|
||||
"""
|
||||
|
||||
if level == AmfiConfigDetectLevel.NO_CHECK:
|
||||
return True
|
||||
|
||||
if level == 1:
|
||||
if level == AmfiConfigDetectLevel.LIBRARY_VALIDATION:
|
||||
return self.SKIP_LIBRARY_VALIDATION
|
||||
if level == 2:
|
||||
if level == AmfiConfigDetectLevel.LIBRARY_VALIDATION_AND_SIG:
|
||||
return bool(self.SKIP_LIBRARY_VALIDATION and self.AMFI_ALLOW_INVALID_SIGNATURE)
|
||||
if level == 3:
|
||||
if level == AmfiConfigDetectLevel.ALLOW_ALL:
|
||||
return self.AMFI_ALLOW_EVERYTHING
|
||||
|
||||
return False
|
||||
+175
-101
@@ -1,116 +1,190 @@
|
||||
import sys
|
||||
from resources import defaults, utilities, validation
|
||||
from resources.sys_patch import sys_patch, sys_patch_auto
|
||||
from resources.build import build
|
||||
from data import model_array
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from resources import defaults, utilities, validation, constants
|
||||
from resources.sys_patch import sys_patch, sys_patch_auto
|
||||
from resources.build import build
|
||||
from data import model_array
|
||||
|
||||
|
||||
# Generic building args
|
||||
class arguments:
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.args = utilities.check_cli_args()
|
||||
|
||||
def parse_arguments(self, settings):
|
||||
self._parse_arguments()
|
||||
|
||||
|
||||
def _parse_arguments(self):
|
||||
"""
|
||||
Parses arguments passed to the patcher
|
||||
"""
|
||||
|
||||
if self.args.validate:
|
||||
validation.validate(settings)
|
||||
elif self.args.build:
|
||||
self._validation_handler()
|
||||
return
|
||||
|
||||
if self.args.build:
|
||||
self._build_handler()
|
||||
return
|
||||
|
||||
if self.args.patch_sys_vol:
|
||||
self._sys_patch_handler()
|
||||
return
|
||||
|
||||
if self.args.unpatch_sys_vol:
|
||||
self._sys_unpatch_handler()
|
||||
return
|
||||
|
||||
if self.args.auto_patch:
|
||||
self._sys_patch_auto_handler()
|
||||
return
|
||||
|
||||
|
||||
def _validation_handler(self):
|
||||
"""
|
||||
Enter validation mode
|
||||
"""
|
||||
|
||||
validation.PatcherValidation(self.constants)
|
||||
|
||||
|
||||
def _sys_patch_handler(self):
|
||||
"""
|
||||
Start root volume patching
|
||||
"""
|
||||
|
||||
logging.info("- Set System Volume patching")
|
||||
if "Library/InstallerSandboxes/" in str(self.constants.payload_path):
|
||||
logging.info("- Running from Installer Sandbox")
|
||||
thread = threading.Thread(target=sys_patch.PatchSysVolume(self.constants.custom_model or self.constants.computer.real_model, self.constants, None).start_patch)
|
||||
thread.start()
|
||||
while thread.is_alive():
|
||||
utilities.block_os_updaters()
|
||||
time.sleep(1)
|
||||
else:
|
||||
sys_patch.PatchSysVolume(self.constants.custom_model or self.constants.computer.real_model, self.constants, None).start_patch()
|
||||
|
||||
|
||||
def _sys_unpatch_handler(self):
|
||||
"""
|
||||
Start root volume unpatching
|
||||
"""
|
||||
logging.info("- Set System Volume unpatching")
|
||||
sys_patch.PatchSysVolume(self.constants.custom_model or self.constants.computer.real_model, self.constants, None).start_unpatch()
|
||||
|
||||
|
||||
def _sys_patch_auto_handler(self):
|
||||
"""
|
||||
Start root volume auto patching
|
||||
"""
|
||||
|
||||
logging.info("- Set Auto patching")
|
||||
sys_patch_auto.AutomaticSysPatch(self.constants).start_auto_patch()
|
||||
|
||||
|
||||
def _build_handler(self):
|
||||
"""
|
||||
Start config building process
|
||||
"""
|
||||
|
||||
if self.args.model:
|
||||
if self.args.model:
|
||||
if self.args.model:
|
||||
logging.info(f"- Using custom model: {self.args.model}")
|
||||
settings.custom_model = self.args.model
|
||||
defaults.generate_defaults(settings.custom_model, False, settings)
|
||||
elif settings.computer.real_model not in model_array.SupportedSMBIOS and settings.allow_oc_everywhere is False:
|
||||
logging.info(
|
||||
"""Your model is not supported by this patcher for running unsupported OSes!"
|
||||
logging.info(f"- Using custom model: {self.args.model}")
|
||||
self.constants.custom_model = self.args.model
|
||||
defaults.GenerateDefaults(self.constants.custom_model, False, self.constants)
|
||||
elif self.constants.computer.real_model not in model_array.SupportedSMBIOS and self.constants.allow_oc_everywhere is False:
|
||||
logging.info(
|
||||
"""Your model is not supported by this patcher for running unsupported OSes!"
|
||||
|
||||
If you plan to create the USB for another machine, please select the "Change Model" option in the menu."""
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info(f"- Using detected model: {settings.computer.real_model}")
|
||||
defaults.generate_defaults(settings.custom_model, True, settings)
|
||||
If you plan to create the USB for another machine, please select the "Change Model" option in the menu."""
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info(f"- Using detected model: {self.constants.computer.real_model}")
|
||||
defaults.GenerateDefaults(self.constants.custom_model, True, self.constants)
|
||||
|
||||
if self.args.disk:
|
||||
logging.info(f"- Install Disk set: {self.args.disk}")
|
||||
settings.disk = self.args.disk
|
||||
if self.args.verbose:
|
||||
logging.info("- Set verbose configuration")
|
||||
settings.verbose_debug = True
|
||||
else:
|
||||
settings.verbose_debug = False # Override Defaults detected
|
||||
if self.args.debug_oc:
|
||||
logging.info("- Set OpenCore DEBUG configuration")
|
||||
settings.opencore_debug = True
|
||||
settings.opencore_build = "DEBUG"
|
||||
if self.args.debug_kext:
|
||||
logging.info("- Set kext DEBUG configuration")
|
||||
settings.kext_debug = True
|
||||
if self.args.hide_picker:
|
||||
logging.info("- Set HidePicker configuration")
|
||||
settings.showpicker = False
|
||||
if self.args.disable_sip:
|
||||
logging.info("- Set Disable SIP configuration")
|
||||
settings.sip_status = False
|
||||
else:
|
||||
settings.sip_status = True # Override Defaults detected
|
||||
if self.args.disable_smb:
|
||||
logging.info("- Set Disable SecureBootModel configuration")
|
||||
settings.secure_status = False
|
||||
else:
|
||||
settings.secure_status = True # Override Defaults detected
|
||||
if self.args.vault:
|
||||
logging.info("- Set Vault configuration")
|
||||
settings.vault = True
|
||||
if self.args.firewire:
|
||||
logging.info("- Set FireWire Boot configuration")
|
||||
settings.firewire_boot = True
|
||||
if self.args.nvme:
|
||||
logging.info("- Set NVMe Boot configuration")
|
||||
settings.nvme_boot = True
|
||||
if self.args.wlan:
|
||||
logging.info("- Set Wake on WLAN configuration")
|
||||
settings.enable_wake_on_wlan = True
|
||||
if self.args.disable_tb:
|
||||
logging.info("- Set Disable Thunderbolt configuration")
|
||||
settings.disable_tb = True
|
||||
if self.args.force_surplus:
|
||||
logging.info("- Forcing SurPlus override configuration")
|
||||
settings.force_surplus = True
|
||||
if self.args.moderate_smbios:
|
||||
logging.info("- Set Moderate SMBIOS Patching configuration")
|
||||
settings.serial_settings = "Moderate"
|
||||
if self.args.smbios_spoof:
|
||||
if self.args.smbios_spoof == "Minimal":
|
||||
settings.serial_settings = "Minimal"
|
||||
elif self.args.smbios_spoof == "Moderate":
|
||||
settings.serial_settings = "Moderate"
|
||||
elif self.args.smbios_spoof == "Advanced":
|
||||
settings.serial_settings = "Advanced"
|
||||
else:
|
||||
logging.info(f"- Unknown SMBIOS arg passed: {self.args.smbios_spoof}")
|
||||
if self.args.disk:
|
||||
logging.info(f"- Install Disk set: {self.args.disk}")
|
||||
self.constants.disk = self.args.disk
|
||||
|
||||
if self.args.support_all:
|
||||
logging.info("- Building for natively supported model")
|
||||
settings.allow_oc_everywhere = True
|
||||
settings.serial_settings = "None"
|
||||
build.build_opencore(settings.custom_model or settings.computer.real_model, settings).build_opencore()
|
||||
elif self.args.patch_sys_vol:
|
||||
logging.info("- Set System Volume patching")
|
||||
if self.args.verbose:
|
||||
logging.info("- Set verbose configuration")
|
||||
self.constants.verbose_debug = True
|
||||
else:
|
||||
self.constants.verbose_debug = False # Override Defaults detected
|
||||
|
||||
if "Library/InstallerSandboxes/" in str(settings.payload_path):
|
||||
logging.info("- Running from Installer Sandbox")
|
||||
thread = threading.Thread(target=sys_patch.PatchSysVolume(settings.custom_model or settings.computer.real_model, settings, None).start_patch)
|
||||
thread.start()
|
||||
while thread.is_alive():
|
||||
utilities.block_os_updaters()
|
||||
time.sleep(1)
|
||||
if self.args.debug_oc:
|
||||
logging.info("- Set OpenCore DEBUG configuration")
|
||||
self.constants.opencore_debug = True
|
||||
self.constants.opencore_build = "DEBUG"
|
||||
|
||||
if self.args.debug_kext:
|
||||
logging.info("- Set kext DEBUG configuration")
|
||||
self.constants.kext_debug = True
|
||||
|
||||
if self.args.hide_picker:
|
||||
logging.info("- Set HidePicker configuration")
|
||||
self.constants.showpicker = False
|
||||
|
||||
if self.args.disable_sip:
|
||||
logging.info("- Set Disable SIP configuration")
|
||||
self.constants.sip_status = False
|
||||
else:
|
||||
self.constants.sip_status = True # Override Defaults detected
|
||||
|
||||
if self.args.disable_smb:
|
||||
logging.info("- Set Disable SecureBootModel configuration")
|
||||
self.constants.secure_status = False
|
||||
else:
|
||||
self.constants.secure_status = True # Override Defaults detected
|
||||
|
||||
if self.args.vault:
|
||||
logging.info("- Set Vault configuration")
|
||||
self.constants.vault = True
|
||||
|
||||
if self.args.firewire:
|
||||
logging.info("- Set FireWire Boot configuration")
|
||||
self.constants.firewire_boot = True
|
||||
|
||||
if self.args.nvme:
|
||||
logging.info("- Set NVMe Boot configuration")
|
||||
self.constants.nvme_boot = True
|
||||
|
||||
if self.args.wlan:
|
||||
logging.info("- Set Wake on WLAN configuration")
|
||||
self.constants.enable_wake_on_wlan = True
|
||||
|
||||
if self.args.disable_tb:
|
||||
logging.info("- Set Disable Thunderbolt configuration")
|
||||
self.constants.disable_tb = True
|
||||
|
||||
if self.args.force_surplus:
|
||||
logging.info("- Forcing SurPlus override configuration")
|
||||
self.constants.force_surplus = True
|
||||
|
||||
if self.args.moderate_smbios:
|
||||
logging.info("- Set Moderate SMBIOS Patching configuration")
|
||||
self.constants.serial_settings = "Moderate"
|
||||
|
||||
if self.args.smbios_spoof:
|
||||
if self.args.smbios_spoof == "Minimal":
|
||||
self.constants.serial_settings = "Minimal"
|
||||
elif self.args.smbios_spoof == "Moderate":
|
||||
self.constants.serial_settings = "Moderate"
|
||||
elif self.args.smbios_spoof == "Advanced":
|
||||
self.constants.serial_settings = "Advanced"
|
||||
else:
|
||||
sys_patch.PatchSysVolume(settings.custom_model or settings.computer.real_model, settings, None).start_patch()
|
||||
elif self.args.unpatch_sys_vol:
|
||||
logging.info("- Set System Volume unpatching")
|
||||
sys_patch.PatchSysVolume(settings.custom_model or settings.computer.real_model, settings, None).start_unpatch()
|
||||
elif self.args.auto_patch:
|
||||
logging.info("- Set Auto patching")
|
||||
sys_patch_auto.AutomaticSysPatch(settings).start_auto_patch()
|
||||
logging.info(f"- Unknown SMBIOS arg passed: {self.args.smbios_spoof}")
|
||||
|
||||
if self.args.support_all:
|
||||
logging.info("- Building for natively supported model")
|
||||
self.constants.allow_oc_everywhere = True
|
||||
self.constants.serial_settings = "None"
|
||||
|
||||
build.build_opencore(self.constants.custom_model or self.constants.computer.real_model, self.constants).build_opencore()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+21
-10
@@ -1,29 +1,40 @@
|
||||
# Parse Commit Info from binary's info.plist
|
||||
# App Structure:
|
||||
# OpenCore-Patcher.app:
|
||||
# Contents:
|
||||
# MacOS:
|
||||
# OpenCore-Patcher
|
||||
# Info.plist
|
||||
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
|
||||
class commit_info:
|
||||
class ParseCommitInfo:
|
||||
|
||||
def __init__(self, binary_path: str):
|
||||
"""
|
||||
Parameters:
|
||||
binary_path (str): Path to binary
|
||||
"""
|
||||
|
||||
def __init__(self, binary_path):
|
||||
self.binary_path = str(binary_path)
|
||||
self.plist_path = self.convert_binary_path_to_plist_path()
|
||||
self.plist_path = self._convert_binary_path_to_plist_path()
|
||||
|
||||
|
||||
def convert_binary_path_to_plist_path(self):
|
||||
def _convert_binary_path_to_plist_path(self):
|
||||
"""
|
||||
Resolve Info.plist path from binary path
|
||||
"""
|
||||
|
||||
if Path(self.binary_path).exists():
|
||||
plist_path = self.binary_path.replace("MacOS/OpenCore-Patcher", "Info.plist")
|
||||
if Path(plist_path).exists() and plist_path.endswith(".plist"):
|
||||
return plist_path
|
||||
return None
|
||||
|
||||
|
||||
def generate_commit_info(self):
|
||||
"""
|
||||
Generate commit info from Info.plist
|
||||
|
||||
Returns:
|
||||
tuple: (Branch, Commit Date, Commit URL)
|
||||
"""
|
||||
|
||||
if self.plist_path:
|
||||
plist_info = plistlib.load(Path(self.plist_path).open("rb"))
|
||||
if "Github" in plist_info:
|
||||
|
||||
@@ -13,7 +13,7 @@ class Constants:
|
||||
def __init__(self):
|
||||
# Patcher Versioning
|
||||
self.patcher_version = "0.6.2" # OpenCore-Legacy-Patcher
|
||||
self.patcher_support_pkg_version = "0.8.3" # PatcherSupportPkg
|
||||
self.patcher_support_pkg_version = "0.8.5" # PatcherSupportPkg
|
||||
self.url_patcher_support_pkg = "https://github.com/dortania/PatcherSupportPkg/releases/download/"
|
||||
self.nightly_url_patcher_support_pkg = "https://nightly.link/dortania/PatcherSupportPkg/workflows/build/master/"
|
||||
self.discord_link = "https://discord.gg/rqdPgH8xSN"
|
||||
@@ -44,7 +44,7 @@ class Constants:
|
||||
self.cpufriend_version = "1.2.6" # CPUFriend
|
||||
self.bluetool_version = "2.6.4" # BlueToolFixup (BrcmPatchRAM)
|
||||
self.cslvfixup_version = "2.6.1" # CSLVFixup
|
||||
self.autopkg_version = "1.0.1" # AutoPkgInstaller
|
||||
self.autopkg_version = "1.0.2" # AutoPkgInstaller
|
||||
self.cryptexfixup_version = "1.0.1" # CryptexFixup
|
||||
|
||||
## Apple
|
||||
|
||||
+76
-37
@@ -1,42 +1,60 @@
|
||||
# Generate Default Data
|
||||
from resources import utilities, device_probe, generate_smbios, global_settings
|
||||
from data import smbios_data, cpu_data, os_data
|
||||
import subprocess
|
||||
|
||||
from resources import (
|
||||
utilities,
|
||||
device_probe,
|
||||
generate_smbios,
|
||||
global_settings,
|
||||
constants
|
||||
)
|
||||
from data import (
|
||||
smbios_data,
|
||||
cpu_data,
|
||||
os_data
|
||||
)
|
||||
|
||||
class generate_defaults:
|
||||
|
||||
def __init__(self, model, host_is_target, settings):
|
||||
self.model = model
|
||||
self.constants = settings
|
||||
self.host_is_target = host_is_target
|
||||
class GenerateDefaults:
|
||||
|
||||
def __init__(self, model: str, host_is_target: bool, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.model: str = model
|
||||
|
||||
self.host_is_target: bool = host_is_target
|
||||
|
||||
# Reset Variables
|
||||
self.constants.sip_status = True
|
||||
self.constants.secure_status = False
|
||||
self.constants.disable_cs_lv = False
|
||||
self.constants.disable_amfi = False
|
||||
self.constants.fu_status = True
|
||||
self.constants.fu_arguments = None
|
||||
self.constants.sip_status: bool = True
|
||||
self.constants.secure_status: bool = False
|
||||
self.constants.disable_cs_lv: bool = False
|
||||
self.constants.disable_amfi: bool = False
|
||||
self.constants.fu_status: bool = True
|
||||
|
||||
self.constants.custom_serial_number = ""
|
||||
self.constants.custom_board_serial_number = ""
|
||||
self.constants.fu_arguments: str = None
|
||||
|
||||
self.constants.custom_serial_number: str = ""
|
||||
self.constants.custom_board_serial_number: str = ""
|
||||
|
||||
if self.host_is_target is True:
|
||||
for gpu in self.constants.computer.gpus:
|
||||
if gpu.device_id_unspoofed == -1:
|
||||
gpu.device_id_unspoofed = gpu.device_id
|
||||
if gpu.vendor_id_unspoofed == -1:
|
||||
gpu.vendor_id_unspoofed = gpu.vendor_id
|
||||
|
||||
for gpu in self.constants.computer.gpus:
|
||||
if gpu.device_id_unspoofed == -1:
|
||||
gpu.device_id_unspoofed = gpu.device_id
|
||||
if gpu.vendor_id_unspoofed == -1:
|
||||
gpu.vendor_id_unspoofed = gpu.vendor_id
|
||||
|
||||
self.general_probe()
|
||||
self.nvram_probe()
|
||||
self.gpu_probe()
|
||||
self.networking_probe()
|
||||
self.misc_hardwares_probe()
|
||||
self.smbios_probe()
|
||||
self._general_probe()
|
||||
self._nvram_probe()
|
||||
self._gpu_probe()
|
||||
self._networking_probe()
|
||||
self._misc_hardwares_probe()
|
||||
self._smbios_probe()
|
||||
|
||||
|
||||
def general_probe(self):
|
||||
def _general_probe(self):
|
||||
"""
|
||||
General probe for data
|
||||
"""
|
||||
|
||||
if "Book" in self.model:
|
||||
self.constants.set_content_caching = False
|
||||
@@ -46,11 +64,11 @@ class generate_defaults:
|
||||
if self.model in ["MacBookPro8,2", "MacBookPro8,3"]:
|
||||
# Users disabling TS2 most likely have a faulty dGPU
|
||||
# users can override this in settings
|
||||
ts2_status = global_settings.global_settings().read_property("MacBookPro_TeraScale_2_Accel")
|
||||
ts2_status = global_settings.GlobalEnviromentSettings().read_property("MacBookPro_TeraScale_2_Accel")
|
||||
if ts2_status is True:
|
||||
self.constants.allow_ts2_accel = True
|
||||
else:
|
||||
global_settings.global_settings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
global_settings.GlobalEnviromentSettings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
self.constants.allow_ts2_accel = False
|
||||
|
||||
if self.model in smbios_data.smbios_dictionary:
|
||||
@@ -67,14 +85,19 @@ class generate_defaults:
|
||||
# Check if running in RecoveryOS
|
||||
self.constants.recovery_status = utilities.check_recovery()
|
||||
|
||||
if global_settings.global_settings().read_property("Force_Web_Drivers") is True:
|
||||
if global_settings.GlobalEnviromentSettings().read_property("Force_Web_Drivers") is True:
|
||||
self.constants.force_nv_web = True
|
||||
|
||||
result = global_settings.global_settings().read_property("ShouldNukeKDKs")
|
||||
result = global_settings.GlobalEnviromentSettings().read_property("ShouldNukeKDKs")
|
||||
if result is False:
|
||||
self.constants.should_nuke_kdks = False
|
||||
|
||||
def smbios_probe(self):
|
||||
|
||||
def _smbios_probe(self):
|
||||
"""
|
||||
SMBIOS specific probe
|
||||
"""
|
||||
|
||||
if not self.host_is_target:
|
||||
if self.model in ["MacPro4,1", "MacPro5,1"]:
|
||||
# Allow H.265 on AMD
|
||||
@@ -105,7 +128,11 @@ class generate_defaults:
|
||||
self.constants.force_vmm = False
|
||||
|
||||
|
||||
def nvram_probe(self):
|
||||
def _nvram_probe(self):
|
||||
"""
|
||||
NVRAM specific probe
|
||||
"""
|
||||
|
||||
if not self.host_is_target:
|
||||
return
|
||||
|
||||
@@ -126,7 +153,11 @@ class generate_defaults:
|
||||
self.constants.custom_cpu_model_value = custom_cpu_model_value.split("%00")[0]
|
||||
|
||||
|
||||
def networking_probe(self):
|
||||
def _networking_probe(self):
|
||||
"""
|
||||
Networking specific probe
|
||||
"""
|
||||
|
||||
if self.host_is_target:
|
||||
if not (
|
||||
(
|
||||
@@ -163,7 +194,11 @@ class generate_defaults:
|
||||
self.constants.fu_status = True
|
||||
self.constants.fu_arguments = " -disable_sidecar_mac"
|
||||
|
||||
def misc_hardwares_probe(self):
|
||||
|
||||
def _misc_hardwares_probe(self):
|
||||
"""
|
||||
Misc probe
|
||||
"""
|
||||
if self.host_is_target:
|
||||
if self.constants.computer.usb_controllers:
|
||||
if self.model in smbios_data.smbios_dictionary:
|
||||
@@ -176,7 +211,11 @@ class generate_defaults:
|
||||
break
|
||||
|
||||
|
||||
def gpu_probe(self):
|
||||
def _gpu_probe(self):
|
||||
"""
|
||||
Graphics specific probe
|
||||
"""
|
||||
|
||||
gpu_dict = []
|
||||
if self.host_is_target:
|
||||
gpu_dict = self.constants.computer.gpus
|
||||
|
||||
@@ -6,32 +6,42 @@
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
class global_settings:
|
||||
|
||||
class GlobalEnviromentSettings:
|
||||
"""
|
||||
Library for querying and writing global enviroment settings
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.file_name = ".com.dortania.opencore-legacy-patcher.plist"
|
||||
self.global_settings_folder = "/Users/Shared"
|
||||
self.global_settings_plist = f"{self.global_settings_folder}/{self.file_name}"
|
||||
self.generate_settings_file()
|
||||
self.convert_defaults_to_global_settings()
|
||||
self.file_name: str = ".com.dortania.opencore-legacy-patcher.plist"
|
||||
self.global_settings_folder: str = "/Users/Shared"
|
||||
self.global_settings_plist: str = f"{self.global_settings_folder}/{self.file_name}"
|
||||
|
||||
def generate_settings_file(self):
|
||||
if Path(self.global_settings_plist).exists():
|
||||
return
|
||||
try:
|
||||
plistlib.dump({"Developed by Dortania": True,}, Path(self.global_settings_plist).open("wb"))
|
||||
except PermissionError:
|
||||
logging.info("- Permission error: Unable to write to global settings file")
|
||||
self._generate_settings_file()
|
||||
self._convert_defaults_to_global_settings()
|
||||
self._fix_file_permission()
|
||||
|
||||
|
||||
def read_property(self, property_name: str):
|
||||
"""
|
||||
Reads a property from the global settings file
|
||||
"""
|
||||
|
||||
def read_property(self, property_name):
|
||||
if Path(self.global_settings_plist).exists():
|
||||
plist = plistlib.load(Path(self.global_settings_plist).open("rb"))
|
||||
if property_name in plist:
|
||||
return plist[property_name]
|
||||
return None
|
||||
|
||||
def write_property(self, property_name, property_value):
|
||||
|
||||
def write_property(self, property_name: str, property_value):
|
||||
"""
|
||||
Writes a property to the global settings file
|
||||
"""
|
||||
|
||||
if Path(self.global_settings_plist).exists():
|
||||
plist = plistlib.load(Path(self.global_settings_plist).open("rb"))
|
||||
plist[property_name] = property_value
|
||||
@@ -41,7 +51,20 @@ class global_settings:
|
||||
logging.info("- Failed to write to global settings file")
|
||||
|
||||
|
||||
def convert_defaults_to_global_settings(self):
|
||||
def _generate_settings_file(self):
|
||||
if Path(self.global_settings_plist).exists():
|
||||
return
|
||||
try:
|
||||
plistlib.dump({"Developed by Dortania": True,}, Path(self.global_settings_plist).open("wb"))
|
||||
except PermissionError:
|
||||
logging.info("- Permission error: Unable to write to global settings file")
|
||||
|
||||
|
||||
def _convert_defaults_to_global_settings(self):
|
||||
"""
|
||||
Converts legacy defaults to global settings
|
||||
"""
|
||||
|
||||
defaults_path = "~/Library/Preferences/com.dortania.opencore-legacy-patcher.plist"
|
||||
defaults_path = Path(defaults_path).expanduser()
|
||||
|
||||
@@ -60,4 +83,23 @@ class global_settings:
|
||||
try:
|
||||
Path(defaults_path).unlink()
|
||||
except PermissionError:
|
||||
logging.info("- Permission error: Unable to delete defaults plist")
|
||||
logging.info("- Permission error: Unable to delete defaults plist")
|
||||
|
||||
|
||||
def _fix_file_permission(self):
|
||||
"""
|
||||
Fixes file permission for log file
|
||||
|
||||
If OCLP was invoked as root, file permission will only allow root to write to settings file
|
||||
This in turn breaks normal OCLP execution to write to settings file
|
||||
"""
|
||||
|
||||
if os.geteuid() != 0:
|
||||
return
|
||||
|
||||
# Set file permission to allow any user to write to log file
|
||||
result = subprocess.run(["chmod", "777", self.global_settings_plist], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
logging.warning("- Failed to fix settings file permissions:")
|
||||
if result.stderr:
|
||||
logging.warning(result.stderr.decode("utf-8"))
|
||||
+191
-65
@@ -3,28 +3,51 @@
|
||||
# Currently Work in Progress
|
||||
|
||||
import plistlib
|
||||
import wx
|
||||
import sys
|
||||
import webbrowser
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import wx.adv
|
||||
from wx.lib.agw import hyperlink
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
import webbrowser
|
||||
|
||||
import time
|
||||
|
||||
import binascii
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
import py_sip_xnu
|
||||
import logging
|
||||
|
||||
from resources import constants, defaults, install, installer, utilities, run, generate_smbios, updates, integrity_verification, global_settings, kdk_handler, network_handler
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
import wx
|
||||
import wx.adv
|
||||
from wx.lib.agw import hyperlink
|
||||
|
||||
import py_sip_xnu
|
||||
|
||||
from resources import (
|
||||
constants,
|
||||
defaults,
|
||||
install,
|
||||
utilities,
|
||||
generate_smbios,
|
||||
updates,
|
||||
integrity_verification,
|
||||
global_settings,
|
||||
kdk_handler,
|
||||
network_handler,
|
||||
macos_installer_handler
|
||||
)
|
||||
|
||||
from resources.sys_patch import sys_patch_detect, sys_patch
|
||||
from resources.build import build
|
||||
from data import model_array, os_data, smbios_data, sip_data, cpu_data
|
||||
from resources.gui import menu_redirect, gui_help
|
||||
|
||||
from data import model_array, os_data, smbios_data, sip_data, cpu_data
|
||||
|
||||
|
||||
|
||||
class wx_python_gui:
|
||||
def __init__(self, versions, frame=None, frame_modal=None):
|
||||
@@ -232,7 +255,7 @@ class wx_python_gui:
|
||||
if local_build_date <= installed_build_date:
|
||||
return False
|
||||
|
||||
elif updates.check_binary_updates(self.constants).check_if_build_newer(local_version, application_version) is False:
|
||||
elif updates.CheckBinaryUpdates(self.constants)._check_if_build_newer(local_version, application_version) is False:
|
||||
return False
|
||||
|
||||
# Ask user if they want to move the application to the Applications folder
|
||||
@@ -283,11 +306,11 @@ class wx_python_gui:
|
||||
return
|
||||
|
||||
did_find_update = False
|
||||
ignore_updates = global_settings.global_settings().read_property("IgnoreAppUpdates")
|
||||
ignore_updates = global_settings.GlobalEnviromentSettings().read_property("IgnoreAppUpdates")
|
||||
if ignore_updates is not True:
|
||||
self.constants.ignore_updates = False
|
||||
self.constants.has_checked_updates = True
|
||||
dict = updates.check_binary_updates(self.constants).check_binary_updates()
|
||||
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
|
||||
if dict:
|
||||
for entry in dict:
|
||||
version = dict[entry]["Version"]
|
||||
@@ -307,7 +330,7 @@ class wx_python_gui:
|
||||
elif response == wx.ID_NO:
|
||||
logging.info("- Setting IgnoreAppUpdates to True")
|
||||
self.constants.ignore_updates = True
|
||||
global_settings.global_settings().write_property("IgnoreAppUpdates", True)
|
||||
global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", True)
|
||||
else:
|
||||
self.constants.ignore_updates = True
|
||||
logging.info("- Ignoring App Updates due to defaults")
|
||||
@@ -565,10 +588,10 @@ class wx_python_gui:
|
||||
if self.constants.start_build_install is True:
|
||||
self.build_install_menu()
|
||||
elif "--gui_patch" in sys.argv:
|
||||
self.patches = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.patches = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.root_patch_start()
|
||||
elif "--gui_unpatch" in sys.argv:
|
||||
self.patches = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.patches = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.root_patch_revert()
|
||||
self.finished_auto_patch = True
|
||||
self.constants.start_build_install = False
|
||||
@@ -1055,7 +1078,7 @@ class wx_python_gui:
|
||||
)
|
||||
self.subheader.Centre(wx.HORIZONTAL)
|
||||
|
||||
patches = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
patches = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.patches = patches
|
||||
can_unpatch = patches["Validation: Unpatching Possible"]
|
||||
if not any(not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True for patch in patches):
|
||||
@@ -1263,24 +1286,36 @@ class wx_python_gui:
|
||||
self.pulse_alternative(self.progress_bar)
|
||||
wx.GetApp().Yield()
|
||||
|
||||
self.progress_bar.Hide()
|
||||
|
||||
if self.patches["Settings: Kernel Debug Kit missing"] is True:
|
||||
# Download KDK (if needed)
|
||||
self.subheader.SetLabel("Downloading Kernel Debug Kit")
|
||||
self.subheader.Centre(wx.HORIZONTAL)
|
||||
self.developer_note.SetLabel("Starting shortly")
|
||||
|
||||
wx.GetApp().Yield()
|
||||
|
||||
kdk_result = False
|
||||
kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version)
|
||||
if kdk_obj.success is True:
|
||||
kdk_download_obj = kdk_obj.retrieve_download()
|
||||
self.kdk_obj = None
|
||||
def kdk_thread_spawn():
|
||||
self.kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version)
|
||||
|
||||
kdk_thread = threading.Thread(target=kdk_thread_spawn)
|
||||
kdk_thread.start()
|
||||
|
||||
while kdk_thread.is_alive():
|
||||
self.pulse_alternative(self.progress_bar)
|
||||
wx.GetApp().Yield()
|
||||
|
||||
self.progress_bar.Hide()
|
||||
|
||||
if self.kdk_obj.success is True:
|
||||
kdk_download_obj = self.kdk_obj.retrieve_download()
|
||||
if not kdk_download_obj:
|
||||
kdk_result = True
|
||||
else:
|
||||
kdk_download_obj.download()
|
||||
|
||||
self.header.SetLabel(f"Downloading KDK Build: {kdk_obj.kdk_url_build}")
|
||||
self.header.SetLabel(f"Downloading KDK Build: {self.kdk_obj.kdk_url_build}")
|
||||
self.header.Centre(wx.HORIZONTAL)
|
||||
|
||||
self.progress_bar.SetValue(0)
|
||||
@@ -1304,7 +1339,7 @@ class wx_python_gui:
|
||||
)
|
||||
self.developer_note.Centre(wx.HORIZONTAL)
|
||||
|
||||
self.progress_bar.SetValue(kdk_download_obj.get_percent())
|
||||
self.progress_bar.SetValue(int(kdk_download_obj.get_percent()))
|
||||
|
||||
wx.GetApp().Yield()
|
||||
time.sleep(0.1)
|
||||
@@ -1314,12 +1349,12 @@ class wx_python_gui:
|
||||
logging.error(kdk_download_obj.error_msg)
|
||||
error_msg = kdk_download_obj.error_msg
|
||||
else:
|
||||
kdk_result = kdk_obj.validate_kdk_checksum()
|
||||
error_msg = kdk_obj.error_msg
|
||||
kdk_result = self.kdk_obj.validate_kdk_checksum()
|
||||
error_msg = self.kdk_obj.error_msg
|
||||
else:
|
||||
logging.error("Failed to download KDK")
|
||||
logging.error(kdk_obj.error_msg)
|
||||
error_msg = kdk_obj.error_msg
|
||||
logging.error(self.kdk_obj.error_msg)
|
||||
error_msg = self.kdk_obj.error_msg
|
||||
|
||||
if kdk_result is False:
|
||||
# Create popup window to inform user of error
|
||||
@@ -1617,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)
|
||||
@@ -1655,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"]:
|
||||
@@ -1823,7 +1860,7 @@ class wx_python_gui:
|
||||
)
|
||||
self.download_label_2.Centre(wx.HORIZONTAL)
|
||||
|
||||
self.download_progress.SetValue(ia_download.get_percent())
|
||||
self.download_progress.SetValue(int(ia_download.get_percent()))
|
||||
|
||||
wx.GetApp().Yield()
|
||||
time.sleep(0.1)
|
||||
@@ -1929,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():
|
||||
@@ -1966,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()
|
||||
@@ -2070,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:
|
||||
@@ -2153,7 +2190,7 @@ class wx_python_gui:
|
||||
self.developer_note_label_2.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Progress Bar
|
||||
max_file_size = 1024 * 1024 * 1024 * 18 # 18GB, best guess for installer + chainloaded packages
|
||||
max_file_size = 19000 # Best guess for installer + chainloaded packages
|
||||
self.progress_bar = wx.Gauge(self.frame, range=max_file_size, size=(-1, 20))
|
||||
self.progress_bar.SetPosition(
|
||||
wx.Point(
|
||||
@@ -2225,7 +2262,7 @@ class wx_python_gui:
|
||||
output = float(utilities.monitor_disk_output(disk))
|
||||
bytes_written = output - default_output
|
||||
if install_thread.is_alive():
|
||||
self.progress_bar.SetValue(bytes_written)
|
||||
self.progress_bar.SetValue(int(bytes_written))
|
||||
self.progress_label.SetLabel(f"Bytes Written: {round(bytes_written, 2)}MB")
|
||||
wx.GetApp().Yield()
|
||||
else:
|
||||
@@ -2270,12 +2307,15 @@ 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()
|
||||
args = [self.constants.oclp_helper_path, "/bin/sh", self.constants.installer_sh_path]
|
||||
output, error, returncode = run.Run()._stream_output(comm=args)
|
||||
args = [self.constants.oclp_helper_path, "/bin/sh", self.constants.installer_sh_path]
|
||||
result = subprocess.run(args, capture_output=True, text=True)
|
||||
output = result.stdout
|
||||
error = result.stderr if result.stderr else ""
|
||||
|
||||
if "Install media now available at" in output:
|
||||
logging.info("- Successfully created macOS installer")
|
||||
while self.download_thread.is_alive():
|
||||
@@ -2325,18 +2365,104 @@ class wx_python_gui:
|
||||
subprocess.run(["ditto", "-V", "-x", "-k", "--sequesterRsrc", "--rsrc", self.constants.installer_pkg_zip_path, self.constants.payload_path])
|
||||
|
||||
|
||||
def _kdk_chainload(self, build: str, version: str, download_dir: str):
|
||||
"""
|
||||
Download the correct KDK to be chainloaded in the macOS installer
|
||||
|
||||
Parameters
|
||||
build (str): The build number of the macOS installer (e.g. 20A5343j)
|
||||
version (str): The version of the macOS installer (e.g. 11.0.1)
|
||||
"""
|
||||
|
||||
kdk_dmg_path = Path(download_dir) / "KDK.dmg"
|
||||
kdk_pkg_path = Path(download_dir) / "KDK.pkg"
|
||||
|
||||
if kdk_dmg_path.exists():
|
||||
kdk_dmg_path.unlink()
|
||||
if kdk_pkg_path.exists():
|
||||
kdk_pkg_path.unlink()
|
||||
|
||||
logging.info("- Initiating KDK download")
|
||||
logging.info(f" - Build: {build}")
|
||||
logging.info(f" - Version: {version}")
|
||||
logging.info(f" - Working Directory: {download_dir}")
|
||||
|
||||
kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, build, version, ignore_installed=True)
|
||||
if kdk_obj.success is False:
|
||||
logging.info("- Failed to retrieve KDK")
|
||||
logging.info(kdk_obj.error_msg)
|
||||
return
|
||||
|
||||
kdk_download_obj = kdk_obj.retrieve_download(override_path=kdk_dmg_path)
|
||||
if kdk_download_obj is None:
|
||||
logging.info("- Failed to retrieve KDK")
|
||||
logging.info(kdk_obj.error_msg)
|
||||
|
||||
# Check remaining disk space before downloading
|
||||
space = utilities.get_free_space(download_dir)
|
||||
if space < (kdk_obj.kdk_url_expected_size * 2):
|
||||
logging.info("- Not enough disk space to download and install KDK")
|
||||
logging.info(f"- Attempting to download locally first")
|
||||
if space < kdk_obj.kdk_url_expected_size:
|
||||
logging.info("- Not enough disk space to install KDK, skipping")
|
||||
return
|
||||
# Ideally we'd download the KDK onto the disk to display progress in the UI
|
||||
# However we'll just download to our temp directory and move it to the target disk
|
||||
kdk_dmg_path = self.constants.kdk_download_path
|
||||
|
||||
kdk_download_obj.download(spawn_thread=False)
|
||||
if kdk_download_obj.download_complete is False:
|
||||
logging.info("- Failed to download KDK")
|
||||
logging.info(kdk_download_obj.error_msg)
|
||||
return
|
||||
|
||||
if not kdk_dmg_path.exists():
|
||||
logging.info(f"- KDK missing: {kdk_dmg_path}")
|
||||
return
|
||||
|
||||
# Now that we have a KDK, extract it to get the pkg
|
||||
with tempfile.TemporaryDirectory() as mount_point:
|
||||
logging.info("- Mounting KDK")
|
||||
result = subprocess.run(["hdiutil", "attach", kdk_dmg_path, "-mountpoint", mount_point, "-nobrowse"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to mount KDK")
|
||||
logging.info(result.stdout.decode("utf-8"))
|
||||
return
|
||||
|
||||
logging.info("- Copying KDK")
|
||||
subprocess.run(["cp", "-r", f"{mount_point}/KernelDebugKit.pkg", kdk_pkg_path])
|
||||
|
||||
logging.info("- Unmounting KDK")
|
||||
result = subprocess.run(["hdiutil", "detach", mount_point], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to unmount KDK")
|
||||
logging.info(result.stdout.decode("utf-8"))
|
||||
return
|
||||
|
||||
logging.info("- Removing KDK Disk Image")
|
||||
kdk_dmg_path.unlink()
|
||||
|
||||
|
||||
def install_installer_pkg(self, disk):
|
||||
disk = disk + "s2" # ESP sits at 1, and we know macOS will have created the main partition at 2
|
||||
if Path(self.constants.installer_pkg_path).exists():
|
||||
path = utilities.grab_mount_point_from_disk(disk)
|
||||
if Path(path + "/System/Library/CoreServices/SystemVersion.plist").exists():
|
||||
os_version = plistlib.load(Path(path + "/System/Library/CoreServices/SystemVersion.plist").open("rb"))
|
||||
kernel_version = os_data.os_conversion.os_to_kernel(os_version["ProductVersion"])
|
||||
if int(kernel_version) >= os_data.os_data.big_sur:
|
||||
subprocess.run(["mkdir", "-p", f"{path}/Library/Packages/"])
|
||||
subprocess.run(["cp", "-r", self.constants.installer_pkg_path, f"{path}/Library/Packages/"])
|
||||
else:
|
||||
logging.info("- Installer unsupported, requires Big Sur or newer")
|
||||
|
||||
if not Path(self.constants.installer_pkg_path).exists():
|
||||
return
|
||||
|
||||
path = utilities.grab_mount_point_from_disk(disk)
|
||||
if not Path(path + "/System/Library/CoreServices/SystemVersion.plist").exists():
|
||||
return
|
||||
|
||||
os_version = plistlib.load(Path(path + "/System/Library/CoreServices/SystemVersion.plist").open("rb"))
|
||||
kernel_version = os_data.os_conversion.os_to_kernel(os_version["ProductVersion"])
|
||||
if int(kernel_version) < os_data.os_data.big_sur:
|
||||
logging.info("- Installer unsupported, requires Big Sur or newer")
|
||||
return
|
||||
|
||||
subprocess.run(["mkdir", "-p", f"{path}/Library/Packages/"])
|
||||
subprocess.run(["cp", "-r", self.constants.installer_pkg_path, f"{path}/Library/Packages/"])
|
||||
|
||||
self._kdk_chainload(os_version["ProductBuildVersion"], os_version["ProductVersion"], Path(path + "/Library/Packages/"))
|
||||
|
||||
|
||||
def settings_menu(self, event=None):
|
||||
@@ -2523,11 +2649,11 @@ class wx_python_gui:
|
||||
if user_choice == self.computer.real_model:
|
||||
logging.info(f"Using Real Model: {user_choice}")
|
||||
self.constants.custom_model = None
|
||||
defaults.generate_defaults(self.computer.real_model, True, self.constants)
|
||||
defaults.GenerateDefaults(self.computer.real_model, True, self.constants)
|
||||
else:
|
||||
logging.info(f"Using Custom Model: {user_choice}")
|
||||
self.constants.custom_model = user_choice
|
||||
defaults.generate_defaults(self.constants.custom_model, False, self.constants)
|
||||
defaults.GenerateDefaults(self.constants.custom_model, False, self.constants)
|
||||
# Reload Settings
|
||||
self.settings_menu(None)
|
||||
|
||||
@@ -2864,7 +2990,7 @@ class wx_python_gui:
|
||||
else:
|
||||
logging.info("Nuke KDKs disabled")
|
||||
self.constants.should_nuke_kdks = False
|
||||
global_settings.global_settings().write_property("ShouldNukeKDKs", self.constants.should_nuke_kdks)
|
||||
global_settings.GlobalEnviromentSettings().write_property("ShouldNukeKDKs", self.constants.should_nuke_kdks)
|
||||
|
||||
def disable_library_validation_click(self, event):
|
||||
if self.disable_library_validation_checkbox.GetValue():
|
||||
@@ -2887,9 +3013,9 @@ class wx_python_gui:
|
||||
def set_ignore_app_updates_click(self, event):
|
||||
self.constants.ignore_updates = self.set_ignore_app_updates_checkbox.GetValue()
|
||||
if self.constants.ignore_updates is True:
|
||||
global_settings.global_settings().write_property("IgnoreAppUpdates", True)
|
||||
global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", True)
|
||||
else:
|
||||
global_settings.global_settings().write_property("IgnoreAppUpdates", False)
|
||||
global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", False)
|
||||
|
||||
def firewire_click(self, event=None):
|
||||
if self.firewire_boot_checkbox.GetValue():
|
||||
@@ -2974,21 +3100,21 @@ class wx_python_gui:
|
||||
def ts2_accel_click(self, event=None):
|
||||
if self.set_terascale_accel_checkbox.GetValue():
|
||||
logging.info("TS2 Acceleration Enabled")
|
||||
global_settings.global_settings().write_property("MacBookPro_TeraScale_2_Accel", True)
|
||||
global_settings.GlobalEnviromentSettings().write_property("MacBookPro_TeraScale_2_Accel", True)
|
||||
self.constants.allow_ts2_accel = True
|
||||
else:
|
||||
logging.info("TS2 Acceleration Disabled")
|
||||
global_settings.global_settings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
global_settings.GlobalEnviromentSettings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
self.constants.allow_ts2_accel = False
|
||||
|
||||
def force_web_drivers_click(self, event=None):
|
||||
if self.force_web_drivers_checkbox.GetValue():
|
||||
logging.info("Force Web Drivers Enabled")
|
||||
global_settings.global_settings().write_property("Force_Web_Drivers", True)
|
||||
global_settings.GlobalEnviromentSettings().write_property("Force_Web_Drivers", True)
|
||||
self.constants.force_nv_web = True
|
||||
else:
|
||||
logging.info("Force Web Drivers Disabled")
|
||||
global_settings.global_settings().write_property("Force_Web_Drivers", False)
|
||||
global_settings.GlobalEnviromentSettings().write_property("Force_Web_Drivers", False)
|
||||
self.constants.force_nv_web = False
|
||||
|
||||
def windows_gmux_click(self, event=None):
|
||||
@@ -3698,19 +3824,19 @@ OpenCore Legacy Patcher by default knows the most ideal
|
||||
self.subheader_4.SetPosition(wx.Point(0, self.subheader2_2.GetPosition().y + self.subheader2_2.GetSize().height+ 5))
|
||||
self.subheader_4.Centre(wx.HORIZONTAL)
|
||||
|
||||
is_dark_menu_bar = subprocess.run(["defaults", "read", "-g", "Moraea_DarkMenuBar"], stdout=subprocess.PIPE).stdout.decode("utf-8").strip()
|
||||
is_dark_menu_bar = subprocess.run(["defaults", "read", "-g", "Moraea_DarkMenuBar"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode("utf-8").strip()
|
||||
if is_dark_menu_bar in ["1", "true"]:
|
||||
is_dark_menu_bar = True
|
||||
else:
|
||||
is_dark_menu_bar = False
|
||||
|
||||
is_blur_enabled = subprocess.run(["defaults", "read", "-g", "Moraea_BlurBeta"], stdout=subprocess.PIPE).stdout.decode("utf-8").strip()
|
||||
is_blur_enabled = subprocess.run(["defaults", "read", "-g", "Moraea_BlurBeta"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode("utf-8").strip()
|
||||
if is_blur_enabled in ["1", "true"]:
|
||||
is_blur_enabled = True
|
||||
else:
|
||||
is_blur_enabled = False
|
||||
|
||||
is_rim_disabled = subprocess.run(["defaults", "read", "-g", "Moraea_RimBetaDisabled"], stdout=subprocess.PIPE).stdout.decode("utf-8").strip()
|
||||
is_rim_disabled = subprocess.run(["defaults", "read", "-g", "Moraea_RimBetaDisabled"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode("utf-8").strip()
|
||||
if is_rim_disabled in ["1", "true"]:
|
||||
is_rim_disabled = True
|
||||
else:
|
||||
|
||||
+8
-75
@@ -8,7 +8,7 @@ import shutil
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from resources import utilities, constants, tui_helpers
|
||||
from resources import utilities, constants
|
||||
from data import os_data
|
||||
|
||||
class tui_disk_installation:
|
||||
@@ -75,65 +75,6 @@ class tui_disk_installation:
|
||||
return supported_partitions
|
||||
|
||||
|
||||
def copy_efi(self):
|
||||
utilities.cls()
|
||||
utilities.header(["Installing OpenCore to Drive"])
|
||||
|
||||
if not self.constants.opencore_release_folder.exists():
|
||||
tui_helpers.TUIOnlyLogging.info(
|
||||
["Installing OpenCore to Drive"],
|
||||
"Press [Enter] to go back.\n",
|
||||
[
|
||||
"""OpenCore folder missing!
|
||||
Please build OpenCore first!"""
|
||||
],
|
||||
).start()
|
||||
return
|
||||
|
||||
logging.info("\nDisk picker is loading...")
|
||||
|
||||
all_disks = self.list_disks()
|
||||
menu = tui_helpers.TUIMenu(
|
||||
["Select Disk"],
|
||||
"Please select the disk you would like to install OpenCore to: ",
|
||||
in_between=["Missing disks? Ensure they have an EFI or FAT32 partition."],
|
||||
return_number_instead_of_direct_call=True,
|
||||
loop=True,
|
||||
)
|
||||
for disk in all_disks:
|
||||
menu.add_menu_option(f"{disk}: {all_disks[disk]['name']} ({all_disks[disk]['size']})", key=disk[4:])
|
||||
|
||||
response = menu.start()
|
||||
|
||||
if response == -1:
|
||||
return
|
||||
|
||||
disk_identifier = "disk" + response
|
||||
selected_disk = all_disks[disk_identifier]
|
||||
|
||||
menu = tui_helpers.TUIMenu(
|
||||
["Select Partition"],
|
||||
"Please select the partition you would like to install OpenCore to: ",
|
||||
return_number_instead_of_direct_call=True,
|
||||
loop=True,
|
||||
in_between=["Missing partitions? Ensure they are formatted as an EFI or FAT32.", "", "* denotes likely candidate."],
|
||||
)
|
||||
for partition in selected_disk["partitions"]:
|
||||
if selected_disk["partitions"][partition]["fs"] not in ("msdos", "EFI"):
|
||||
continue
|
||||
text = f"{partition}: {selected_disk['partitions'][partition]['name']} ({utilities.human_fmt(selected_disk['partitions'][partition]['size'])})"
|
||||
if selected_disk["partitions"][partition]["type"] == "EFI" or (
|
||||
selected_disk["partitions"][partition]["type"] == "Microsoft Basic Data" and selected_disk["partitions"][partition]["size"] < 1024 * 1024 * 512
|
||||
): # 512 megabytes:
|
||||
text += " *"
|
||||
menu.add_menu_option(text, key=partition[len(disk_identifier) + 1 :])
|
||||
|
||||
response = menu.start()
|
||||
|
||||
if response == -1:
|
||||
return
|
||||
self.install_opencore(f"{disk_identifier}s{response}")
|
||||
|
||||
def install_opencore(self, full_disk_identifier):
|
||||
def determine_sd_card(media_name):
|
||||
# Array filled with common SD Card names
|
||||
@@ -169,18 +110,13 @@ Please build OpenCore first!"""
|
||||
# cancelled prompt
|
||||
return
|
||||
else:
|
||||
if self.constants.gui_mode is False:
|
||||
tui_helpers.TUIOnlyLogging.info(
|
||||
["Copying OpenCore"], "Press [Enter] to go back.\n", ["An error occurred!"] + result.stderr.decode().split("\n") + [""]
|
||||
).start()
|
||||
else:
|
||||
logging.info("An error occurred!")
|
||||
logging.info(result.stderr.decode())
|
||||
logging.info("An error occurred!")
|
||||
logging.info(result.stderr.decode())
|
||||
|
||||
# Check if we're in Safe Mode, and if so, tell user FAT32 is unsupported
|
||||
if utilities.check_boot_mode() == "safe_boot":
|
||||
logging.info("\nSafe Mode detected. FAT32 is unsupported by macOS in this mode.")
|
||||
logging.info("Please disable Safe Mode and try again.")
|
||||
# Check if we're in Safe Mode, and if so, tell user FAT32 is unsupported
|
||||
if utilities.check_boot_mode() == "safe_boot":
|
||||
logging.info("\nSafe Mode detected. FAT32 is unsupported by macOS in this mode.")
|
||||
logging.info("Please disable Safe Mode and try again.")
|
||||
return
|
||||
partition_info = plistlib.loads(subprocess.run(f"diskutil info -plist {full_disk_identifier}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
parent_disk = partition_info["ParentWholeDisk"]
|
||||
@@ -252,10 +188,7 @@ Please build OpenCore first!"""
|
||||
logging.info("\nPress [Enter] to continue.\n")
|
||||
input()
|
||||
else:
|
||||
if self.constants.gui_mode is False:
|
||||
tui_helpers.TUIOnlyLogging.info(["Copying OpenCore"], "Press [Enter] to go back.\n", ["EFI failed to mount!"]).start()
|
||||
else:
|
||||
logging.info("EFI failed to mount!")
|
||||
logging.info("EFI failed to mount!")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
# Creates a macOS Installer
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import logging
|
||||
from resources import utilities, tui_helpers, 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 select_disk_to_format():
|
||||
utilities.cls()
|
||||
utilities.header(["Installing OpenCore to Drive"])
|
||||
|
||||
logging.info("\nDisk picker is loading...")
|
||||
|
||||
all_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
|
||||
menu = tui_helpers.TUIMenu(
|
||||
["Select Disk to write the macOS Installer onto"],
|
||||
"Please select the disk you would like to install OpenCore to: ",
|
||||
in_between=["Missing drives? Verify they are 14GB+ and external (ie. USB)", "", "Ensure all data is backed up on selected drive, entire drive will be erased!"],
|
||||
return_number_instead_of_direct_call=True,
|
||||
loop=True,
|
||||
)
|
||||
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
|
||||
menu.add_menu_option(f"{disk}: {all_disks[disk]['name']} ({utilities.human_fmt(all_disks[disk]['size'])})", key=disk[4:])
|
||||
|
||||
response = menu.start()
|
||||
|
||||
if response == -1:
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
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
|
||||
+308
-63
@@ -4,6 +4,8 @@
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
import tempfile
|
||||
import plistlib
|
||||
|
||||
import packaging.version
|
||||
import requests
|
||||
@@ -13,10 +15,14 @@ import os
|
||||
|
||||
import logging
|
||||
|
||||
from resources import utilities, network_handler
|
||||
from resources.constants import Constants
|
||||
from resources import utilities, network_handler, constants
|
||||
from data import os_data
|
||||
|
||||
KDK_INSTALL_PATH = "/Library/Developer/KDKs"
|
||||
KDK_INSTALL_PATH: str = "/Library/Developer/KDKs"
|
||||
KDK_INFO_PLIST: str = "KDKInfo.plist"
|
||||
KDK_API_LINK: str = "https://raw.githubusercontent.com/dortania/KdkSupportPkg/gh-pages/manifest.json"
|
||||
|
||||
KDK_ASSET_LIST: list = None
|
||||
|
||||
|
||||
class KernelDebugKitObject:
|
||||
@@ -43,12 +49,14 @@ class KernelDebugKitObject:
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, constants: Constants, host_build: str, host_version: str, ignore_installed: bool = False):
|
||||
self.constants: Constants = constants
|
||||
def __init__(self, global_constants: constants.Constants, host_build: str, host_version: str, ignore_installed: bool = False, passive: bool = False):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.host_build: str = host_build # ex. 20A5384c
|
||||
self.host_version: str = host_version # ex. 11.0.1
|
||||
|
||||
self.passive: bool = passive # Don't perform actions requiring elevated privileges
|
||||
|
||||
self.ignore_installed: bool = ignore_installed # If True, will ignore any installed KDKs and download the latest
|
||||
self.kdk_already_installed: bool = False
|
||||
|
||||
@@ -75,17 +83,20 @@ class KernelDebugKitObject:
|
||||
self._get_latest_kdk()
|
||||
|
||||
|
||||
def _get_available_kdks(self):
|
||||
def _get_remote_kdks(self):
|
||||
"""
|
||||
Fetches a list of available KDKs from the KdkSupportPkg API
|
||||
Additionally caches the list for future use, avoiding extra API calls
|
||||
|
||||
Returns:
|
||||
list: A list of KDKs, sorted by version and date if available. Returns None if the API is unreachable
|
||||
"""
|
||||
|
||||
KDK_API_LINK = "https://raw.githubusercontent.com/dortania/KdkSupportPkg/gh-pages/manifest.json"
|
||||
global KDK_ASSET_LIST
|
||||
|
||||
logging.info("- Pulling KDK list from KdkSupportPkg API")
|
||||
if KDK_ASSET_LIST:
|
||||
return KDK_ASSET_LIST
|
||||
|
||||
try:
|
||||
results = network_handler.SESSION.get(
|
||||
@@ -93,7 +104,7 @@ class KernelDebugKitObject:
|
||||
headers={
|
||||
"User-Agent": f"OCLP/{self.constants.patcher_version}"
|
||||
},
|
||||
timeout=10
|
||||
timeout=5
|
||||
)
|
||||
except (requests.exceptions.Timeout, requests.exceptions.TooManyRedirects, requests.exceptions.ConnectionError):
|
||||
logging.info("- Could not contact KDK API")
|
||||
@@ -103,14 +114,16 @@ class KernelDebugKitObject:
|
||||
logging.info("- Could not fetch KDK list")
|
||||
return None
|
||||
|
||||
return sorted(results.json(), key=lambda x: (packaging.version.parse(x["version"]), datetime.datetime.fromisoformat(x["date"])), reverse=True)
|
||||
KDK_ASSET_LIST = sorted(results.json(), key=lambda x: (packaging.version.parse(x["version"]), datetime.datetime.fromisoformat(x["date"])), reverse=True)
|
||||
|
||||
return KDK_ASSET_LIST
|
||||
|
||||
|
||||
def _get_latest_kdk(self, host_build: str = None, host_version: str = None):
|
||||
"""
|
||||
Fetches the latest KDK for the current macOS version
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
host_build (str, optional): The build version of the current macOS version.
|
||||
If empty, will use the host_build from the class. Defaults to None.
|
||||
host_version (str, optional): The version of the current macOS version.
|
||||
@@ -121,17 +134,21 @@ class KernelDebugKitObject:
|
||||
host_build = self.host_build
|
||||
host_version = self.host_version
|
||||
|
||||
logging.info(f"- Fetching latest KDK for {host_build} ({host_version})")
|
||||
self.kdk_installed_path = self._local_kdk_installed_build()
|
||||
parsed_version = cast(packaging.version.Version, packaging.version.parse(host_version))
|
||||
|
||||
if os_data.os_conversion.os_to_kernel(str(parsed_version.major)) < os_data.os_data.ventura:
|
||||
self.error_msg = "KDKs are not required for macOS Monterey or older"
|
||||
logging.warning(f"- {self.error_msg}")
|
||||
return
|
||||
|
||||
self.kdk_installed_path = self._local_kdk_installed()
|
||||
if self.kdk_installed_path:
|
||||
logging.info(f"- KDK already installed ({Path(self.kdk_installed_path).name}), skipping")
|
||||
self.kdk_already_installed = True
|
||||
self.success = True
|
||||
return
|
||||
|
||||
remote_kdk_version = self._get_available_kdks()
|
||||
|
||||
parsed_version = cast(packaging.version.Version, packaging.version.parse(host_version))
|
||||
remote_kdk_version = self._get_remote_kdks()
|
||||
|
||||
if remote_kdk_version is None:
|
||||
logging.warning("- Failed to fetch KDK list, falling back to local KDK matching")
|
||||
@@ -140,17 +157,19 @@ class KernelDebugKitObject:
|
||||
# ex. 13.0.1 vs 13.0
|
||||
loose_version = f"{parsed_version.major}.{parsed_version.minor}"
|
||||
logging.info(f"- Checking for KDKs loosely matching {loose_version}")
|
||||
self.kdk_installed_path = self._local_kdk_installed_version(loose_version)
|
||||
self.kdk_installed_path = self._local_kdk_installed(match=loose_version, check_version=True)
|
||||
if self.kdk_installed_path:
|
||||
logging.info(f"- Found matching KDK: {Path(self.kdk_installed_path).name}")
|
||||
self.kdk_already_installed = True
|
||||
self.success = True
|
||||
return
|
||||
|
||||
older_version = f"{parsed_version.major}.{parsed_version.minor - 1 if parsed_version.minor > 0 else 0}"
|
||||
logging.info(f"- Checking for KDKs matching {older_version}")
|
||||
self.kdk_installed_path = self._local_kdk_installed_version(older_version)
|
||||
self.kdk_installed_path = self._local_kdk_installed(match=older_version, check_version=True)
|
||||
if self.kdk_installed_path:
|
||||
logging.info(f"- Found matching KDK: {Path(self.kdk_installed_path).name}")
|
||||
self.kdk_already_installed = True
|
||||
self.success = True
|
||||
return
|
||||
|
||||
@@ -195,7 +214,7 @@ class KernelDebugKitObject:
|
||||
|
||||
|
||||
# Check if this KDK is already installed
|
||||
self.kdk_installed_path = self._local_kdk_installed_build(self.kdk_url_build)
|
||||
self.kdk_installed_path = self._local_kdk_installed(match=self.kdk_url_build)
|
||||
if self.kdk_installed_path:
|
||||
logging.info(f"- KDK already installed ({Path(self.kdk_installed_path).name}), skipping")
|
||||
self.kdk_already_installed = True
|
||||
@@ -236,18 +255,93 @@ class KernelDebugKitObject:
|
||||
|
||||
logging.info(f"- Returning DownloadObject for KDK: {Path(self.kdk_url).name}")
|
||||
self.success = True
|
||||
return network_handler.DownloadObject(self.kdk_url, self.constants.kdk_download_path if override_path == "" else Path(override_path))
|
||||
|
||||
kdk_download_path = self.constants.kdk_download_path if override_path == "" else Path(override_path)
|
||||
kdk_plist_path = Path(f"{kdk_download_path.parent}/{KDK_INFO_PLIST}") if override_path == "" else Path(f"{Path(override_path).parent}/{KDK_INFO_PLIST}")
|
||||
|
||||
self._generate_kdk_info_plist(kdk_plist_path)
|
||||
return network_handler.DownloadObject(self.kdk_url, kdk_download_path)
|
||||
|
||||
|
||||
def _local_kdk_valid(self, kdk_path: str):
|
||||
def _generate_kdk_info_plist(self, plist_path: str):
|
||||
"""
|
||||
Generates a KDK Info.plist
|
||||
|
||||
"""
|
||||
|
||||
plist_path = Path(plist_path)
|
||||
if plist_path.exists():
|
||||
plist_path.unlink()
|
||||
|
||||
kdk_dict = {
|
||||
"build": self.kdk_url_build,
|
||||
"version": self.kdk_url_version,
|
||||
}
|
||||
|
||||
try:
|
||||
plist_path.touch()
|
||||
plistlib.dump(kdk_dict, plist_path.open("wb"), sort_keys=False)
|
||||
except Exception as e:
|
||||
logging.error(f"- Failed to generate KDK Info.plist: {e}")
|
||||
|
||||
|
||||
def _local_kdk_valid(self, kdk_path: Path):
|
||||
"""
|
||||
Validates provided KDK, ensure no corruption
|
||||
|
||||
The reason for this is due to macOS deleting files from the KDK during OS updates,
|
||||
similar to how Install macOS.app is deleted during OS updates
|
||||
|
||||
Args:
|
||||
kdk_path (str): Path to KDK
|
||||
Uses Apple's pkg receipt system to verify the original contents of the KDK
|
||||
|
||||
Parameters:
|
||||
kdk_path (Path): Path to KDK
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False if invalid
|
||||
"""
|
||||
|
||||
if not Path(f"{kdk_path}/System/Library/CoreServices/SystemVersion.plist").exists():
|
||||
logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing SystemVersion.plist")
|
||||
self._remove_kdk(kdk_path)
|
||||
return False
|
||||
|
||||
# Get build from KDK
|
||||
kdk_plist_data = plistlib.load(Path(f"{kdk_path}/System/Library/CoreServices/SystemVersion.plist").open("rb"))
|
||||
if "ProductBuildVersion" not in kdk_plist_data:
|
||||
logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing ProductBuildVersion")
|
||||
self._remove_kdk(kdk_path)
|
||||
return False
|
||||
|
||||
kdk_build = kdk_plist_data["ProductBuildVersion"]
|
||||
|
||||
# Check pkg receipts for this build, will give a canonical list if all files that should be present
|
||||
result = subprocess.run(["pkgutil", "--files", f"com.apple.pkg.KDK.{kdk_build}"], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
# If pkg receipt is missing, we'll fallback to legacy validation
|
||||
logging.info(f"- pkg receipt missing for {kdk_path.name}, falling back to legacy validation")
|
||||
return self._local_kdk_valid_legacy(kdk_path)
|
||||
|
||||
# Go through each line of the pkg receipt and ensure it exists
|
||||
for line in result.stdout.decode("utf-8").splitlines():
|
||||
if not line.startswith("System/Library/Extensions"):
|
||||
continue
|
||||
if not Path(f"{kdk_path}/{line}").exists():
|
||||
logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing file: {line}")
|
||||
self._remove_kdk(kdk_path)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _local_kdk_valid_legacy(self, kdk_path: Path):
|
||||
"""
|
||||
Legacy variant of validating provided KDK
|
||||
Uses best guess of files that should be present
|
||||
This should ideally never be invoked, but used as a fallback
|
||||
|
||||
Parameters:
|
||||
kdk_path (Path): Path to KDK
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False if invalid
|
||||
@@ -260,8 +354,6 @@ class KernelDebugKitObject:
|
||||
"AMDRadeonX6000.kext/Contents/MacOS/AMDRadeonX6000",
|
||||
]
|
||||
|
||||
kdk_path = Path(kdk_path)
|
||||
|
||||
for kext in KEXT_CATALOG:
|
||||
if not Path(f"{kdk_path}/System/Library/Extensions/{kext}").exists():
|
||||
logging.info(f"- Corrupted KDK found, removing due to missing: {kdk_path}/System/Library/Extensions/{kext}")
|
||||
@@ -271,39 +363,14 @@ class KernelDebugKitObject:
|
||||
return True
|
||||
|
||||
|
||||
def _local_kdk_installed_build(self, build: str = None):
|
||||
def _local_kdk_installed(self, match: str = None, check_version: bool = False):
|
||||
"""
|
||||
Checks if KDK matching build is installed
|
||||
If so, validates it has not been corrupted
|
||||
|
||||
Returns:
|
||||
str: Path to KDK if valid, None if not
|
||||
"""
|
||||
|
||||
if self.ignore_installed is True:
|
||||
return None
|
||||
|
||||
if build is None:
|
||||
build = self.host_build
|
||||
|
||||
if not Path(KDK_INSTALL_PATH).exists():
|
||||
return None
|
||||
|
||||
for kdk_folder in Path(KDK_INSTALL_PATH).iterdir():
|
||||
if not kdk_folder.is_dir():
|
||||
continue
|
||||
if not kdk_folder.name.endswith(f"{build}.kdk"):
|
||||
continue
|
||||
|
||||
if self._local_kdk_valid(kdk_folder):
|
||||
return kdk_folder
|
||||
|
||||
return None
|
||||
|
||||
def _local_kdk_installed_version(self, version: str = None):
|
||||
"""
|
||||
Checks if KDK matching version is installed
|
||||
If so, validates it has not been corrupted
|
||||
Parameters:
|
||||
match (str): string to match against (ex. build or version)
|
||||
check_version (bool): If True, match against version, otherwise match against build
|
||||
|
||||
Returns:
|
||||
str: Path to KDK if valid, None if not
|
||||
@@ -312,8 +379,11 @@ class KernelDebugKitObject:
|
||||
if self.ignore_installed is True:
|
||||
return None
|
||||
|
||||
if version is None:
|
||||
version = self.host_version
|
||||
if match is None:
|
||||
if check_version:
|
||||
match = self.host_version
|
||||
else:
|
||||
match = self.host_build
|
||||
|
||||
if not Path(KDK_INSTALL_PATH).exists():
|
||||
return None
|
||||
@@ -321,12 +391,41 @@ class KernelDebugKitObject:
|
||||
for kdk_folder in Path(KDK_INSTALL_PATH).iterdir():
|
||||
if not kdk_folder.is_dir():
|
||||
continue
|
||||
if version not in kdk_folder.name:
|
||||
continue
|
||||
if check_version:
|
||||
if match not in kdk_folder.name:
|
||||
continue
|
||||
else:
|
||||
if not kdk_folder.name.endswith(f"{match}.kdk"):
|
||||
continue
|
||||
|
||||
if self._local_kdk_valid(kdk_folder):
|
||||
return kdk_folder
|
||||
|
||||
# If we can't find a KDK, next check if there's a backup present
|
||||
# Check for KDK packages in the same directory as the KDK
|
||||
for kdk_pkg in Path(KDK_INSTALL_PATH).iterdir():
|
||||
if kdk_pkg.is_dir():
|
||||
continue
|
||||
if not kdk_pkg.name.endswith(".pkg"):
|
||||
continue
|
||||
if check_version:
|
||||
if match not in kdk_pkg.name:
|
||||
continue
|
||||
else:
|
||||
if not kdk_pkg.name.endswith(f"{match}.pkg"):
|
||||
continue
|
||||
|
||||
logging.info(f"- Found KDK backup: {kdk_pkg.name}")
|
||||
if self.passive is False:
|
||||
logging.info("- Attempting KDK restoration")
|
||||
if KernelDebugKitUtilities().install_kdk_pkg(kdk_pkg):
|
||||
logging.info("- Successfully restored KDK")
|
||||
return self._local_kdk_installed(match=match, check_version=check_version)
|
||||
else:
|
||||
# When in passive mode, we're just checking if a KDK could be restored
|
||||
logging.info("- KDK restoration skipped, running in passive mode")
|
||||
return kdk_pkg
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -334,15 +433,26 @@ class KernelDebugKitObject:
|
||||
"""
|
||||
Removes provided KDK
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
kdk_path (str): Path to KDK
|
||||
"""
|
||||
|
||||
if self.passive is True:
|
||||
return
|
||||
|
||||
if os.getuid() != 0:
|
||||
logging.warning("- Cannot remove KDK, not running as root")
|
||||
return
|
||||
|
||||
result = utilities.elevated(["rm", "-rf", kdk_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if not Path(kdk_path).exists():
|
||||
logging.warning(f"- KDK does not exist: {kdk_path}")
|
||||
return
|
||||
|
||||
rm_args = ["rm", "-f", kdk_path]
|
||||
if Path(kdk_path).is_dir():
|
||||
rm_args = ["rm", "-rf", kdk_path]
|
||||
|
||||
result = utilities.elevated(rm_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.warning(f"- Failed to remove KDK: {kdk_path}")
|
||||
logging.warning(f"- {result.stdout.decode('utf-8')}")
|
||||
@@ -354,11 +464,13 @@ class KernelDebugKitObject:
|
||||
"""
|
||||
Removes KDKs that are not in use
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
exclude_builds (list, optional): Builds to exclude from removal.
|
||||
If None, defaults to host and closest match builds.
|
||||
"""
|
||||
|
||||
if self.passive is True:
|
||||
return
|
||||
|
||||
if exclude_builds is None:
|
||||
exclude_builds = [
|
||||
@@ -375,10 +487,12 @@ class KernelDebugKitObject:
|
||||
logging.info("- Cleaning unused KDKs")
|
||||
for kdk_folder in Path(KDK_INSTALL_PATH).iterdir():
|
||||
if kdk_folder.is_dir():
|
||||
if kdk_folder.name.endswith(".kdk"):
|
||||
if kdk_folder.name.endswith(".kdk") or kdk_folder.name.endswith(".pkg"):
|
||||
should_remove = True
|
||||
for build in exclude_builds:
|
||||
if build != "" and kdk_folder.name.endswith(f"{build}.kdk"):
|
||||
if build != "":
|
||||
continue
|
||||
if kdk_folder.name.endswith(f"{build}.kdk") or kdk_folder.name.endswith(f"{build}.pkg"):
|
||||
should_remove = False
|
||||
break
|
||||
if should_remove is False:
|
||||
@@ -390,7 +504,7 @@ class KernelDebugKitObject:
|
||||
"""
|
||||
Validates KDK DMG checksum
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
kdk_dmg_path (str, optional): Path to KDK DMG. Defaults to None.
|
||||
|
||||
Returns:
|
||||
@@ -420,3 +534,134 @@ class KernelDebugKitObject:
|
||||
self._remove_unused_kdks()
|
||||
|
||||
self.success = True
|
||||
|
||||
|
||||
class KernelDebugKitUtilities:
|
||||
"""
|
||||
Utilities for KDK handling
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
def install_kdk_pkg(self, kdk_path: Path):
|
||||
"""
|
||||
Installs provided KDK packages
|
||||
|
||||
Parameters:
|
||||
kdk_path (Path): Path to KDK package
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False if not
|
||||
"""
|
||||
|
||||
if os.getuid() != 0:
|
||||
logging.warning("- Cannot install KDK, not running as root")
|
||||
return False
|
||||
|
||||
logging.info(f"- Installing KDK package: {kdk_path.name}")
|
||||
logging.info(f" - This may take a while...")
|
||||
|
||||
# TODO: Check whether enough disk space is available
|
||||
|
||||
result = utilities.elevated(["installer", "-pkg", kdk_path, "-target", "/"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to install KDK:")
|
||||
logging.info(result.stdout.decode('utf-8'))
|
||||
if result.stderr:
|
||||
logging.info(result.stderr.decode('utf-8'))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def install_kdk_dmg(self, kdk_path: Path):
|
||||
"""
|
||||
Installs provided KDK disk image
|
||||
|
||||
Parameters:
|
||||
kdk_path (Path): Path to KDK disk image
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False if not
|
||||
"""
|
||||
|
||||
if os.getuid() != 0:
|
||||
logging.warning("- Cannot install KDK, not running as root")
|
||||
return False
|
||||
|
||||
logging.info(f"- Extracting downloaded KDK disk image")
|
||||
with tempfile.TemporaryDirectory() as mount_point:
|
||||
result = subprocess.run(["hdiutil", "attach", kdk_path, "-mountpoint", mount_point, "-nobrowse"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to mount KDK:")
|
||||
logging.info(result.stdout.decode('utf-8'))
|
||||
return False
|
||||
|
||||
kdk_pkg_path = Path(f"{mount_point}/KernelDebugKit.pkg")
|
||||
|
||||
if not kdk_pkg_path.exists():
|
||||
logging.warning("- Failed to find KDK package in DMG, likely corrupted!!!")
|
||||
self._unmount_disk_image(mount_point)
|
||||
return False
|
||||
|
||||
if self.install_kdk_pkg(kdk_pkg_path) is False:
|
||||
self._unmount_disk_image(mount_point)
|
||||
return False
|
||||
|
||||
self._create_backup(kdk_pkg_path, Path(f"{kdk_path.parent}/{KDK_INFO_PLIST}"))
|
||||
self._unmount_disk_image(mount_point)
|
||||
|
||||
logging.info("- Successfully installed KDK")
|
||||
return True
|
||||
|
||||
def _unmount_disk_image(self, mount_point):
|
||||
"""
|
||||
Unmounts provided disk image silently
|
||||
|
||||
Parameters:
|
||||
mount_point (Path): Path to mount point
|
||||
"""
|
||||
subprocess.run(["hdiutil", "detach", mount_point], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
|
||||
def _create_backup(self, kdk_path: Path, kdk_info_plist: Path):
|
||||
"""
|
||||
Creates a backup of the KDK
|
||||
|
||||
Parameters:
|
||||
kdk_path (Path): Path to KDK
|
||||
kdk_info_plist (Path): Path to KDK Info.plist
|
||||
"""
|
||||
|
||||
if not kdk_path.exists():
|
||||
logging.warning("- KDK does not exist, cannot create backup")
|
||||
return
|
||||
if not kdk_info_plist.exists():
|
||||
logging.warning("- KDK Info.plist does not exist, cannot create backup")
|
||||
return
|
||||
|
||||
kdk_info_dict = plistlib.load(kdk_info_plist.open("rb"))
|
||||
|
||||
if 'version' not in kdk_info_dict or 'build' not in kdk_info_dict:
|
||||
logging.warning("- Malformed KDK Info.plist provided, cannot create backup")
|
||||
return
|
||||
|
||||
if os.getuid() != 0:
|
||||
logging.warning("- Cannot create KDK backup, not running as root")
|
||||
return
|
||||
|
||||
kdk_dst_name = f"KDK_{kdk_info_dict['version']}_{kdk_info_dict['build']}.pkg"
|
||||
kdk_dst_path = Path(f"{KDK_INSTALL_PATH}/{kdk_dst_name}")
|
||||
|
||||
logging.info(f"- Creating backup: {kdk_dst_name}")
|
||||
if kdk_dst_path.exists():
|
||||
logging.info("- Backup already exists, skipping")
|
||||
return
|
||||
|
||||
result = utilities.elevated(["cp", "-R", kdk_path, kdk_dst_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to create KDK backup:")
|
||||
logging.info(result.stdout.decode('utf-8'))
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -19,9 +21,12 @@ class InitializeLoggingSupport:
|
||||
>>> from resources.logging_handler import InitializeLoggingSupport
|
||||
>>> InitializeLoggingSupport()
|
||||
|
||||
FOR DEVELOPERS:
|
||||
- Do not invoke logging until after '_attempt_initialize_logging_configuration()' has been invoked
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self):
|
||||
self.log_filename: str = "OpenCore-Patcher.log"
|
||||
self.log_filepath: Path = None
|
||||
|
||||
@@ -35,6 +40,7 @@ class InitializeLoggingSupport:
|
||||
self._clean_log_file()
|
||||
self._attempt_initialize_logging_configuration()
|
||||
self._implement_custom_traceback_handler()
|
||||
self._fix_file_permission()
|
||||
|
||||
|
||||
def __del__(self):
|
||||
@@ -81,12 +87,34 @@ class InitializeLoggingSupport:
|
||||
print(f"- Failed to clean log file: {e}")
|
||||
|
||||
|
||||
def _fix_file_permission(self):
|
||||
"""
|
||||
Fixes file permission for log file
|
||||
|
||||
If OCLP was invoked as root, file permission will only allow root to write to log file
|
||||
This in turn breaks normal OCLP execution to write to log file
|
||||
"""
|
||||
|
||||
if os.geteuid() != 0:
|
||||
return
|
||||
|
||||
result = subprocess.run(["chmod", "777", self.log_filepath], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
print(f"- Failed to fix log file permissions")
|
||||
if result.stderr:
|
||||
print(result.stderr.decode("utf-8"))
|
||||
|
||||
|
||||
def _initialize_logging_configuration(self, log_to_file: bool = True):
|
||||
"""
|
||||
Initialize logging framework configuration
|
||||
|
||||
StreamHandler's format is used to mimic the default behavior of print()
|
||||
While FileHandler's format is for more in-depth logging
|
||||
|
||||
Parameters:
|
||||
log_to_file (bool): Whether to log to file or not
|
||||
|
||||
"""
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -101,6 +129,7 @@ class InitializeLoggingSupport:
|
||||
logging.getLogger().handlers[0].setFormatter(logging.Formatter("%(message)s"))
|
||||
logging.getLogger().handlers[1].maxBytes = self.max_file_size
|
||||
|
||||
|
||||
def _attempt_initialize_logging_configuration(self):
|
||||
"""
|
||||
Attempt to initialize logging framework configuration
|
||||
@@ -136,6 +165,7 @@ class InitializeLoggingSupport:
|
||||
sys.excepthook = custom_excepthook
|
||||
threading.excepthook = custom_thread_excepthook
|
||||
|
||||
|
||||
def _restore_original_excepthook(self):
|
||||
"""
|
||||
Restore original traceback handlers
|
||||
|
||||
@@ -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)
|
||||
+57
-90
@@ -4,21 +4,16 @@ import sys
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from data import model_array
|
||||
from resources.build import build
|
||||
from resources.gui import gui_main
|
||||
from resources import (
|
||||
cli_menu,
|
||||
constants,
|
||||
utilities,
|
||||
device_probe,
|
||||
os_probe,
|
||||
defaults,
|
||||
arguments,
|
||||
install,
|
||||
tui_helpers,
|
||||
reroute_payloads,
|
||||
commit_info,
|
||||
logging_handler
|
||||
@@ -26,35 +21,50 @@ from resources import (
|
||||
|
||||
|
||||
class OpenCoreLegacyPatcher:
|
||||
def __init__(self, launch_gui=False):
|
||||
self.constants = constants.Constants()
|
||||
self.constants.wxpython_variant = launch_gui
|
||||
"""
|
||||
Initial entry point for starting OpenCore Legacy Patcher
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
logging_handler.InitializeLoggingSupport()
|
||||
|
||||
self.constants: constants.Constants = constants.Constants()
|
||||
|
||||
self.constants.wxpython_variant: bool = True
|
||||
|
||||
logging.info(f"- Loading OpenCore Legacy Patcher v{self.constants.patcher_version}...")
|
||||
|
||||
self.generate_base_data()
|
||||
self._generate_base_data()
|
||||
|
||||
if utilities.check_cli_args() is None:
|
||||
if launch_gui is True:
|
||||
utilities.disable_cls()
|
||||
from resources.gui import gui_main
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
else:
|
||||
self.main_menu()
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
|
||||
|
||||
def generate_base_data(self):
|
||||
self.constants.detected_os = os_probe.detect_kernel_major()
|
||||
self.constants.detected_os_minor = os_probe.detect_kernel_minor()
|
||||
self.constants.detected_os_build = os_probe.detect_os_build()
|
||||
self.constants.detected_os_version = os_probe.detect_os_version()
|
||||
def _generate_base_data(self):
|
||||
"""
|
||||
Generate base data required for the patcher to run
|
||||
"""
|
||||
|
||||
# Generate OS data
|
||||
os_data = os_probe.OSProbe()
|
||||
self.constants.detected_os = os_data.detect_kernel_major()
|
||||
self.constants.detected_os_minor = os_data.detect_kernel_minor()
|
||||
self.constants.detected_os_build = os_data.detect_os_build()
|
||||
self.constants.detected_os_version = os_data.detect_os_version()
|
||||
|
||||
# Generate computer data
|
||||
self.constants.computer = device_probe.Computer.probe()
|
||||
self.constants.recovery_status = utilities.check_recovery()
|
||||
self.computer = self.constants.computer
|
||||
self.constants.booted_oc_disk = utilities.find_disk_off_uuid(utilities.clean_device_path(self.computer.opencore_path))
|
||||
if self.constants.computer.firmware_vendor:
|
||||
if self.constants.computer.firmware_vendor != "Apple":
|
||||
self.constants.host_is_hackintosh = True
|
||||
|
||||
# Generate environment data
|
||||
self.constants.recovery_status = utilities.check_recovery()
|
||||
utilities.disable_cls()
|
||||
|
||||
# Generate binary data
|
||||
launcher_script = None
|
||||
launcher_binary = sys.executable
|
||||
if "python" in launcher_binary:
|
||||
@@ -64,83 +74,40 @@ class OpenCoreLegacyPatcher:
|
||||
launcher_script = launcher_script.replace("/resources/main.py", "/OpenCore-Patcher-GUI.command")
|
||||
self.constants.launcher_binary = launcher_binary
|
||||
self.constants.launcher_script = launcher_script
|
||||
self.constants.unpack_thread = threading.Thread(target=reroute_payloads.reroute_payloads(self.constants).setup_tmp_disk_image)
|
||||
self.constants.unpack_thread.start()
|
||||
self.constants.commit_info = commit_info.commit_info(self.constants.launcher_binary).generate_commit_info()
|
||||
|
||||
# Now that we have commit info, update nightly link
|
||||
# Initialize working directory
|
||||
self.constants.unpack_thread = threading.Thread(target=reroute_payloads.RoutePayloadDiskImage, args=(self.constants,))
|
||||
self.constants.unpack_thread.start()
|
||||
|
||||
# Generate commit info
|
||||
self.constants.commit_info = commit_info.ParseCommitInfo(self.constants.launcher_binary).generate_commit_info()
|
||||
if self.constants.commit_info[0] not in ["Running from source", "Built from source"]:
|
||||
# Now that we have commit info, update nightly link
|
||||
branch = self.constants.commit_info[0]
|
||||
branch = branch.replace("refs/heads/", "")
|
||||
self.constants.installer_pkg_url_nightly = self.constants.installer_pkg_url_nightly.replace("main", branch)
|
||||
|
||||
defaults.generate_defaults(self.computer.real_model, True, self.constants)
|
||||
# Generate defaults
|
||||
defaults.GenerateDefaults(self.computer.real_model, True, self.constants)
|
||||
|
||||
if utilities.check_cli_args() is not None:
|
||||
logging.info("- Detected arguments, switching to CLI mode")
|
||||
self.constants.gui_mode = True # Assumes no user interaction is required
|
||||
ignore_args = ["--auto_patch", "--gui_patch", "--gui_unpatch"]
|
||||
if not any(x in sys.argv for x in ignore_args):
|
||||
self.constants.current_path = Path.cwd()
|
||||
self.constants.cli_mode = True
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
logging.info("- Rerouting payloads location")
|
||||
self.constants.payload_path = sys._MEIPASS / Path("payloads")
|
||||
ignore_args = ignore_args.pop(0)
|
||||
if not any(x in sys.argv for x in ignore_args):
|
||||
while self.constants.unpack_thread.is_alive():
|
||||
time.sleep(0.1)
|
||||
arguments.arguments().parse_arguments(self.constants)
|
||||
else:
|
||||
if utilities.check_cli_args() is None:
|
||||
logging.info(f"- No arguments present, loading {'GUI' if self.constants.wxpython_variant is True else 'TUI'} mode")
|
||||
return
|
||||
|
||||
logging.info("- Detected arguments, switching to CLI mode")
|
||||
self.constants.gui_mode = True # Assumes no user interaction is required
|
||||
|
||||
def main_menu(self):
|
||||
response = None
|
||||
while not (response and response == -1):
|
||||
title = [
|
||||
f"OpenCore Legacy Patcher v{self.constants.patcher_version}",
|
||||
f"Selected Model: {self.constants.custom_model or self.computer.real_model}",
|
||||
]
|
||||
ignore_args = ["--auto_patch", "--gui_patch", "--gui_unpatch"]
|
||||
if not any(x in sys.argv for x in ignore_args):
|
||||
self.constants.current_path = Path.cwd()
|
||||
self.constants.cli_mode = True
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
logging.info("- Rerouting payloads location")
|
||||
self.constants.payload_path = sys._MEIPASS / Path("payloads")
|
||||
ignore_args = ignore_args.pop(0)
|
||||
|
||||
if (self.constants.custom_model or self.computer.real_model) not in model_array.SupportedSMBIOS and self.constants.allow_oc_everywhere is False:
|
||||
in_between = [
|
||||
"Your model is not supported by this patcher for running unsupported OSes!",
|
||||
"",
|
||||
'If you plan to create the USB for another machine, please select the \n"Change Model" option in the menu.',
|
||||
"",
|
||||
'If you want to run OCLP on a native Mac, please toggle \n"Allow OpenCore on native Models" in settings',
|
||||
]
|
||||
elif not self.constants.custom_model and self.computer.real_model == "iMac7,1" and "SSE4.1" not in self.computer.cpu.flags:
|
||||
in_between = [
|
||||
"Your model requires a CPU upgrade to a CPU supporting SSE4.1+ to be supported by this patcher!",
|
||||
"",
|
||||
f'If you plan to create the USB for another {self.computer.real_model} with SSE4.1+, please select the "Change Model" option in the menu.',
|
||||
]
|
||||
elif self.constants.custom_model == "iMac7,1":
|
||||
in_between = ["This model is supported", "However please ensure the CPU has been upgraded to support SSE4.1+"]
|
||||
else:
|
||||
in_between = ["This model is supported"]
|
||||
if not any(x in sys.argv for x in ignore_args):
|
||||
while self.constants.unpack_thread.is_alive():
|
||||
time.sleep(0.1)
|
||||
|
||||
menu = tui_helpers.TUIMenu(title, "Please select an option: ", in_between=in_between, auto_number=True, top_level=True)
|
||||
|
||||
options = (
|
||||
[["Build OpenCore", build.build_opencore(self.constants.custom_model or self.constants.computer.real_model, self.constants).build_opencore]]
|
||||
if ((self.constants.custom_model or self.computer.real_model) in model_array.SupportedSMBIOS) or self.constants.allow_oc_everywhere is True
|
||||
else []
|
||||
) + [
|
||||
["Install OpenCore to USB/internal drive", install.tui_disk_installation(self.constants).copy_efi],
|
||||
["Post-Install Volume Patch", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).PatchVolume],
|
||||
["Change Model", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).change_model],
|
||||
["Patcher Settings", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).patcher_settings],
|
||||
["Installer Creation", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).download_macOS],
|
||||
["Credits", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).credits],
|
||||
]
|
||||
|
||||
for option in options:
|
||||
menu.add_menu_option(option[0], function=option[1])
|
||||
|
||||
response = menu.start()
|
||||
|
||||
if getattr(sys, "frozen", False) and self.constants.recovery_status is False:
|
||||
subprocess.run("""osascript -e 'tell application "Terminal" to close first window' & exit""", shell=True)
|
||||
arguments.arguments(self.constants)
|
||||
|
||||
+67
-21
@@ -5,31 +5,77 @@ import subprocess
|
||||
import plistlib
|
||||
|
||||
|
||||
def detect_kernel_major():
|
||||
# Return Major Kernel Version
|
||||
# Example Output: 21 (integer)
|
||||
return int(platform.uname().release.partition(".")[0])
|
||||
class OSProbe:
|
||||
"""
|
||||
Library for querying OS information specific to macOS
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.uname_data = platform.uname()
|
||||
|
||||
|
||||
def detect_kernel_minor():
|
||||
# Return Minor Kernel Version
|
||||
# Example Output: 1 (integer)
|
||||
return int(platform.uname().release.partition(".")[2].partition(".")[0])
|
||||
def detect_kernel_major(self):
|
||||
"""
|
||||
Detect the booted major kernel version
|
||||
|
||||
Returns:
|
||||
int: Major kernel version (ex. 21, from 21.1.0)
|
||||
"""
|
||||
|
||||
return int(self.uname_data.release.partition(".")[0])
|
||||
|
||||
|
||||
def detect_os_version():
|
||||
# Return OS version
|
||||
# Example Output: 12.0 (string)
|
||||
return subprocess.run("sw_vers -productVersion".split(), stdout=subprocess.PIPE).stdout.decode().strip()
|
||||
def detect_kernel_minor(self):
|
||||
"""
|
||||
Detect the booted minor kernel version
|
||||
|
||||
Returns:
|
||||
int: Minor kernel version (ex. 1, from 21.1.0)
|
||||
"""
|
||||
|
||||
return int(self.uname_data.release.partition(".")[2].partition(".")[0])
|
||||
|
||||
|
||||
def detect_os_build():
|
||||
# Return OS build
|
||||
# Example Output: 21A5522h (string)
|
||||
def detect_os_version(self):
|
||||
"""
|
||||
Detect the booted OS version
|
||||
|
||||
# With macOS 13.2, Apple implemented the Rapid Security Response system which
|
||||
# will change the reported build to the RSR version and not the original host
|
||||
# To get the proper versions:
|
||||
# - Host: /System/Library/CoreServices/SystemVersion.plist
|
||||
# - RSR: /System/Volumes/Preboot/Cryptexes/OS/System/Library/CoreServices/SystemVersion.plist
|
||||
return plistlib.load(open("/System/Library/CoreServices/SystemVersion.plist", "rb"))["ProductBuildVersion"]
|
||||
Returns:
|
||||
str: OS version (ex. 12.0)
|
||||
"""
|
||||
|
||||
result = subprocess.run(["sw_vers", "-productVersion"], stdout=subprocess.PIPE)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError("Failed to detect OS version")
|
||||
|
||||
return result.stdout.decode().strip()
|
||||
|
||||
|
||||
def detect_os_build(self, rsr: bool = False):
|
||||
"""
|
||||
Detect the booted OS build
|
||||
|
||||
Implementation note:
|
||||
With macOS 13.2, Apple implemented the Rapid Security Response system which
|
||||
will change the reported build to the RSR version and not the original host
|
||||
|
||||
To get the proper versions:
|
||||
- Host: /System/Library/CoreServices/SystemVersion.plist
|
||||
- RSR: /System/Volumes/Preboot/Cryptexes/OS/System/Library/CoreServices/SystemVersion.plist
|
||||
|
||||
|
||||
Parameters:
|
||||
rsr (bool): Whether to use the RSR version of the build
|
||||
|
||||
Returns:
|
||||
str: OS build (ex. 21A5522h)
|
||||
"""
|
||||
|
||||
file_path = "/System/Library/CoreServices/SystemVersion.plist"
|
||||
if rsr is True:
|
||||
file_path = f"/System/Volumes/Preboot/Cryptexes/OS{file_path}"
|
||||
|
||||
try:
|
||||
return plistlib.load(open(file_path, "rb"))["ProductBuildVersion"]
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to detect OS build: {e}")
|
||||
|
||||
@@ -9,21 +9,31 @@ import tempfile
|
||||
import atexit
|
||||
import logging
|
||||
|
||||
class reroute_payloads:
|
||||
def __init__(self, constants):
|
||||
self.constants = constants
|
||||
from resources import constants
|
||||
|
||||
class RoutePayloadDiskImage:
|
||||
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self._setup_tmp_disk_image()
|
||||
|
||||
|
||||
def _setup_tmp_disk_image(self):
|
||||
"""
|
||||
Initialize temp directory and mount payloads.dmg
|
||||
Create overlay for patcher to write to
|
||||
|
||||
Currently only applicable for GUI variant and not running from source
|
||||
"""
|
||||
|
||||
def setup_tmp_disk_image(self):
|
||||
# Create a temp directory to mount the payloads.dmg
|
||||
# Then reroute r/w to this new temp directory
|
||||
# Currently only applicable for GUI variant
|
||||
if self.constants.wxpython_variant is True and not self.constants.launcher_script:
|
||||
logging.info("- Running in Binary GUI mode, switching to tmp directory")
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
logging.info(f"- New payloads location: {self.temp_dir.name}")
|
||||
logging.info("- Creating payloads directory")
|
||||
Path(self.temp_dir.name / Path("payloads")).mkdir(parents=True, exist_ok=True)
|
||||
self.unmount_active_dmgs(unmount_all_active=False)
|
||||
self._unmount_active_dmgs(unmount_all_active=False)
|
||||
output = subprocess.run(
|
||||
[
|
||||
"hdiutil", "attach", "-noverify", f"{self.constants.payload_path}.dmg",
|
||||
@@ -38,16 +48,25 @@ class reroute_payloads:
|
||||
logging.info("- Mounted payloads.dmg")
|
||||
self.constants.current_path = Path(self.temp_dir.name)
|
||||
self.constants.payload_path = Path(self.temp_dir.name) / Path("payloads")
|
||||
atexit.register(self.unmount_active_dmgs, unmount_all_active=False)
|
||||
atexit.register(self._unmount_active_dmgs, unmount_all_active=False)
|
||||
else:
|
||||
logging.info("- Failed to mount payloads.dmg")
|
||||
logging.info(f"Output: {output.stdout.decode()}")
|
||||
logging.info(f"Return Code: {output.returncode}")
|
||||
|
||||
def unmount_active_dmgs(self, unmount_all_active=True):
|
||||
# Find all DMGs that are mounted, and forcefully unmount them
|
||||
# If our disk image was previously mounted, we need to unmount it to use again
|
||||
# This can happen if we crash during a previous secession, however 'atexit' class should hopefully avoid this
|
||||
|
||||
def _unmount_active_dmgs(self, unmount_all_active=True):
|
||||
"""
|
||||
Unmounts disk images associated with OCLP
|
||||
|
||||
Finds all DMGs that are mounted, and forcefully unmount them
|
||||
If our disk image was previously mounted, we need to unmount it to use again
|
||||
This can happen if we crash during a previous secession, however 'atexit' class should hopefully avoid this
|
||||
|
||||
Parameters:
|
||||
unmount_all_active (bool): If True, unmount all active DMGs, otherwise only unmount our own DMG
|
||||
"""
|
||||
|
||||
dmg_info = subprocess.run(["hdiutil", "info", "-plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
dmg_info = plistlib.loads(dmg_info.stdout)
|
||||
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
# Module for running processes with real time output
|
||||
# Written by CorpNewt
|
||||
# Source: https://github.com/corpnewt/pymodules/blob/884c3de15b6a2570afde52fe8a14a3e946ffb18a/run.py
|
||||
|
||||
import sys, subprocess, time, threading, shlex, logging
|
||||
from queue import Queue, Empty
|
||||
|
||||
ON_POSIX = 'posix' in sys.builtin_module_names
|
||||
|
||||
class Run:
|
||||
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def _read_output(self, pipe, q):
|
||||
try:
|
||||
for line in iter(lambda: pipe.read(1), b''):
|
||||
q.put(line)
|
||||
except ValueError:
|
||||
pass
|
||||
pipe.close()
|
||||
|
||||
def _create_thread(self, output):
|
||||
# Creates a new queue and thread object to watch based on the output pipe sent
|
||||
q = Queue()
|
||||
t = threading.Thread(target=self._read_output, args=(output, q))
|
||||
t.daemon = True
|
||||
return (q,t)
|
||||
|
||||
def _stream_output(self, comm, shell = False):
|
||||
output = error = ""
|
||||
p = None
|
||||
try:
|
||||
if shell and type(comm) is list:
|
||||
comm = " ".join(shlex.quote(x) for x in comm)
|
||||
if not shell and type(comm) is str:
|
||||
comm = shlex.split(comm)
|
||||
p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX)
|
||||
# Setup the stdout thread/queue
|
||||
q,t = self._create_thread(p.stdout)
|
||||
qe,te = self._create_thread(p.stderr)
|
||||
# Start both threads
|
||||
t.start()
|
||||
te.start()
|
||||
|
||||
while True:
|
||||
c = z = ""
|
||||
try: c = q.get_nowait()
|
||||
except Empty: pass
|
||||
else:
|
||||
sys.stdout.write(c)
|
||||
output += c
|
||||
sys.stdout.flush()
|
||||
try: z = qe.get_nowait()
|
||||
except Empty: pass
|
||||
else:
|
||||
sys.stderr.write(z)
|
||||
error += z
|
||||
sys.stderr.flush()
|
||||
if not c==z=="": continue # Keep going until empty
|
||||
# No output - see if still running
|
||||
p.poll()
|
||||
if p.returncode != None:
|
||||
# Subprocess ended
|
||||
break
|
||||
# No output, but subprocess still running - stall for 20ms
|
||||
time.sleep(0.02)
|
||||
|
||||
o, e = p.communicate()
|
||||
return (output+o, error+e, p.returncode)
|
||||
except:
|
||||
if p:
|
||||
try: o, e = p.communicate()
|
||||
except: o = e = ""
|
||||
return (output+o, error+e, p.returncode)
|
||||
return ("", "Command not found!", 1)
|
||||
|
||||
def _decode(self, value, encoding="utf-8", errors="ignore"):
|
||||
# Helper method to only decode if bytes type
|
||||
if sys.version_info >= (3,0) and isinstance(value, bytes):
|
||||
return value.decode(encoding,errors)
|
||||
return value
|
||||
|
||||
def _run_command(self, comm, shell = False):
|
||||
c = None
|
||||
try:
|
||||
if shell and type(comm) is list:
|
||||
comm = " ".join(shlex.quote(x) for x in comm)
|
||||
if not shell and type(comm) is str:
|
||||
comm = shlex.split(comm)
|
||||
p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
c = p.communicate()
|
||||
except:
|
||||
if c == None:
|
||||
return ("", "Command not found!", 1)
|
||||
return (self._decode(c[0]), self._decode(c[1]), p.returncode)
|
||||
|
||||
def run(self, command_list, leave_on_fail = False):
|
||||
# Command list should be an array of dicts
|
||||
if type(command_list) is dict:
|
||||
# We only have one command
|
||||
command_list = [command_list]
|
||||
output_list = []
|
||||
for comm in command_list:
|
||||
args = comm.get("args", [])
|
||||
shell = comm.get("shell", False)
|
||||
stream = comm.get("stream", False)
|
||||
sudo = comm.get("sudo", False)
|
||||
stdout = comm.get("stdout", False)
|
||||
stderr = comm.get("stderr", False)
|
||||
mess = comm.get("message", None)
|
||||
show = comm.get("show", False)
|
||||
|
||||
if not mess == None:
|
||||
logging.info(mess)
|
||||
|
||||
if not len(args):
|
||||
# nothing to process
|
||||
continue
|
||||
if sudo:
|
||||
# Check if we have sudo
|
||||
out = self._run_command(["which", "sudo"])
|
||||
if "sudo" in out[0]:
|
||||
# Can sudo
|
||||
if type(args) is list:
|
||||
args.insert(0, out[0].replace("\n", "")) # add to start of list
|
||||
elif type(args) is str:
|
||||
args = out[0].replace("\n", "") + " " + args # add to start of string
|
||||
|
||||
if show:
|
||||
logging.info(" ".join(args))
|
||||
|
||||
if stream:
|
||||
# Stream it!
|
||||
out = self._stream_output(args, shell)
|
||||
else:
|
||||
# Just run and gather output
|
||||
out = self._run_command(args, shell)
|
||||
if stdout and len(out[0]):
|
||||
logging.info(out[0])
|
||||
if stderr and len(out[1]):
|
||||
logging.info(out[1])
|
||||
# Append output
|
||||
output_list.append(out)
|
||||
# Check for errors
|
||||
if leave_on_fail and out[2] != 0:
|
||||
# Got an error - leave
|
||||
break
|
||||
if len(output_list) == 1:
|
||||
# We only ran one command - just return that output
|
||||
return output_list[0]
|
||||
return output_list
|
||||
+121
-114
@@ -46,9 +46,9 @@ from data import os_data
|
||||
|
||||
|
||||
class PatchSysVolume:
|
||||
def __init__(self, model, versions, hardware_details=None):
|
||||
def __init__(self, model: str, global_constants: constants.Constants, hardware_details: list = None):
|
||||
self.model = model
|
||||
self.constants: constants.Constants() = versions
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.computer = self.constants.computer
|
||||
self.root_mount_path = None
|
||||
self.root_supports_snapshot = utilities.check_if_root_is_apfs_snapshot()
|
||||
@@ -61,9 +61,9 @@ class PatchSysVolume:
|
||||
# GUI will detect hardware patches before starting PatchSysVolume()
|
||||
# However the TUI will not, so allow for data to be passed in manually avoiding multiple calls
|
||||
if hardware_details is None:
|
||||
hardware_details = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
hardware_details = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.hardware_details = hardware_details
|
||||
self.init_pathing(custom_root_mount_path=None, custom_data_mount_path=None)
|
||||
self._init_pathing(custom_root_mount_path=None, custom_data_mount_path=None)
|
||||
|
||||
self.skip_root_kmutil_requirement = self.hardware_details["Settings: Supports Auxiliary Cache"]
|
||||
|
||||
@@ -72,7 +72,15 @@ class PatchSysVolume:
|
||||
if Path(self.constants.payload_local_binaries_root_path).exists():
|
||||
shutil.rmtree(self.constants.payload_local_binaries_root_path)
|
||||
|
||||
def init_pathing(self, custom_root_mount_path=None, custom_data_mount_path=None):
|
||||
def _init_pathing(self, custom_root_mount_path: Path = None, custom_data_mount_path: Path = None):
|
||||
"""
|
||||
Initializes the pathing for root volume patching
|
||||
|
||||
Parameters:
|
||||
custom_root_mount_path (Path): Custom path to mount the root volume
|
||||
custom_data_mount_path (Path): Custom path to mount the data volume
|
||||
|
||||
"""
|
||||
if custom_root_mount_path and custom_data_mount_path:
|
||||
self.mount_location = custom_root_mount_path
|
||||
self.data_mount_location = custom_data_mount_path
|
||||
@@ -83,11 +91,12 @@ class PatchSysVolume:
|
||||
else:
|
||||
self.mount_location = ""
|
||||
self.mount_location_data = ""
|
||||
|
||||
self.mount_extensions = f"{self.mount_location}/System/Library/Extensions"
|
||||
self.mount_application_support = f"{self.mount_location_data}/Library/Application Support"
|
||||
|
||||
|
||||
def mount_root_vol(self):
|
||||
def _mount_root_vol(self):
|
||||
# Returns boolean if Root Volume is available
|
||||
self.root_mount_path = utilities.get_disk_path()
|
||||
if self.root_mount_path.startswith("disk"):
|
||||
@@ -113,56 +122,53 @@ class PatchSysVolume:
|
||||
return False
|
||||
|
||||
|
||||
def invoke_kdk_handler(self):
|
||||
# If we're invoked, there is no KDK installed (or something went wrong)
|
||||
kdk_result = False
|
||||
error_msg = ""
|
||||
|
||||
kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version)
|
||||
|
||||
if kdk_obj.success is False:
|
||||
error_msg = kdk_obj.error_msg
|
||||
return kdk_result, error_msg, None
|
||||
|
||||
kdk_download_obj = kdk_obj.retrieve_download()
|
||||
|
||||
# We didn't get a download object, something's wrong
|
||||
if not kdk_download_obj:
|
||||
if kdk_obj.kdk_already_installed is True:
|
||||
error_msg = "KDK already installed, function should not have been invoked"
|
||||
return kdk_result, error_msg, None
|
||||
else:
|
||||
error_msg = "Could not retrieve KDK"
|
||||
return kdk_result, error_msg, None
|
||||
|
||||
# Hold thread until download is complete
|
||||
kdk_download_obj.download(spawn_thread=False)
|
||||
|
||||
if kdk_download_obj.download_complete is False:
|
||||
error_msg = kdk_download_obj.error_msg
|
||||
return kdk_result, error_msg, None
|
||||
|
||||
kdk_result = kdk_obj.validate_kdk_checksum()
|
||||
downloaded_kdk = self.constants.kdk_download_path
|
||||
|
||||
return kdk_result, error_msg, downloaded_kdk
|
||||
|
||||
|
||||
def merge_kdk_with_root(self, save_hid_cs=False):
|
||||
def _merge_kdk_with_root(self, save_hid_cs=False):
|
||||
if self.skip_root_kmutil_requirement is True:
|
||||
return
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
|
||||
downloaded_kdk = None
|
||||
kdk_path = sys_patch_helpers.sys_patch_helpers(self.constants).determine_kdk_present(match_closest=False)
|
||||
if kdk_path is None:
|
||||
if not self.constants.kdk_download_path.exists():
|
||||
kdk_result, error_msg, downloaded_kdk = self.invoke_kdk_handler()
|
||||
if kdk_result is False:
|
||||
raise Exception(f"Unable to download KDK: {error_msg}")
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).install_kdk()
|
||||
kdk_path = sys_patch_helpers.sys_patch_helpers(self.constants).determine_kdk_present(match_closest=True, override_build=downloaded_kdk)
|
||||
if self.constants.kdk_download_path.exists():
|
||||
if kdk_handler.KernelDebugKitUtilities().install_kdk_dmg(self.constants.kdk_download_path) is False:
|
||||
logging.info("Failed to install KDK")
|
||||
raise Exception("Failed to install KDK")
|
||||
|
||||
kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version)
|
||||
if kdk_obj.success is False:
|
||||
logging.info(f"Unable to get KDK info: {kdk_obj.error_msg}")
|
||||
raise Exception(f"Unable to get KDK info: {kdk_obj.error_msg}")
|
||||
|
||||
if kdk_obj.kdk_already_installed is False:
|
||||
|
||||
kdk_download_obj = kdk_obj.retrieve_download()
|
||||
if not kdk_download_obj:
|
||||
logging.info(f"Could not retrieve KDK: {kdk_obj.error_msg}")
|
||||
|
||||
# Hold thread until download is complete
|
||||
kdk_download_obj.download(spawn_thread=False)
|
||||
|
||||
if kdk_download_obj.download_complete is False:
|
||||
error_msg = kdk_download_obj.error_msg
|
||||
logging.info(f"Could not download KDK: {error_msg}")
|
||||
raise Exception(f"Could not download KDK: {error_msg}")
|
||||
|
||||
if kdk_obj.validate_kdk_checksum() is False:
|
||||
logging.info(f"KDK checksum validation failed: {kdk_obj.error_msg}")
|
||||
raise Exception(f"KDK checksum validation failed: {kdk_obj.error_msg}")
|
||||
|
||||
kdk_handler.KernelDebugKitUtilities().install_kdk_dmg(self.constants.kdk_download_path)
|
||||
# re-init kdk_obj to get the new kdk_installed_path
|
||||
kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version)
|
||||
if kdk_obj.success is False:
|
||||
logging.info(f"Unable to get KDK info: {kdk_obj.error_msg}")
|
||||
raise Exception(f"Unable to get KDK info: {kdk_obj.error_msg}")
|
||||
|
||||
if kdk_obj.kdk_already_installed is False:
|
||||
# We shouldn't get here, but just in case
|
||||
logging.warning(f"KDK was not installed, but should have been: {kdk_obj.error_msg}")
|
||||
raise Exception("KDK was not installed, but should have been: {kdk_obj.error_msg}")
|
||||
|
||||
kdk_path = Path(kdk_obj.kdk_installed_path) if kdk_obj.kdk_installed_path != "" else None
|
||||
|
||||
oclp_plist = Path("/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist")
|
||||
if (Path(self.mount_location) / Path("System/Library/Extensions/System.kext/PlugIns/Libkern.kext/Libkern")).exists() and oclp_plist.exists():
|
||||
@@ -178,7 +184,7 @@ class PatchSysVolume:
|
||||
pass
|
||||
|
||||
if kdk_path is None:
|
||||
logging.info(f"- Unable to find Kernel Debug Kit: {downloaded_kdk}")
|
||||
logging.info(f"- Unable to find Kernel Debug Kit")
|
||||
raise Exception("Unable to find Kernel Debug Kit")
|
||||
self.kdk_path = kdk_path
|
||||
logging.info(f"- Found KDK at: {kdk_path}")
|
||||
@@ -190,7 +196,7 @@ class PatchSysVolume:
|
||||
# Note it's a folder, not a file
|
||||
utilities.elevated(["cp", "-r", cs_path, f"{self.constants.payload_path}/IOHIDEventDriver_CodeSignature.bak"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
logging.info("- Merging KDK with Root Volume")
|
||||
logging.info(f"- Merging KDK with Root Volume: {kdk_path.name}")
|
||||
utilities.elevated(
|
||||
# Only merge '/System/Library/Extensions'
|
||||
# 'Kernels' and 'KernelSupport' is wasted space for root patching (we don't care above dev kernels)
|
||||
@@ -214,7 +220,7 @@ class PatchSysVolume:
|
||||
utilities.elevated(["rm", "-rf", f"{self.constants.payload_path}/IOHIDEventDriver_CodeSignature.bak"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
|
||||
def unpatch_root_vol(self):
|
||||
def _unpatch_root_vol(self):
|
||||
if self.constants.detected_os > os_data.os_data.catalina and self.root_supports_snapshot is True:
|
||||
logging.info("- Reverting to last signed APFS snapshot")
|
||||
result = utilities.elevated(["bless", "--mount", self.mount_location, "--bootefi", "--last-sealed-snapshot"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
@@ -224,18 +230,18 @@ class PatchSysVolume:
|
||||
logging.info(result.stdout.decode())
|
||||
logging.info("- Failed to revert snapshot via Apple's 'bless' command")
|
||||
else:
|
||||
self.clean_skylight_plugins()
|
||||
self.delete_nonmetal_enforcement()
|
||||
self.clean_auxiliary_kc()
|
||||
self._clean_skylight_plugins()
|
||||
self._delete_nonmetal_enforcement()
|
||||
self._clean_auxiliary_kc()
|
||||
self.constants.root_patcher_succeeded = True
|
||||
logging.info("- Unpatching complete")
|
||||
logging.info("\nPlease reboot the machine for patches to take effect")
|
||||
|
||||
def rebuild_snapshot(self):
|
||||
if self.rebuild_kernel_collection() is True:
|
||||
def _rebuild_snapshot(self):
|
||||
if self._rebuild_kernel_collection() is True:
|
||||
self.update_preboot_kernel_cache()
|
||||
self.rebuild_dyld_shared_cache()
|
||||
if self.create_new_apfs_snapshot() is True:
|
||||
self._rebuild_dyld_shared_cache()
|
||||
if self._create_new_apfs_snapshot() is True:
|
||||
logging.info("- Patching complete")
|
||||
logging.info("\nPlease reboot the machine for patches to take effect")
|
||||
if self.needs_kmutil_exemptions is True:
|
||||
@@ -244,7 +250,7 @@ class PatchSysVolume:
|
||||
if self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to continue")
|
||||
|
||||
def rebuild_kernel_collection(self):
|
||||
def _rebuild_kernel_collection(self):
|
||||
logging.info("- Rebuilding Kernel Cache (This may take some time)")
|
||||
if self.constants.detected_os > os_data.os_data.catalina:
|
||||
# Base Arguments
|
||||
@@ -331,15 +337,15 @@ class PatchSysVolume:
|
||||
return False
|
||||
|
||||
for file in ["KextPolicy", "KextPolicy-shm", "KextPolicy-wal"]:
|
||||
self.remove_file("/private/var/db/SystemPolicyConfiguration/", file)
|
||||
self._remove_file("/private/var/db/SystemPolicyConfiguration/", file)
|
||||
else:
|
||||
# Install RSRHelper utility to handle desynced KCs
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).install_rsr_repair_binary()
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).install_rsr_repair_binary()
|
||||
|
||||
logging.info("- Successfully built new kernel cache")
|
||||
return True
|
||||
|
||||
def create_new_apfs_snapshot(self):
|
||||
def _create_new_apfs_snapshot(self):
|
||||
if self.root_supports_snapshot is True:
|
||||
logging.info("- Creating new APFS snapshot")
|
||||
bless = utilities.elevated(
|
||||
@@ -356,24 +362,25 @@ class PatchSysVolume:
|
||||
if "Can't use last-sealed-snapshot or create-snapshot on non system volume" in bless.stdout.decode():
|
||||
logging.info("- This is an APFS bug with Monterey and newer! Perform a clean installation to ensure your APFS volume is built correctly")
|
||||
return False
|
||||
self.unmount_drive()
|
||||
self._unmount_drive()
|
||||
return True
|
||||
|
||||
def unmount_drive(self):
|
||||
def _unmount_drive(self):
|
||||
logging.info("- Unmounting Root Volume (Don't worry if this fails)")
|
||||
utilities.elevated(["diskutil", "unmount", self.root_mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode()
|
||||
|
||||
def rebuild_dyld_shared_cache(self):
|
||||
if self.constants.detected_os <= os_data.os_data.catalina:
|
||||
logging.info("- Rebuilding dyld shared cache")
|
||||
utilities.process_status(utilities.elevated(["update_dyld_shared_cache", "-root", f"{self.mount_location}/"]))
|
||||
def _rebuild_dyld_shared_cache(self):
|
||||
if self.constants.detected_os > os_data.os_data.catalina:
|
||||
return
|
||||
logging.info("- Rebuilding dyld shared cache")
|
||||
utilities.process_status(utilities.elevated(["update_dyld_shared_cache", "-root", f"{self.mount_location}/"]))
|
||||
|
||||
def update_preboot_kernel_cache(self):
|
||||
if self.constants.detected_os == os_data.os_data.catalina:
|
||||
logging.info("- Rebuilding preboot kernel cache")
|
||||
utilities.process_status(utilities.elevated(["kcditto"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
def clean_skylight_plugins(self):
|
||||
def _clean_skylight_plugins(self):
|
||||
if (Path(self.mount_application_support) / Path("SkyLightPlugins/")).exists():
|
||||
logging.info("- Found SkylightPlugins folder, removing old plugins")
|
||||
utilities.process_status(utilities.elevated(["rm", "-Rf", f"{self.mount_application_support}/SkyLightPlugins"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
@@ -382,14 +389,14 @@ class PatchSysVolume:
|
||||
logging.info("- Creating SkylightPlugins folder")
|
||||
utilities.process_status(utilities.elevated(["mkdir", "-p", f"{self.mount_application_support}/SkyLightPlugins/"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
def delete_nonmetal_enforcement(self):
|
||||
def _delete_nonmetal_enforcement(self):
|
||||
for arg in ["useMetal", "useIOP"]:
|
||||
result = subprocess.run(["defaults", "read", "/Library/Preferences/com.apple.CoreDisplay", arg], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode("utf-8").strip()
|
||||
if result in ["0", "false", "1", "true"]:
|
||||
logging.info(f"- Removing non-Metal Enforcement Preference: {arg}")
|
||||
utilities.elevated(["defaults", "delete", "/Library/Preferences/com.apple.CoreDisplay", arg])
|
||||
|
||||
def clean_auxiliary_kc(self):
|
||||
def _clean_auxiliary_kc(self):
|
||||
# When reverting root volume patches, the AuxKC will still retain the UUID
|
||||
# it was built against. Thus when Boot/SysKC are reverted, Aux will break
|
||||
# To resolve this, delete all installed kexts in /L*/E* and rebuild the AuxKC
|
||||
@@ -410,7 +417,7 @@ class PatchSysVolume:
|
||||
for file in oclp_plist_data[key]["Install"][location]:
|
||||
if not file.endswith(".kext"):
|
||||
continue
|
||||
self.remove_file("/Library/Extensions", file)
|
||||
self._remove_file("/Library/Extensions", file)
|
||||
|
||||
# Handle situations where users migrated from older OSes with a lot of garbage in /L*/E*
|
||||
# ex. Nvidia Web Drivers, NetUSB, dosdude1's patches, etc.
|
||||
@@ -434,17 +441,17 @@ class PatchSysVolume:
|
||||
# ex. Symlinks pointing to symlinks pointing to dead files
|
||||
pass
|
||||
|
||||
def write_patchset(self, patchset):
|
||||
def _write_patchset(self, patchset):
|
||||
destination_path = f"{self.mount_location}/System/Library/CoreServices"
|
||||
file_name = "OpenCore-Legacy-Patcher.plist"
|
||||
destination_path_file = f"{destination_path}/{file_name}"
|
||||
if sys_patch_helpers.sys_patch_helpers(self.constants).generate_patchset_plist(patchset, file_name, self.kdk_path):
|
||||
if sys_patch_helpers.SysPatchHelpers(self.constants).generate_patchset_plist(patchset, file_name, self.kdk_path):
|
||||
logging.info("- Writing patchset information to Root Volume")
|
||||
if Path(destination_path_file).exists():
|
||||
utilities.process_status(utilities.elevated(["rm", destination_path_file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
utilities.process_status(utilities.elevated(["cp", f"{self.constants.payload_path}/{file_name}", destination_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
def add_auxkc_support(self, install_file, source_folder_path, install_patch_directory, destination_folder_path):
|
||||
def _add_auxkc_support(self, install_file, source_folder_path, install_patch_directory, destination_folder_path):
|
||||
# In macOS Ventura, KDKs are required to build new Boot and System KCs
|
||||
# However for some patch sets, we're able to use the Auxiliary KCs with '/Library/Extensions'
|
||||
|
||||
@@ -480,11 +487,11 @@ class PatchSysVolume:
|
||||
plist_data["OSBundleRequired"] = "Auxiliary"
|
||||
plistlib.dump(plist_data, plist_path.open("wb"))
|
||||
|
||||
self.check_kexts_needs_authentication(install_file)
|
||||
self._check_kexts_needs_authentication(install_file)
|
||||
|
||||
return updated_install_location
|
||||
|
||||
def check_kexts_needs_authentication(self, kext_name):
|
||||
def _check_kexts_needs_authentication(self, kext_name):
|
||||
# Verify whether the user needs to authenticate in System Preferences
|
||||
# Specifically under 'private/var/db/KernelManagement/AuxKC/CurrentAuxKC/com.apple.kcgen.instructions.plist'
|
||||
# ["kextsToBuild"][i]:
|
||||
@@ -506,21 +513,21 @@ class PatchSysVolume:
|
||||
logging.info(f" - {kext_name} requires authentication in System Preferences")
|
||||
self.constants.needs_to_open_preferences = True # Notify in GUI to open System Preferences
|
||||
|
||||
def patch_root_vol(self):
|
||||
def _patch_root_vol(self):
|
||||
logging.info(f"- Running patches for {self.model}")
|
||||
if self.patch_set_dictionary != {}:
|
||||
self.execute_patchset(self.patch_set_dictionary)
|
||||
self._execute_patchset(self.patch_set_dictionary)
|
||||
else:
|
||||
self.execute_patchset(sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).generate_patchset(self.hardware_details))
|
||||
self._execute_patchset(sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).generate_patchset(self.hardware_details))
|
||||
|
||||
if self.constants.wxpython_variant is True and self.constants.detected_os >= os_data.os_data.big_sur:
|
||||
sys_patch_auto.AutomaticSysPatch(self.constants).install_auto_patcher_launch_agent()
|
||||
|
||||
self.rebuild_snapshot()
|
||||
self._rebuild_snapshot()
|
||||
|
||||
def execute_patchset(self, required_patches):
|
||||
def _execute_patchset(self, required_patches):
|
||||
source_files_path = str(self.constants.payload_local_binaries_root_path)
|
||||
self.preflight_checks(required_patches, source_files_path)
|
||||
self._preflight_checks(required_patches, source_files_path)
|
||||
for patch in required_patches:
|
||||
logging.info("- Installing Patchset: " + patch)
|
||||
if "Remove" in required_patches[patch]:
|
||||
@@ -528,7 +535,7 @@ class PatchSysVolume:
|
||||
logging.info("- Remove Files at: " + remove_patch_directory)
|
||||
for remove_patch_file in required_patches[patch]["Remove"][remove_patch_directory]:
|
||||
destination_folder_path = str(self.mount_location) + remove_patch_directory
|
||||
self.remove_file(destination_folder_path, remove_patch_file)
|
||||
self._remove_file(destination_folder_path, remove_patch_file)
|
||||
|
||||
|
||||
for method_install in ["Install", "Install Non-Root"]:
|
||||
@@ -542,10 +549,10 @@ class PatchSysVolume:
|
||||
else:
|
||||
if install_patch_directory == "/Library/Extensions":
|
||||
self.needs_kmutil_exemptions = True
|
||||
self.check_kexts_needs_authentication(install_file)
|
||||
self._check_kexts_needs_authentication(install_file)
|
||||
destination_folder_path = str(self.mount_location_data) + install_patch_directory
|
||||
|
||||
updated_destination_folder_path = self.add_auxkc_support(install_file, source_folder_path, install_patch_directory, destination_folder_path)
|
||||
updated_destination_folder_path = self._add_auxkc_support(install_file, source_folder_path, install_patch_directory, destination_folder_path)
|
||||
|
||||
if destination_folder_path != updated_destination_folder_path:
|
||||
# Update required_patches to reflect the new destination folder path
|
||||
@@ -556,7 +563,7 @@ class PatchSysVolume:
|
||||
|
||||
destination_folder_path = updated_destination_folder_path
|
||||
|
||||
self.install_new_file(source_folder_path, destination_folder_path, install_file)
|
||||
self._install_new_file(source_folder_path, destination_folder_path, install_file)
|
||||
|
||||
if "Processes" in required_patches[patch]:
|
||||
for process in required_patches[patch]["Processes"]:
|
||||
@@ -569,24 +576,24 @@ class PatchSysVolume:
|
||||
logging.info(f"- Running Process:\n{process}")
|
||||
utilities.process_status(subprocess.run(process, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True))
|
||||
if any(x in required_patches for x in ["AMD Legacy GCN", "AMD Legacy Polaris", "AMD Legacy Vega"]):
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).disable_window_server_caching()
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).disable_window_server_caching()
|
||||
if any(x in required_patches for x in ["Intel Ivy Bridge", "Intel Haswell"]):
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).remove_news_widgets()
|
||||
self.write_patchset(required_patches)
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).remove_news_widgets()
|
||||
self._write_patchset(required_patches)
|
||||
|
||||
def preflight_checks(self, required_patches, source_files_path):
|
||||
def _preflight_checks(self, required_patches, source_files_path):
|
||||
logging.info("- Running Preflight Checks before patching")
|
||||
|
||||
# Make sure old SkyLight plugins aren't being used
|
||||
self.clean_skylight_plugins()
|
||||
self._clean_skylight_plugins()
|
||||
# Make sure non-Metal Enforcement preferences are not present
|
||||
self.delete_nonmetal_enforcement()
|
||||
self._delete_nonmetal_enforcement()
|
||||
# Make sure we clean old kexts in /L*/E* that are not in the patchset
|
||||
self.clean_auxiliary_kc()
|
||||
self._clean_auxiliary_kc()
|
||||
|
||||
# Make sure SNB kexts are compatible with the host
|
||||
if "Intel Sandy Bridge" in required_patches:
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).snb_board_id_patch(source_files_path)
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).snb_board_id_patch(source_files_path)
|
||||
|
||||
for patch in required_patches:
|
||||
# Check if all files are present
|
||||
@@ -602,11 +609,11 @@ class PatchSysVolume:
|
||||
should_save_cs = False
|
||||
if "Legacy USB 1.1" in required_patches:
|
||||
should_save_cs = True
|
||||
self.merge_kdk_with_root(save_hid_cs=should_save_cs)
|
||||
self._merge_kdk_with_root(save_hid_cs=should_save_cs)
|
||||
|
||||
logging.info("- Finished Preflight, starting patching")
|
||||
|
||||
def install_new_file(self, source_folder, destination_folder, file_name):
|
||||
def _install_new_file(self, source_folder, destination_folder, file_name):
|
||||
# .frameworks are merged
|
||||
# .kexts and .apps are deleted and replaced
|
||||
file_name_str = str(file_name)
|
||||
@@ -619,7 +626,7 @@ class PatchSysVolume:
|
||||
# merge with rsync
|
||||
logging.info(f" - Installing: {file_name}")
|
||||
utilities.elevated(["rsync", "-r", "-i", "-a", f"{source_folder}/{file_name}", f"{destination_folder}/"], stdout=subprocess.PIPE)
|
||||
self.fix_permissions(destination_folder + "/" + file_name)
|
||||
self._fix_permissions(destination_folder + "/" + file_name)
|
||||
elif Path(source_folder + "/" + file_name_str).is_dir():
|
||||
# Applicable for .kext, .app, .plugin, .bundle, all of which are directories
|
||||
if Path(destination_folder + "/" + file_name).exists():
|
||||
@@ -628,7 +635,7 @@ class PatchSysVolume:
|
||||
else:
|
||||
logging.info(f" - Installing: {file_name}")
|
||||
utilities.process_status(utilities.elevated(["cp", "-R", f"{source_folder}/{file_name}", destination_folder], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
self.fix_permissions(destination_folder + "/" + file_name)
|
||||
self._fix_permissions(destination_folder + "/" + file_name)
|
||||
else:
|
||||
# Assume it's an individual file, replace as normal
|
||||
if Path(destination_folder + "/" + file_name).exists():
|
||||
@@ -637,9 +644,9 @@ class PatchSysVolume:
|
||||
else:
|
||||
logging.info(f" - Installing: {file_name}")
|
||||
utilities.process_status(utilities.elevated(["cp", f"{source_folder}/{file_name}", destination_folder], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
self.fix_permissions(destination_folder + "/" + file_name)
|
||||
self._fix_permissions(destination_folder + "/" + file_name)
|
||||
|
||||
def remove_file(self, destination_folder, file_name):
|
||||
def _remove_file(self, destination_folder, file_name):
|
||||
if Path(destination_folder + "/" + file_name).exists():
|
||||
logging.info(f" - Removing: {file_name}")
|
||||
if Path(destination_folder + "/" + file_name).is_dir():
|
||||
@@ -648,7 +655,7 @@ class PatchSysVolume:
|
||||
utilities.process_status(utilities.elevated(["rm", f"{destination_folder}/{file_name}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
|
||||
def fix_permissions(self, destination_file):
|
||||
def _fix_permissions(self, destination_file):
|
||||
chmod_args = ["chmod", "-Rf", "755", destination_file]
|
||||
chown_args = ["chown", "-Rf", "root:wheel", destination_file]
|
||||
if not Path(destination_file).is_dir():
|
||||
@@ -659,7 +666,7 @@ class PatchSysVolume:
|
||||
utilities.process_status(utilities.elevated(chown_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
|
||||
def check_files(self):
|
||||
def _check_files(self):
|
||||
if Path(self.constants.payload_local_binaries_root_path).exists():
|
||||
logging.info("- Local PatcherSupportPkg resources available, continuing...")
|
||||
return True
|
||||
@@ -678,7 +685,7 @@ class PatchSysVolume:
|
||||
def start_patch(self):
|
||||
logging.info("- Starting Patch Process")
|
||||
logging.info(f"- Determining Required Patch set for Darwin {self.constants.detected_os}")
|
||||
self.patch_set_dictionary = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).generate_patchset(self.hardware_details)
|
||||
self.patch_set_dictionary = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).generate_patchset(self.hardware_details)
|
||||
|
||||
if self.patch_set_dictionary == {}:
|
||||
change_menu = None
|
||||
@@ -692,11 +699,11 @@ class PatchSysVolume:
|
||||
logging.info("- Continuing root patching")
|
||||
if change_menu in ["y", "Y"]:
|
||||
logging.info("- Verifying whether Root Patching possible")
|
||||
if sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).verify_patch_allowed(print_errors=not self.constants.wxpython_variant) is True:
|
||||
if sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).verify_patch_allowed(print_errors=not self.constants.wxpython_variant) is True:
|
||||
logging.info("- Patcher is capable of patching")
|
||||
if self.check_files():
|
||||
if self.mount_root_vol() is True:
|
||||
self.patch_root_vol()
|
||||
if self._check_files():
|
||||
if self._mount_root_vol() is True:
|
||||
self._patch_root_vol()
|
||||
if self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to return to the main menu")
|
||||
else:
|
||||
@@ -711,9 +718,9 @@ class PatchSysVolume:
|
||||
|
||||
def start_unpatch(self):
|
||||
logging.info("- Starting Unpatch Process")
|
||||
if sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).verify_patch_allowed(print_errors=True) is True:
|
||||
if self.mount_root_vol() is True:
|
||||
self.unpatch_root_vol()
|
||||
if sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).verify_patch_allowed(print_errors=True) is True:
|
||||
if self._mount_root_vol() is True:
|
||||
self._unpatch_root_vol()
|
||||
if self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to return to the main menu")
|
||||
else:
|
||||
|
||||
@@ -1,29 +1,42 @@
|
||||
# Auto Patching's main purpose is to try and tell the user they're missing root patches
|
||||
# New users may not realize OS updates remove our patches, so we try and run when nessasary
|
||||
# Conditions for running:
|
||||
# - Verify running GUI (TUI users can write their own scripts)
|
||||
# - Verify the Snapshot Seal is intact (if not, assume user is running patches)
|
||||
# - Verify this model needs patching (if not, assume user upgraded hardware and OCLP was not removed)
|
||||
# - Verify there are no updates for OCLP (ensure we have the latest patch sets)
|
||||
# If all these tests pass, start Root Patcher
|
||||
# Copyright (C) 2022, Mykola Grymalyuk
|
||||
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import logging
|
||||
from resources import utilities, updates, global_settings, network_handler
|
||||
from pathlib import Path
|
||||
|
||||
from resources import utilities, updates, global_settings, network_handler, constants
|
||||
from resources.sys_patch import sys_patch_detect
|
||||
from resources.gui import gui_main
|
||||
|
||||
class AutomaticSysPatch:
|
||||
|
||||
def __init__(self, constants):
|
||||
self.constants = constants
|
||||
class AutomaticSysPatch:
|
||||
"""
|
||||
Library of functions for launch agent, including automatic patching
|
||||
"""
|
||||
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
|
||||
def start_auto_patch(self):
|
||||
"""
|
||||
Initiates automatic patching
|
||||
|
||||
Auto Patching's main purpose is to try and tell the user they're missing root patches
|
||||
New users may not realize OS updates remove our patches, so we try and run when nessasary
|
||||
|
||||
Conditions for running:
|
||||
- Verify running GUI (TUI users can write their own scripts)
|
||||
- Verify the Snapshot Seal is intact (if not, assume user is running patches)
|
||||
- Verify this model needs patching (if not, assume user upgraded hardware and OCLP was not removed)
|
||||
- Verify there are no updates for OCLP (ensure we have the latest patch sets)
|
||||
|
||||
If all these tests pass, start Root Patcher
|
||||
|
||||
"""
|
||||
|
||||
logging.info("- Starting Automatic Patching")
|
||||
if self.constants.wxpython_variant is False:
|
||||
logging.info("- Auto Patch option is not supported on TUI, please use GUI")
|
||||
@@ -31,7 +44,7 @@ class AutomaticSysPatch:
|
||||
|
||||
if utilities.check_seal() is True:
|
||||
logging.info("- Detected Snapshot seal intact, detecting patches")
|
||||
patches = sys_patch_detect.detect_root_patch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
if not any(not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True for patch in patches):
|
||||
patches = []
|
||||
if patches:
|
||||
@@ -46,7 +59,7 @@ class AutomaticSysPatch:
|
||||
if patches[patch] is True and not patch.startswith("Settings") and not patch.startswith("Validation"):
|
||||
patch_string += f"- {patch}\n"
|
||||
# Check for updates
|
||||
dict = updates.check_binary_updates(self.constants).check_binary_updates()
|
||||
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
|
||||
if not dict:
|
||||
logging.info("- No new binaries found on Github, proceeding with patching")
|
||||
if self.constants.launcher_script is None:
|
||||
@@ -113,26 +126,35 @@ class AutomaticSysPatch:
|
||||
else:
|
||||
logging.info("- Detected Snapshot seal not intact, skipping")
|
||||
|
||||
if self.determine_if_versions_match() is False:
|
||||
self.determine_if_boot_matches()
|
||||
if self._determine_if_versions_match():
|
||||
self._determine_if_boot_matches()
|
||||
|
||||
|
||||
def determine_if_versions_match(self):
|
||||
def _determine_if_versions_match(self):
|
||||
"""
|
||||
Determine if the booted version of OCLP matches the installed version
|
||||
|
||||
ie. Installed app is 0.2.0, but EFI version is 0.1.0
|
||||
|
||||
Returns:
|
||||
bool: True if versions match, False if not
|
||||
"""
|
||||
|
||||
logging.info("- Checking booted vs installed OCLP Build")
|
||||
if self.constants.computer.oclp_version is None:
|
||||
logging.info("- Booted version not found")
|
||||
return False
|
||||
return True
|
||||
|
||||
if self.constants.computer.oclp_version == self.constants.patcher_version:
|
||||
logging.info("- Versions match")
|
||||
return False
|
||||
return True
|
||||
|
||||
# Check if installed version is newer than booted version
|
||||
if updates.check_binary_updates(self.constants).check_if_build_newer(
|
||||
if updates.CheckBinaryUpdates(self.constants)._check_if_build_newer(
|
||||
self.constants.computer.oclp_version.split("."), self.constants.patcher_version.split(".")
|
||||
) is True:
|
||||
logging.info("- Installed version is newer than booted version")
|
||||
return False
|
||||
return True
|
||||
|
||||
args = [
|
||||
"osascript",
|
||||
@@ -150,18 +172,25 @@ class AutomaticSysPatch:
|
||||
self.constants.start_build_install = True
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def determine_if_boot_matches(self):
|
||||
# Goal of this function is to determine whether the user
|
||||
# is using a USB drive to Boot OpenCore but macOS does not
|
||||
# reside on the same drive as the USB.
|
||||
|
||||
# If we determine them to be mismatched, notify the user
|
||||
# and ask if they want to install to install to disk
|
||||
def _determine_if_boot_matches(self):
|
||||
"""
|
||||
Determine if the boot drive matches the macOS drive
|
||||
ie. Booted from USB, but macOS is on internal disk
|
||||
|
||||
Goal of this function is to determine whether the user
|
||||
is using a USB drive to Boot OpenCore but macOS does not
|
||||
reside on the same drive as the USB.
|
||||
|
||||
If we determine them to be mismatched, notify the user
|
||||
and ask if they want to install to install to disk.
|
||||
"""
|
||||
|
||||
logging.info("- Determining if macOS drive matches boot drive")
|
||||
should_notify = global_settings.global_settings().read_property("AutoPatch_Notify_Mismatched_Disks")
|
||||
|
||||
should_notify = global_settings.GlobalEnviromentSettings().read_property("AutoPatch_Notify_Mismatched_Disks")
|
||||
if should_notify is False:
|
||||
logging.info("- Skipping due to user preference")
|
||||
return
|
||||
@@ -223,9 +252,16 @@ class AutomaticSysPatch:
|
||||
|
||||
|
||||
def install_auto_patcher_launch_agent(self):
|
||||
# Installs the following:
|
||||
# - OpenCore-Patcher.app in /Library/Application Support/Dortania/
|
||||
# - com.dortania.opencore-legacy-patcher.auto-patch.plist in /Library/LaunchAgents/
|
||||
"""
|
||||
Install the Auto Patcher Launch Agent
|
||||
|
||||
Installs the following:
|
||||
- OpenCore-Patcher.app in /Library/Application Support/Dortania/
|
||||
- com.dortania.opencore-legacy-patcher.auto-patch.plist in /Library/LaunchAgents/
|
||||
|
||||
See start_auto_patch() comments for more info
|
||||
"""
|
||||
|
||||
if self.constants.launcher_script is not None:
|
||||
logging.info("- Skipping Auto Patcher Launch Agent, not supported when running from source")
|
||||
return
|
||||
|
||||
@@ -3,19 +3,39 @@
|
||||
# Used when supplying data to sys_patch.py
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
|
||||
from resources import constants, device_probe, utilities, amfi_detect, network_handler
|
||||
from resources.sys_patch import sys_patch_helpers
|
||||
from data import model_array, os_data, sip_data, sys_patch_dict, smbios_data, cpu_data
|
||||
|
||||
import py_sip_xnu
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import logging
|
||||
import py_sip_xnu
|
||||
from pathlib import Path
|
||||
|
||||
from resources import (
|
||||
constants,
|
||||
device_probe,
|
||||
utilities,
|
||||
amfi_detect,
|
||||
network_handler,
|
||||
kdk_handler
|
||||
)
|
||||
from data import (
|
||||
model_array,
|
||||
os_data,
|
||||
sip_data,
|
||||
sys_patch_dict,
|
||||
smbios_data,
|
||||
cpu_data
|
||||
)
|
||||
|
||||
|
||||
class DetectRootPatch:
|
||||
"""
|
||||
Library for querying root volume patches applicable for booted system
|
||||
"""
|
||||
|
||||
def __init__(self, model: str, global_constants: constants.Constants):
|
||||
self.model: str = model
|
||||
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
class detect_root_patch:
|
||||
def __init__(self, model, versions):
|
||||
self.model = model
|
||||
self.constants: constants.Constants() = versions
|
||||
self.computer = self.constants.computer
|
||||
|
||||
# GPU Patch Detection
|
||||
@@ -63,7 +83,12 @@ class detect_root_patch:
|
||||
self.missing_nv_web_opengl = False
|
||||
self.missing_nv_compat = False
|
||||
|
||||
def detect_gpus(self):
|
||||
|
||||
def _detect_gpus(self):
|
||||
"""
|
||||
Query GPUs and set flags for applicable patches
|
||||
"""
|
||||
|
||||
gpus = self.constants.computer.gpus
|
||||
non_metal_os = os_data.os_data.catalina
|
||||
for i, gpu in enumerate(gpus):
|
||||
@@ -75,7 +100,7 @@ class detect_root_patch:
|
||||
self.amfi_must_disable = True
|
||||
if os_data.os_data.ventura in self.constants.legacy_accel_support:
|
||||
self.amfi_shim_bins = True
|
||||
self.legacy_keyboard_backlight = self.check_legacy_keyboard_backlight()
|
||||
self.legacy_keyboard_backlight = self._check_legacy_keyboard_backlight()
|
||||
self.requires_root_kc = True
|
||||
elif gpu.arch == device_probe.NVIDIA.Archs.Kepler and self.constants.force_nv_web is False:
|
||||
if self.constants.detected_os > os_data.os_data.big_sur:
|
||||
@@ -165,7 +190,7 @@ class detect_root_patch:
|
||||
self.amfi_must_disable = True
|
||||
if os_data.os_data.ventura in self.constants.legacy_accel_support:
|
||||
self.amfi_shim_bins = True
|
||||
self.legacy_keyboard_backlight = self.check_legacy_keyboard_backlight()
|
||||
self.legacy_keyboard_backlight = self._check_legacy_keyboard_backlight()
|
||||
self.requires_root_kc = True
|
||||
elif gpu.arch == device_probe.Intel.Archs.Sandy_Bridge:
|
||||
if self.constants.detected_os > non_metal_os:
|
||||
@@ -173,7 +198,7 @@ class detect_root_patch:
|
||||
self.amfi_must_disable = True
|
||||
if os_data.os_data.ventura in self.constants.legacy_accel_support:
|
||||
self.amfi_shim_bins = True
|
||||
self.legacy_keyboard_backlight = self.check_legacy_keyboard_backlight()
|
||||
self.legacy_keyboard_backlight = self._check_legacy_keyboard_backlight()
|
||||
self.requires_root_kc = True
|
||||
elif gpu.arch == device_probe.Intel.Archs.Ivy_Bridge:
|
||||
if self.constants.detected_os > os_data.os_data.big_sur:
|
||||
@@ -220,17 +245,21 @@ class detect_root_patch:
|
||||
self.requires_root_kc = True
|
||||
else:
|
||||
if self.requires_root_kc is True:
|
||||
self.missing_kdk = not self.check_kdk()
|
||||
self.missing_kdk = not self._check_kdk()
|
||||
|
||||
self.check_networking_support()
|
||||
self._check_networking_support()
|
||||
|
||||
|
||||
def check_networking_support(self):
|
||||
# On macOS Ventura, networking support is required to download KDKs.
|
||||
# However for machines such as BCM94322, BCM94328 and Atheros chipsets,
|
||||
# users may only have wifi as their only supported network interface.
|
||||
# Thus we'll allow for KDK-less installs for these machines on first run.
|
||||
# On subsequent runs, we'll require networking to be enabled.
|
||||
def _check_networking_support(self):
|
||||
"""
|
||||
Query for network requirement, ex. KDK downloading
|
||||
|
||||
On macOS Ventura, networking support is required to download KDKs.
|
||||
However for machines such as BCM94322, BCM94328 and Atheros chipsets,
|
||||
users may only have wifi as their only supported network interface.
|
||||
Thus we'll allow for KDK-less installs for these machines on first run.
|
||||
On subsequent runs, we'll require networking to be enabled.
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
@@ -272,7 +301,11 @@ class detect_root_patch:
|
||||
self.legacy_keyboard_backlight = False
|
||||
|
||||
|
||||
def check_dgpu_status(self):
|
||||
def _check_dgpu_status(self):
|
||||
"""
|
||||
Query whether system has an active dGPU
|
||||
"""
|
||||
|
||||
dgpu = self.constants.computer.dgpu
|
||||
if dgpu:
|
||||
if dgpu.class_code and dgpu.class_code == 0xFFFFFFFF:
|
||||
@@ -281,25 +314,45 @@ class detect_root_patch:
|
||||
return True
|
||||
return False
|
||||
|
||||
def detect_demux(self):
|
||||
|
||||
def _detect_demux(self):
|
||||
"""
|
||||
Query whether system has been demuxed (ex. MacBookPro8,2, disabled dGPU)
|
||||
"""
|
||||
|
||||
# If GFX0 is missing, assume machine was demuxed
|
||||
# -wegnoegpu would also trigger this, so ensure arg is not present
|
||||
if not "-wegnoegpu" in (utilities.get_nvram("boot-args", decode=True) or ""):
|
||||
igpu = self.constants.computer.igpu
|
||||
dgpu = self.check_dgpu_status()
|
||||
dgpu = self._check_dgpu_status()
|
||||
if igpu and not dgpu:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_legacy_keyboard_backlight(self):
|
||||
|
||||
def _check_legacy_keyboard_backlight(self):
|
||||
"""
|
||||
Query whether system has a legacy keyboard backlight
|
||||
|
||||
Returns:
|
||||
bool: True if legacy keyboard backlight, False otherwise
|
||||
"""
|
||||
|
||||
# iMac12,x+ have an 'ACPI0008' device, but it's not a keyboard backlight
|
||||
# Best to assume laptops will have a keyboard backlight
|
||||
if self.model.startswith("MacBook"):
|
||||
return self.constants.computer.ambient_light_sensor
|
||||
return False
|
||||
|
||||
def check_nv_web_nvram(self):
|
||||
# First check boot-args, then dedicated nvram variable
|
||||
|
||||
def _check_nv_web_nvram(self):
|
||||
"""
|
||||
Query for Nvidia Web Driver property: nvda_drv_vrl or nvda_drv
|
||||
|
||||
Returns:
|
||||
bool: True if property is present, False otherwise
|
||||
"""
|
||||
|
||||
nv_on = utilities.get_nvram("boot-args", decode=True)
|
||||
if nv_on:
|
||||
if "nvda_drv_vrl=" in nv_on:
|
||||
@@ -309,8 +362,17 @@ class detect_root_patch:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_nv_web_opengl(self):
|
||||
# First check boot-args, then whether property exists on GPU
|
||||
|
||||
def _check_nv_web_opengl(self):
|
||||
"""
|
||||
Query for Nvidia Web Driver property: ngfxgl
|
||||
|
||||
Verify Web Drivers will run in OpenGL mode
|
||||
|
||||
Returns:
|
||||
bool: True if property is present, False otherwise
|
||||
"""
|
||||
|
||||
nv_on = utilities.get_nvram("boot-args", decode=True)
|
||||
if nv_on:
|
||||
if "ngfxgl=" in nv_on:
|
||||
@@ -321,8 +383,17 @@ class detect_root_patch:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_nv_compat(self):
|
||||
# Check for 'nv_web' in boot-args, then whether property exists on GPU
|
||||
|
||||
def _check_nv_compat(self):
|
||||
"""
|
||||
Query for Nvidia Web Driver property: ngfxcompat
|
||||
|
||||
Verify Web Drivers will skip NVDAStartupWeb compatibility check
|
||||
|
||||
Returns:
|
||||
bool: True if property is present, False otherwise
|
||||
"""
|
||||
|
||||
nv_on = utilities.get_nvram("boot-args", decode=True)
|
||||
if nv_on:
|
||||
if "ngfxcompat=" in nv_on:
|
||||
@@ -333,15 +404,37 @@ class detect_root_patch:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_whatevergreen(self):
|
||||
|
||||
def _check_whatevergreen(self):
|
||||
"""
|
||||
Query whether WhateverGreen.kext is loaded
|
||||
|
||||
Returns:
|
||||
bool: True if loaded, False otherwise
|
||||
"""
|
||||
|
||||
return utilities.check_kext_loaded("WhateverGreen", self.constants.detected_os)
|
||||
|
||||
def check_kdk(self):
|
||||
if sys_patch_helpers.sys_patch_helpers(self.constants).determine_kdk_present() is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_sip(self):
|
||||
def _check_kdk(self):
|
||||
"""
|
||||
Query whether Kernel Debug Kit is installed
|
||||
|
||||
Returns:
|
||||
bool: True if installed, False otherwise
|
||||
"""
|
||||
|
||||
return kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version, passive=True).kdk_already_installed
|
||||
|
||||
|
||||
def _check_sip(self):
|
||||
"""
|
||||
Query System Integrity checks required for patching
|
||||
|
||||
Returns:
|
||||
tuple: (list, str, str) of SIP values, SIP hex, SIP error message
|
||||
"""
|
||||
|
||||
if self.constants.detected_os > os_data.os_data.catalina:
|
||||
if self.nvidia_web is True:
|
||||
sip = sip_data.system_integrity_protection.root_patch_sip_big_sur_3rd_part_kexts
|
||||
@@ -367,7 +460,15 @@ class detect_root_patch:
|
||||
sip_value = f"For Hackintoshes, please set csr-active-config to '03060000' ({sip_hex})\nFor non-OpenCore Macs, please run 'csrutil disable' in RecoveryOS"
|
||||
return (sip, sip_value, sip_hex)
|
||||
|
||||
def check_uhci_ohci(self):
|
||||
|
||||
def _check_uhci_ohci(self):
|
||||
"""
|
||||
Query whether host has UHCI/OHCI controllers, and requires USB 1.1 patches
|
||||
|
||||
Returns:
|
||||
bool: True if UHCI/OHCI patches required, False otherwise
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return False
|
||||
|
||||
@@ -401,10 +502,19 @@ class detect_root_patch:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Entry point for patch set detection
|
||||
def detect_patch_set(self):
|
||||
"""
|
||||
Query patch sets required for host
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of patch sets
|
||||
"""
|
||||
|
||||
self.has_network = network_handler.NetworkUtilities().verify_network_connection()
|
||||
|
||||
if self.check_uhci_ohci() is True:
|
||||
if self._check_uhci_ohci() is True:
|
||||
self.legacy_uhci_ohci = True
|
||||
self.requires_root_kc = True
|
||||
|
||||
@@ -437,12 +547,12 @@ class detect_root_patch:
|
||||
if self.constants.detected_os > os_data.os_data.high_sierra:
|
||||
if self.model in ["MacBookPro8,2", "MacBookPro8,3"]:
|
||||
# Ref: https://doslabelectronics.com/Demux.html
|
||||
if self.detect_demux() is True:
|
||||
if self._detect_demux() is True:
|
||||
self.legacy_gmux = True
|
||||
else:
|
||||
self.legacy_gmux = True
|
||||
|
||||
self.detect_gpus()
|
||||
self._detect_gpus()
|
||||
|
||||
self.root_patch_dict = {
|
||||
"Graphics: Nvidia Tesla": self.nvidia_tesla,
|
||||
@@ -469,11 +579,11 @@ class detect_root_patch:
|
||||
"Settings: Supports Auxiliary Cache": not self.requires_root_kc,
|
||||
"Settings: Kernel Debug Kit missing": self.missing_kdk if self.constants.detected_os >= os_data.os_data.ventura.value else False,
|
||||
"Validation: Patching Possible": self.verify_patch_allowed(),
|
||||
"Validation: Unpatching Possible": self.verify_unpatch_allowed(),
|
||||
f"Validation: SIP is enabled (Required: {self.check_sip()[2]} or higher)": self.sip_enabled,
|
||||
"Validation: Unpatching Possible": self._verify_unpatch_allowed(),
|
||||
f"Validation: SIP is enabled (Required: {self._check_sip()[2]} or higher)": self.sip_enabled,
|
||||
f"Validation: Currently Booted SIP: ({hex(py_sip_xnu.SipXnu().get_sip_status().value)})": self.sip_enabled,
|
||||
"Validation: SecureBootModel is enabled": self.sbm_enabled,
|
||||
f"Validation: {'AMFI' if self.constants.host_is_hackintosh is True or self.get_amfi_level_needed() > 2 else 'Library Validation'} is enabled": self.amfi_enabled if self.amfi_must_disable is True else False,
|
||||
f"Validation: {'AMFI' if self.constants.host_is_hackintosh is True or self._get_amfi_level_needed() > 2 else 'Library Validation'} is enabled": self.amfi_enabled if self.amfi_must_disable is True else False,
|
||||
"Validation: FileVault is enabled": self.fv_enabled,
|
||||
"Validation: System is dosdude1 patched": self.dosdude_patched,
|
||||
"Validation: WhateverGreen.kext missing": self.missing_whatever_green if self.nvidia_web is True else False,
|
||||
@@ -485,30 +595,53 @@ class detect_root_patch:
|
||||
|
||||
return self.root_patch_dict
|
||||
|
||||
def get_amfi_level_needed(self):
|
||||
if self.amfi_must_disable is True:
|
||||
if self.constants.detected_os > os_data.os_data.catalina:
|
||||
if self.constants.detected_os >= os_data.os_data.ventura:
|
||||
if self.amfi_shim_bins is True:
|
||||
# Currently we require AMFI outright disabled
|
||||
# in Ventura to work with shim'd binaries
|
||||
return 3
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def verify_patch_allowed(self, print_errors=False):
|
||||
sip_dict = self.check_sip()
|
||||
def _get_amfi_level_needed(self):
|
||||
"""
|
||||
Query the AMFI level needed for the patcher to work
|
||||
|
||||
Returns:
|
||||
int: AMFI level needed
|
||||
"""
|
||||
|
||||
if self.amfi_must_disable is False:
|
||||
return amfi_detect.AmfiConfigDetectLevel.NO_CHECK
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.big_sur:
|
||||
return amfi_detect.AmfiConfigDetectLevel.NO_CHECK
|
||||
|
||||
if self.constants.detected_os >= os_data.os_data.ventura:
|
||||
if self.amfi_shim_bins is True:
|
||||
# Currently we require AMFI outright disabled
|
||||
# in Ventura to work with shim'd binaries
|
||||
return amfi_detect.AmfiConfigDetectLevel.ALLOW_ALL
|
||||
|
||||
return amfi_detect.AmfiConfigDetectLevel.LIBRARY_VALIDATION
|
||||
|
||||
|
||||
def verify_patch_allowed(self, print_errors: bool = False):
|
||||
"""
|
||||
Validate that the patcher can be run
|
||||
|
||||
Parameters:
|
||||
print_errors (bool): Print errors to console
|
||||
|
||||
Returns:
|
||||
bool: True if patching is allowed, False otherwise
|
||||
"""
|
||||
|
||||
sip_dict = self._check_sip()
|
||||
sip = sip_dict[0]
|
||||
sip_value = sip_dict[1]
|
||||
|
||||
self.sip_enabled, self.sbm_enabled, self.fv_enabled, self.dosdude_patched = utilities.patching_status(sip, self.constants.detected_os)
|
||||
self.amfi_enabled = not amfi_detect.amfi_configuration_detection().check_config(self.get_amfi_level_needed())
|
||||
self.amfi_enabled = not amfi_detect.AmfiConfigurationDetection().check_config(self._get_amfi_level_needed())
|
||||
|
||||
if self.nvidia_web is True:
|
||||
self.missing_nv_web_nvram = not self.check_nv_web_nvram()
|
||||
self.missing_nv_web_opengl = not self.check_nv_web_opengl()
|
||||
self.missing_nv_compat = not self.check_nv_compat()
|
||||
self.missing_whatever_green = not self.check_whatevergreen()
|
||||
self.missing_nv_web_nvram = not self._check_nv_web_nvram()
|
||||
self.missing_nv_web_opengl = not self._check_nv_web_opengl()
|
||||
self.missing_nv_compat = not self._check_nv_compat()
|
||||
self.missing_whatever_green = not self._check_whatevergreen()
|
||||
|
||||
if print_errors is True:
|
||||
if self.sip_enabled is True:
|
||||
@@ -576,28 +709,54 @@ class detect_root_patch:
|
||||
]
|
||||
):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def verify_unpatch_allowed(self, print_errors=False):
|
||||
# Must be called after verify_patch_allowed
|
||||
return True
|
||||
|
||||
|
||||
def _verify_unpatch_allowed(self):
|
||||
"""
|
||||
Validate that the unpatcher can be run
|
||||
|
||||
Preconditions:
|
||||
Must be called after verify_patch_allowed()
|
||||
|
||||
Returns:
|
||||
bool: True if unpatching is allowed, False otherwise
|
||||
"""
|
||||
|
||||
return not self.sip_enabled
|
||||
|
||||
def generate_patchset(self, hardware_details):
|
||||
all_hardware_patchset = sys_patch_dict.SystemPatchDictionary(self.constants.detected_os, self.constants.detected_os_minor, self.constants.legacy_accel_support)
|
||||
required_patches = {}
|
||||
|
||||
def generate_patchset(self, hardware_details: dict):
|
||||
"""
|
||||
Generate Patchset dictionary for the current system
|
||||
|
||||
Parameters:
|
||||
hardware_details (dict): Dictionary of hardware details generated by detect_patch_set()
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of patches to be applied from sys_patch_dict.py
|
||||
"""
|
||||
|
||||
all_hardware_patchset: dict = sys_patch_dict.SystemPatchDictionary(self.constants.detected_os, self.constants.detected_os_minor, self.constants.legacy_accel_support)
|
||||
required_patches: dict = {}
|
||||
|
||||
utilities.cls()
|
||||
|
||||
logging.info("- The following patches will be applied:")
|
||||
|
||||
if hardware_details["Graphics: Intel Ironlake"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Intel Ironlake": all_hardware_patchset["Graphics"]["Intel Ironlake"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Sandy Bridge"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"Non-Metal ColorSync Workaround": all_hardware_patchset["Graphics"]["Non-Metal ColorSync Workaround"]})
|
||||
required_patches.update({"High Sierra GVA": all_hardware_patchset["Graphics"]["High Sierra GVA"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Intel Sandy Bridge": all_hardware_patchset["Graphics"]["Intel Sandy Bridge"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Ivy Bridge"] is True:
|
||||
required_patches.update({"Metal 3802 Common": all_hardware_patchset["Graphics"]["Metal 3802 Common"]})
|
||||
required_patches.update({"Catalina GVA": all_hardware_patchset["Graphics"]["Catalina GVA"]})
|
||||
@@ -605,23 +764,28 @@ class detect_root_patch:
|
||||
required_patches.update({"Big Sur OpenCL": all_hardware_patchset["Graphics"]["Big Sur OpenCL"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Intel Ivy Bridge": all_hardware_patchset["Graphics"]["Intel Ivy Bridge"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Haswell"] is True:
|
||||
required_patches.update({"Metal 3802 Common": all_hardware_patchset["Graphics"]["Metal 3802 Common"]})
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
required_patches.update({"Monterey OpenCL": all_hardware_patchset["Graphics"]["Monterey OpenCL"]})
|
||||
required_patches.update({"Intel Haswell": all_hardware_patchset["Graphics"]["Intel Haswell"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Broadwell"] is True:
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
required_patches.update({"Monterey OpenCL": all_hardware_patchset["Graphics"]["Monterey OpenCL"]})
|
||||
required_patches.update({"Intel Broadwell": all_hardware_patchset["Graphics"]["Intel Broadwell"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Skylake"] is True:
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
required_patches.update({"Monterey OpenCL": all_hardware_patchset["Graphics"]["Monterey OpenCL"]})
|
||||
required_patches.update({"Intel Skylake": all_hardware_patchset["Graphics"]["Intel Skylake"]})
|
||||
|
||||
if hardware_details["Graphics: Nvidia Tesla"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Nvidia Tesla": all_hardware_patchset["Graphics"]["Nvidia Tesla"]})
|
||||
|
||||
if hardware_details["Graphics: Nvidia Web Drivers"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"Non-Metal IOAccelerator Common": all_hardware_patchset["Graphics"]["Non-Metal IOAccelerator Common"]})
|
||||
@@ -629,6 +793,7 @@ class detect_root_patch:
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Nvidia Web Drivers": all_hardware_patchset["Graphics"]["Nvidia Web Drivers"]})
|
||||
required_patches.update({"Non-Metal Enforcement": all_hardware_patchset["Graphics"]["Non-Metal Enforcement"]})
|
||||
|
||||
if hardware_details["Graphics: Nvidia Kepler"] is True:
|
||||
required_patches.update({"Revert Metal Downgrade": all_hardware_patchset["Graphics"]["Revert Metal Downgrade"]})
|
||||
required_patches.update({"Metal 3802 Common": all_hardware_patchset["Graphics"]["Metal 3802 Common"]})
|
||||
@@ -643,11 +808,13 @@ class detect_root_patch:
|
||||
if "Catalina GVA" in required_patches:
|
||||
del(required_patches["Catalina GVA"])
|
||||
break
|
||||
|
||||
if hardware_details["Graphics: AMD TeraScale 1"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"AMD TeraScale Common": all_hardware_patchset["Graphics"]["AMD TeraScale Common"]})
|
||||
required_patches.update({"AMD TeraScale 1": all_hardware_patchset["Graphics"]["AMD TeraScale 1"]})
|
||||
|
||||
if hardware_details["Graphics: AMD TeraScale 2"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"Non-Metal IOAccelerator Common": all_hardware_patchset["Graphics"]["Non-Metal IOAccelerator Common"]})
|
||||
@@ -658,6 +825,7 @@ class detect_root_patch:
|
||||
# TeraScale 2 MacBooks with faulty GPUs are highly prone to crashing with AMDRadeonX3000 attached
|
||||
# Additionally, AMDRadeonX3000 requires IOAccelerator downgrade which is not installed without 'Non-Metal IOAccelerator Common'
|
||||
del(required_patches["AMD TeraScale 2"]["Install"]["/System/Library/Extensions"]["AMDRadeonX3000.kext"])
|
||||
|
||||
if hardware_details["Graphics: AMD Legacy GCN"] is True or hardware_details["Graphics: AMD Legacy Polaris"] is True:
|
||||
required_patches.update({"Revert Metal Downgrade": all_hardware_patchset["Graphics"]["Revert Metal Downgrade"]})
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
@@ -666,8 +834,10 @@ class detect_root_patch:
|
||||
required_patches.update({"AMD Legacy GCN": all_hardware_patchset["Graphics"]["AMD Legacy GCN"]})
|
||||
else:
|
||||
required_patches.update({"AMD Legacy Polaris": all_hardware_patchset["Graphics"]["AMD Legacy Polaris"]})
|
||||
required_patches.update({"Revert GVA Downgrade": all_hardware_patchset["Graphics"]["Revert GVA Downgrade"]})
|
||||
if "AVX2" not in self.constants.computer.cpu.leafs:
|
||||
required_patches.update({"AMD OpenCL": all_hardware_patchset["Graphics"]["AMD OpenCL"]})
|
||||
|
||||
if hardware_details["Graphics: AMD Legacy Vega"] is True:
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
required_patches.update({"Monterey OpenCL": all_hardware_patchset["Graphics"]["Monterey OpenCL"]})
|
||||
@@ -675,20 +845,28 @@ class detect_root_patch:
|
||||
required_patches.update({"AMD OpenCL": all_hardware_patchset["Graphics"]["AMD OpenCL"]})
|
||||
if hardware_details["Graphics: AMD Legacy GCN"] is True:
|
||||
required_patches.update({"AMD Legacy Vega Extended": all_hardware_patchset["Graphics"]["AMD Legacy Vega Extended"]})
|
||||
else:
|
||||
required_patches.update({"Revert GVA Downgrade": all_hardware_patchset["Graphics"]["Revert GVA Downgrade"]})
|
||||
|
||||
if hardware_details["Brightness: Legacy Backlight Control"] is True:
|
||||
required_patches.update({"Legacy Backlight Control": all_hardware_patchset["Brightness"]["Legacy Backlight Control"]})
|
||||
|
||||
if hardware_details["Audio: Legacy Realtek"] is True:
|
||||
if self.model in ["iMac7,1", "iMac8,1"]:
|
||||
required_patches.update({"Legacy Realtek": all_hardware_patchset["Audio"]["Legacy Realtek"]})
|
||||
else:
|
||||
required_patches.update({"Legacy Non-GOP": all_hardware_patchset["Audio"]["Legacy Non-GOP"]})
|
||||
|
||||
if hardware_details["Networking: Legacy Wireless"] is True:
|
||||
required_patches.update({"Legacy Wireless": all_hardware_patchset["Networking"]["Legacy Wireless"]})
|
||||
required_patches.update({"Legacy Wireless Extended": all_hardware_patchset["Networking"]["Legacy Wireless Extended"]})
|
||||
|
||||
if hardware_details["Miscellaneous: Legacy GMUX"] is True:
|
||||
required_patches.update({"Legacy GMUX": all_hardware_patchset["Miscellaneous"]["Legacy GMUX"]})
|
||||
|
||||
if hardware_details["Miscellaneous: Legacy Keyboard Backlight"] is True:
|
||||
required_patches.update({"Legacy Keyboard Backlight": all_hardware_patchset["Miscellaneous"]["Legacy Keyboard Backlight"]})
|
||||
|
||||
if hardware_details["Miscellaneous: Legacy USB 1.1"] is True:
|
||||
required_patches.update({"Legacy USB 1.1": all_hardware_patchset["Miscellaneous"]["Legacy USB 1.1"]})
|
||||
|
||||
|
||||
@@ -1,58 +1,86 @@
|
||||
# Additional support functions for sys_patch.py
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
# Copyright (C) 2020-2023, Dhinak G, Mykola Grymalyuk
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
from data import os_data
|
||||
from resources import generate_smbios, utilities
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import plistlib
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from resources import constants, bplist
|
||||
|
||||
class sys_patch_helpers:
|
||||
|
||||
def __init__(self, constants):
|
||||
self.constants = constants
|
||||
from data import os_data
|
||||
from resources import bplist, constants, generate_smbios, utilities
|
||||
|
||||
|
||||
def snb_board_id_patch(self, source_files_path):
|
||||
# AppleIntelSNBGraphicsFB hard codes the supported Board IDs for Sandy Bridge iGPUs
|
||||
# Because of this, the kext errors out on unsupported systems
|
||||
# This function simply patches in a supported Board ID, using 'determine_best_board_id_for_sandy()'
|
||||
# to supplement the ideal Board ID
|
||||
class SysPatchHelpers:
|
||||
"""
|
||||
Library of helper functions for sys_patch.py and related libraries
|
||||
"""
|
||||
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
|
||||
def snb_board_id_patch(self, source_files_path: str):
|
||||
"""
|
||||
Patch AppleIntelSNBGraphicsFB.kext to support unsupported Board IDs
|
||||
|
||||
AppleIntelSNBGraphicsFB hard codes the supported Board IDs for Sandy Bridge iGPUs
|
||||
Because of this, the kext errors out on unsupported systems
|
||||
This function simply patches in a supported Board ID, using 'determine_best_board_id_for_sandy()'
|
||||
to supplement the ideal Board ID
|
||||
|
||||
Parameters:
|
||||
source_files_path (str): Path to the source files
|
||||
|
||||
"""
|
||||
|
||||
source_files_path = str(source_files_path)
|
||||
if self.constants.computer.reported_board_id not in self.constants.sandy_board_id_stock:
|
||||
logging.info(f"- Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching")
|
||||
board_to_patch = generate_smbios.determine_best_board_id_for_sandy(self.constants.computer.reported_board_id, self.constants.computer.gpus)
|
||||
logging.info(f"- Replacing {board_to_patch} with {self.constants.computer.reported_board_id}")
|
||||
|
||||
board_to_patch_hex = bytes.fromhex(board_to_patch.encode('utf-8').hex())
|
||||
reported_board_hex = bytes.fromhex(self.constants.computer.reported_board_id.encode('utf-8').hex())
|
||||
if self.constants.computer.reported_board_id in self.constants.sandy_board_id_stock:
|
||||
return
|
||||
|
||||
if len(board_to_patch_hex) > len(reported_board_hex):
|
||||
# Pad the reported Board ID with zeros to match the length of the board to patch
|
||||
reported_board_hex = reported_board_hex + bytes(len(board_to_patch_hex) - len(reported_board_hex))
|
||||
elif len(board_to_patch_hex) < len(reported_board_hex):
|
||||
logging.info(f"- Error: Board ID {self.constants.computer.reported_board_id} is longer than {board_to_patch}")
|
||||
raise Exception("Host's Board ID is longer than the kext's Board ID, cannot patch!!!")
|
||||
logging.info(f"- Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching")
|
||||
|
||||
path = source_files_path + "/10.13.6/System/Library/Extensions/AppleIntelSNBGraphicsFB.kext/Contents/MacOS/AppleIntelSNBGraphicsFB"
|
||||
if Path(path).exists():
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
data = data.replace(board_to_patch_hex, reported_board_hex)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
else:
|
||||
logging.info(f"- Error: Could not find {path}")
|
||||
raise Exception("Failed to find AppleIntelSNBGraphicsFB.kext, cannot patch!!!")
|
||||
board_to_patch = generate_smbios.determine_best_board_id_for_sandy(self.constants.computer.reported_board_id, self.constants.computer.gpus)
|
||||
logging.info(f"- Replacing {board_to_patch} with {self.constants.computer.reported_board_id}")
|
||||
|
||||
board_to_patch_hex = bytes.fromhex(board_to_patch.encode('utf-8').hex())
|
||||
reported_board_hex = bytes.fromhex(self.constants.computer.reported_board_id.encode('utf-8').hex())
|
||||
|
||||
if len(board_to_patch_hex) > len(reported_board_hex):
|
||||
# Pad the reported Board ID with zeros to match the length of the board to patch
|
||||
reported_board_hex = reported_board_hex + bytes(len(board_to_patch_hex) - len(reported_board_hex))
|
||||
elif len(board_to_patch_hex) < len(reported_board_hex):
|
||||
logging.info(f"- Error: Board ID {self.constants.computer.reported_board_id} is longer than {board_to_patch}")
|
||||
raise Exception("Host's Board ID is longer than the kext's Board ID, cannot patch!!!")
|
||||
|
||||
path = source_files_path + "/10.13.6/System/Library/Extensions/AppleIntelSNBGraphicsFB.kext/Contents/MacOS/AppleIntelSNBGraphicsFB"
|
||||
if not Path(path).exists():
|
||||
logging.info(f"- Error: Could not find {path}")
|
||||
raise Exception("Failed to find AppleIntelSNBGraphicsFB.kext, cannot patch!!!")
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
data = data.replace(board_to_patch_hex, reported_board_hex)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def generate_patchset_plist(self, patchset, file_name, kdk_used):
|
||||
def generate_patchset_plist(self, patchset: dict, file_name: str, kdk_used: Path):
|
||||
"""
|
||||
Generate patchset file for user reference
|
||||
|
||||
Parameters:
|
||||
patchset (dict): Dictionary of patchset, see sys_patch_detect.py and sys_patch_dict.py
|
||||
file_name (str): Name of the file to write to
|
||||
kdk_used (Path): Path to the KDK used, if any
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False if not
|
||||
|
||||
"""
|
||||
|
||||
source_path = f"{self.constants.payload_path}"
|
||||
source_path_file = f"{source_path}/{file_name}"
|
||||
|
||||
@@ -68,93 +96,35 @@ class sys_patch_helpers:
|
||||
"Kernel Debug Kit Used": f"{kdk_string}",
|
||||
"OS Version": f"{self.constants.detected_os}.{self.constants.detected_os_minor} ({self.constants.detected_os_build})",
|
||||
}
|
||||
|
||||
data.update(patchset)
|
||||
|
||||
if Path(source_path_file).exists():
|
||||
os.remove(source_path_file)
|
||||
|
||||
# Need to write to a safe location
|
||||
plistlib.dump(data, Path(source_path_file).open("wb"), sort_keys=False)
|
||||
|
||||
if Path(source_path_file).exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def install_kdk(self):
|
||||
if not self.constants.kdk_download_path.exists():
|
||||
return
|
||||
|
||||
logging.info(f"- Installing downloaded KDK (this may take a while)")
|
||||
with tempfile.TemporaryDirectory() as mount_point:
|
||||
utilities.process_status(subprocess.run(["hdiutil", "attach", self.constants.kdk_download_path, "-mountpoint", mount_point, "-nobrowse"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
# Due to a permissions bug in macOS, sometimes the OS will fail on a Read-only file system error
|
||||
# We don't actually need to write inside the KDK DMG, however macOS will do whatever it wants
|
||||
# Thus move the KDK to another location, and run the installer from there
|
||||
kdk_dst_path = Path(f"{self.constants.payload_path}/KernelDebugKit.pkg")
|
||||
if kdk_dst_path.exists():
|
||||
utilities.process_status(utilities.elevated(["rm", kdk_dst_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
utilities.process_status(subprocess.run(["cp", f"{mount_point}/KernelDebugKit.pkg", self.constants.payload_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
result = utilities.elevated(["installer", "-pkg", kdk_dst_path, "-target", "/"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to install KDK:")
|
||||
logging.info(result.stdout.decode('utf-8'))
|
||||
if result.stderr:
|
||||
logging.info(result.stderr.decode('utf-8'))
|
||||
utilities.elevated(["hdiutil", "detach", mount_point], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
raise Exception("Failed to install KDK")
|
||||
utilities.process_status(utilities.elevated(["rm", kdk_dst_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
utilities.elevated(["hdiutil", "detach", mount_point], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
logging.info("- Successfully installed KDK")
|
||||
|
||||
|
||||
def determine_kdk_present(self, match_closest=False, override_build=None):
|
||||
# Check if KDK is present
|
||||
# If 'match_closest' is True, will provide the closest match to the reported KDK
|
||||
|
||||
kdk_array = []
|
||||
|
||||
search_build = self.constants.detected_os_build
|
||||
if override_build:
|
||||
search_build = override_build
|
||||
|
||||
if not Path("/Library/Developer/KDKs").exists():
|
||||
return None
|
||||
|
||||
|
||||
for kdk_folder in Path("/Library/Developer/KDKs").iterdir():
|
||||
if not kdk_folder.name.endswith(".kdk"):
|
||||
continue
|
||||
|
||||
# Ensure direct match
|
||||
if kdk_folder.name.endswith(f"{search_build}.kdk"):
|
||||
# Verify that the KDK is valid
|
||||
if (kdk_folder / Path("System/Library/Extensions/System.kext/PlugIns/Libkern.kext/Libkern")).exists():
|
||||
return kdk_folder
|
||||
if match_closest is True:
|
||||
# ex: KDK_13.0_22A5266r.kdk -> 22A5266r.kdk -> 22A5266r
|
||||
try:
|
||||
build = kdk_folder.name.split("_")[2].split(".")[0]
|
||||
# Don't append if Darwin Major is different
|
||||
if build.startswith(str(self.constants.detected_os)):
|
||||
kdk_array.append(build)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
if match_closest is True:
|
||||
result = os_data.os_conversion.find_largest_build(kdk_array)
|
||||
logging.info(f"- Closest KDK match to {search_build}: {result}")
|
||||
for kdk_folder in Path("/Library/Developer/KDKs").iterdir():
|
||||
if kdk_folder.name.endswith(f"{result}.kdk"):
|
||||
# Verify that the KDK is valid
|
||||
if (kdk_folder / Path("System/Library/Extensions/System.kext/PlugIns/Libkern.kext/Libkern")).exists():
|
||||
return kdk_folder
|
||||
return None
|
||||
|
||||
|
||||
def disable_window_server_caching(self):
|
||||
# On legacy GCN GPUs, the WindowServer cache generated creates
|
||||
# corrupted Opaque shaders.
|
||||
# To work-around this, we disable WindowServer caching
|
||||
# And force macOS into properly generating the Opaque shaders
|
||||
"""
|
||||
Disable WindowServer's asset caching
|
||||
|
||||
On legacy GCN GPUs, the WindowServer cache generated creates
|
||||
corrupted Opaque shaders.
|
||||
|
||||
To work-around this, we disable WindowServer caching
|
||||
And force macOS into properly generating the Opaque shaders
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
|
||||
logging.info("- Disabling WindowServer Caching")
|
||||
# Invoke via 'bash -c' to resolve pathing
|
||||
utilities.elevated(["bash", "-c", "rm -rf /private/var/folders/*/*/*/WindowServer/com.apple.WindowServer"])
|
||||
@@ -166,11 +136,17 @@ class sys_patch_helpers:
|
||||
|
||||
|
||||
def remove_news_widgets(self):
|
||||
# On Ivy Bridge and Haswell iGPUs, RenderBox will crash the News Widgets in
|
||||
# Notification Centre. To ensure users can access Notifications normally,
|
||||
# we manually remove all News Widgets
|
||||
"""
|
||||
Remove News Widgets from Notification Centre
|
||||
|
||||
On Ivy Bridge and Haswell iGPUs, RenderBox will crash the News Widgets in
|
||||
Notification Centre. To ensure users can access Notifications normally,
|
||||
we manually remove all News Widgets
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
|
||||
logging.info("- Parsing Notification Centre Widgets")
|
||||
file_path = "~/Library/Containers/com.apple.notificationcenterui/Data/Library/Preferences/com.apple.notificationcenterui.plist"
|
||||
file_path = Path(file_path).expanduser()
|
||||
@@ -182,22 +158,27 @@ class sys_patch_helpers:
|
||||
did_find = False
|
||||
with open(file_path, "rb") as f:
|
||||
data = plistlib.load(f)
|
||||
if "widgets" in data:
|
||||
if "instances" in data["widgets"]:
|
||||
for widget in list(data["widgets"]["instances"]):
|
||||
widget_data = bplist.BPListReader(widget).parse()
|
||||
for entry in widget_data:
|
||||
if not 'widget' in entry:
|
||||
continue
|
||||
sub_data = bplist.BPListReader(widget_data[entry]).parse()
|
||||
for sub_entry in sub_data:
|
||||
if not '$object' in sub_entry:
|
||||
continue
|
||||
if not b'com.apple.news' in sub_data[sub_entry][2]:
|
||||
continue
|
||||
logging.info(f" - Found News Widget to remove: {sub_data[sub_entry][2].decode('ascii')}")
|
||||
data["widgets"]["instances"].remove(widget)
|
||||
did_find = True
|
||||
if "widgets" not in data:
|
||||
return
|
||||
|
||||
if "instances" not in data["widgets"]:
|
||||
return
|
||||
|
||||
for widget in list(data["widgets"]["instances"]):
|
||||
widget_data = bplist.BPListReader(widget).parse()
|
||||
for entry in widget_data:
|
||||
if 'widget' not in entry:
|
||||
continue
|
||||
sub_data = bplist.BPListReader(widget_data[entry]).parse()
|
||||
for sub_entry in sub_data:
|
||||
if not '$object' in sub_entry:
|
||||
continue
|
||||
if not b'com.apple.news' in sub_data[sub_entry][2]:
|
||||
continue
|
||||
logging.info(f" - Found News Widget to remove: {sub_data[sub_entry][2].decode('ascii')}")
|
||||
data["widgets"]["instances"].remove(widget)
|
||||
did_find = True
|
||||
|
||||
if did_find:
|
||||
with open(file_path, "wb") as f:
|
||||
plistlib.dump(data, f, sort_keys=False)
|
||||
@@ -205,16 +186,23 @@ class sys_patch_helpers:
|
||||
|
||||
|
||||
def install_rsr_repair_binary(self):
|
||||
# With macOS 13.2, Apple implemented the Rapid Security Response System
|
||||
# However Apple added a half baked snapshot reversion system if seal was broken,
|
||||
# which forgets to handle Preboot BootKC syncing
|
||||
"""
|
||||
Installs RSRRepair
|
||||
|
||||
# Thus this application will try to re-sync the BootKC with SysKC in the event of a panic
|
||||
# Reference: https://github.com/dortania/OpenCore-Legacy-Patcher/issues/1019
|
||||
RSRRepair is a utility that will sync the SysKC and BootKC in the event of a panic
|
||||
|
||||
# This is a (hopefully) temporary work-around, however likely to stay.
|
||||
# RSRRepair has the added bonus of fixing desynced KCs from 'bless', so useful in Big Sur+
|
||||
# https://github.com/flagersgit/RSRRepair
|
||||
With macOS 13.2, Apple implemented the Rapid Security Response System
|
||||
However Apple added a half baked snapshot reversion system if seal was broken,
|
||||
which forgets to handle Preboot BootKC syncing.
|
||||
|
||||
Thus this application will try to re-sync the BootKC with SysKC in the event of a panic
|
||||
Reference: https://github.com/dortania/OpenCore-Legacy-Patcher/issues/1019
|
||||
|
||||
This is a (hopefully) temporary work-around, however likely to stay.
|
||||
RSRRepair has the added bonus of fixing desynced KCs from 'bless', so useful in Big Sur+
|
||||
Source: https://github.com/flagersgit/RSRRepair
|
||||
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.big_sur:
|
||||
return
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
from resources import utilities
|
||||
|
||||
class TUIMenu:
|
||||
def __init__(self, title, prompt, options=None, return_number_instead_of_direct_call=False, add_quit=True, auto_number=False, in_between=None, top_level=False, loop=False):
|
||||
self.title = title
|
||||
self.prompt = prompt
|
||||
self.in_between = in_between or []
|
||||
self.options = options or []
|
||||
self.return_number_instead_of_direct_call = return_number_instead_of_direct_call
|
||||
self.auto_number = auto_number
|
||||
self.add_quit = add_quit
|
||||
self.top_level = top_level
|
||||
self.loop = loop
|
||||
self.added_quit = False
|
||||
|
||||
def add_menu_option(self, name, description="", function=None, key=""):
|
||||
self.options.append([key, name, description, function])
|
||||
|
||||
def start(self):
|
||||
return_option = ["Q", "Quit"] if self.top_level else ["B", "Back"]
|
||||
if self.add_quit and not self.added_quit:
|
||||
self.add_menu_option(return_option[1], function=None, key=return_option[0])
|
||||
self.added_quit = True
|
||||
|
||||
while True:
|
||||
utilities.cls()
|
||||
utilities.header(self.title)
|
||||
print()
|
||||
|
||||
for i in self.in_between:
|
||||
print(i)
|
||||
if self.in_between:
|
||||
print()
|
||||
|
||||
for index, option in enumerate(self.options):
|
||||
if self.auto_number and not (index == (len(self.options) - 1) and self.add_quit):
|
||||
option[0] = str((index + 1))
|
||||
print(option[0] + ". " + option[1])
|
||||
for i in option[2]:
|
||||
print("\t" + i)
|
||||
|
||||
print()
|
||||
selected = input(self.prompt)
|
||||
|
||||
keys = [option[0].upper() for option in self.options]
|
||||
if not selected or selected.upper() not in keys:
|
||||
if self.loop:
|
||||
continue
|
||||
else:
|
||||
return
|
||||
if self.add_quit and selected.upper() == return_option[0]:
|
||||
return -1
|
||||
elif self.return_number_instead_of_direct_call:
|
||||
return self.options[keys.index(selected.upper())][0]
|
||||
else:
|
||||
self.options[keys.index(selected.upper())][3]() if self.options[keys.index(selected.upper())][3] else None
|
||||
if not self.loop:
|
||||
return
|
||||
|
||||
|
||||
class TUIOnlyPrint:
|
||||
def __init__(self, title, prompt, in_between=None):
|
||||
self.title = title
|
||||
self.prompt = prompt
|
||||
self.in_between = in_between or []
|
||||
|
||||
def start(self):
|
||||
utilities.cls()
|
||||
utilities.header(self.title)
|
||||
print()
|
||||
|
||||
for i in self.in_between:
|
||||
print(i)
|
||||
if self.in_between:
|
||||
print()
|
||||
|
||||
return input(self.prompt)
|
||||
+79
-52
@@ -5,21 +5,31 @@
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from resources import network_handler
|
||||
from resources import network_handler, constants
|
||||
|
||||
REPO_LATEST_RELEASE_URL: str = "https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest"
|
||||
|
||||
|
||||
class check_binary_updates:
|
||||
def __init__(self, constants):
|
||||
self.constants = constants
|
||||
self.binary_version = self.constants.patcher_version
|
||||
self.binary_version_array = self.binary_version.split(".")
|
||||
self.binary_version_array = [int(x) for x in self.binary_version_array]
|
||||
self.binary_url = "https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest"
|
||||
class CheckBinaryUpdates:
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.available_binaries = {}
|
||||
self.binary_version = self.constants.patcher_version
|
||||
self.binary_version_array = [int(x) for x in self.binary_version.split(".")]
|
||||
|
||||
|
||||
def check_if_build_newer(self, remote_version=None, local_version=None):
|
||||
def _check_if_build_newer(self, remote_version: list = None, local_version: list = None):
|
||||
"""
|
||||
Check if the remote version is newer than the local version
|
||||
|
||||
Parameters:
|
||||
remote_version (list): Remote version to compare against
|
||||
local_version (list): Local version to compare against
|
||||
|
||||
Returns:
|
||||
bool: True if remote version is newer, False if not
|
||||
"""
|
||||
|
||||
if remote_version is None:
|
||||
remote_version = self.remote_version_array
|
||||
if local_version is None:
|
||||
@@ -39,13 +49,32 @@ class check_binary_updates:
|
||||
|
||||
return False
|
||||
|
||||
def determine_local_build_type(self):
|
||||
|
||||
def _determine_local_build_type(self):
|
||||
"""
|
||||
Check if the local build is a GUI or TUI build
|
||||
|
||||
Returns:
|
||||
str: "GUI" or "TUI"
|
||||
"""
|
||||
|
||||
if self.constants.wxpython_variant is True:
|
||||
return "GUI"
|
||||
else:
|
||||
return "TUI"
|
||||
|
||||
def determine_remote_type(self, remote_name):
|
||||
|
||||
def _determine_remote_type(self, remote_name: str):
|
||||
"""
|
||||
Check if the remote build is a GUI or TUI build
|
||||
|
||||
Parameters:
|
||||
remote_name (str): Name of the remote build
|
||||
|
||||
Returns:
|
||||
str: "GUI" or "TUI"
|
||||
"""
|
||||
|
||||
if "TUI" in remote_name:
|
||||
return "TUI"
|
||||
elif "GUI" in remote_name:
|
||||
@@ -53,45 +82,43 @@ class check_binary_updates:
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def check_binary_updates(self):
|
||||
# logging.info("- Checking for updates...")
|
||||
if network_handler.NetworkUtilities(self.binary_url).verify_network_connection():
|
||||
# logging.info("- Network connection functional")
|
||||
response = requests.get(self.binary_url)
|
||||
data_set = response.json()
|
||||
# logging.info("- Retrieved latest version data")
|
||||
self.remote_version = data_set["tag_name"]
|
||||
# logging.info(f"- Latest version: {self.remote_version}")
|
||||
self.remote_version_array = self.remote_version.split(".")
|
||||
self.remote_version_array = [
|
||||
int(x) for x in self.remote_version_array
|
||||
]
|
||||
if self.check_if_build_newer() is True:
|
||||
# logging.info("- Remote version is newer")
|
||||
for asset in data_set["assets"]:
|
||||
logging.info(f"- Found asset: {asset['name']}")
|
||||
if self.determine_remote_type(asset["name"]) == self.determine_local_build_type():
|
||||
# logging.info(f"- Found matching asset: {asset['name']}")
|
||||
self.available_binaries.update({
|
||||
asset['name']: {
|
||||
"Name":
|
||||
asset["name"],
|
||||
"Version":
|
||||
self.remote_version,
|
||||
"Link":
|
||||
asset["browser_download_url"],
|
||||
"Type":
|
||||
self.determine_remote_type(asset["name"]),
|
||||
"Github Link":
|
||||
f"https://github.com/dortania/OpenCore-Legacy-Patcher/releases/{self.remote_version}"
|
||||
}
|
||||
})
|
||||
break
|
||||
if self.available_binaries:
|
||||
return self.available_binaries
|
||||
else:
|
||||
# logging.info("- No matching binaries available")
|
||||
return None
|
||||
# else:
|
||||
# logging.info("- Failed to connect to GitHub API")
|
||||
"""
|
||||
Check if any updates are available for the OpenCore Legacy Patcher binary
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with Link and Version of the latest binary update if available
|
||||
"""
|
||||
|
||||
available_binaries: list = {}
|
||||
|
||||
if not network_handler.NetworkUtilities(REPO_LATEST_RELEASE_URL).verify_network_connection():
|
||||
return None
|
||||
|
||||
response = requests.get(REPO_LATEST_RELEASE_URL)
|
||||
data_set = response.json()
|
||||
|
||||
self.remote_version = data_set["tag_name"]
|
||||
|
||||
self.remote_version_array = self.remote_version.split(".")
|
||||
self.remote_version_array = [int(x) for x in self.remote_version_array]
|
||||
|
||||
if self._check_if_build_newer() is False:
|
||||
return None
|
||||
|
||||
for asset in data_set["assets"]:
|
||||
logging.info(f"- Found asset: {asset['name']}")
|
||||
if self._determine_remote_type(asset["name"]) == self._determine_local_build_type():
|
||||
available_binaries.update({
|
||||
asset['name']: {
|
||||
"Name": asset["name"],
|
||||
"Version": self.remote_version,
|
||||
"Link": asset["browser_download_url"],
|
||||
"Type": self._determine_remote_type(asset["name"]),
|
||||
"Github Link": f"https://github.com/dortania/OpenCore-Legacy-Patcher/releases/{self.remote_version}"
|
||||
}
|
||||
})
|
||||
return available_binaries
|
||||
|
||||
return None
|
||||
@@ -9,7 +9,6 @@ import os
|
||||
import binascii
|
||||
import argparse
|
||||
import atexit
|
||||
import requests
|
||||
import shutil
|
||||
import py_sip_xnu
|
||||
|
||||
|
||||
+135
-80
@@ -1,51 +1,70 @@
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from resources.sys_patch import sys_patch_helpers
|
||||
from resources.build import build
|
||||
from resources import constants
|
||||
from data import example_data, model_array, sys_patch_dict, os_data
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
|
||||
def validate(settings):
|
||||
# Runs through ocvalidate to check for errors
|
||||
class PatcherValidation:
|
||||
"""
|
||||
Validation class for the patcher
|
||||
|
||||
valid_dumps = [
|
||||
example_data.MacBookPro.MacBookPro92_Stock,
|
||||
example_data.MacBookPro.MacBookPro111_Stock,
|
||||
example_data.MacBookPro.MacBookPro133_Stock,
|
||||
# example_data.MacBookPro.MacBookPro171_Stock,
|
||||
example_data.Macmini.Macmini52_Stock,
|
||||
example_data.Macmini.Macmini61_Stock,
|
||||
example_data.Macmini.Macmini71_Stock,
|
||||
# example_data.Macmini.Macmini91_Stock,
|
||||
example_data.iMac.iMac81_Stock,
|
||||
example_data.iMac.iMac112_Stock,
|
||||
example_data.iMac.iMac122_Upgraded,
|
||||
example_data.iMac.iMac122_Upgraded_Nvidia,
|
||||
example_data.iMac.iMac151_Stock,
|
||||
example_data.MacPro.MacPro31_Stock,
|
||||
example_data.MacPro.MacPro31_Upgrade,
|
||||
example_data.MacPro.MacPro31_Modern_AMD,
|
||||
example_data.MacPro.MacPro31_Modern_Kepler,
|
||||
example_data.MacPro.MacPro41_Upgrade,
|
||||
example_data.MacPro.MacPro41_Modern_AMD,
|
||||
example_data.MacPro.MacPro41_51__Flashed_Modern_AMD,
|
||||
example_data.MacPro.MacPro41_51_Flashed_NVIDIA_WEB_DRIVERS,
|
||||
]
|
||||
Primarily for Continuous Integration
|
||||
"""
|
||||
|
||||
valid_dumps_native = [
|
||||
example_data.iMac.iMac201_Stock,
|
||||
example_data.MacBookPro.MacBookPro141_SSD_Upgrade,
|
||||
]
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
settings.validate = True
|
||||
self.constants.validate = True
|
||||
|
||||
self.valid_dumps = [
|
||||
example_data.MacBookPro.MacBookPro92_Stock,
|
||||
example_data.MacBookPro.MacBookPro111_Stock,
|
||||
example_data.MacBookPro.MacBookPro133_Stock,
|
||||
|
||||
example_data.Macmini.Macmini52_Stock,
|
||||
example_data.Macmini.Macmini61_Stock,
|
||||
example_data.Macmini.Macmini71_Stock,
|
||||
|
||||
example_data.iMac.iMac81_Stock,
|
||||
example_data.iMac.iMac112_Stock,
|
||||
example_data.iMac.iMac122_Upgraded,
|
||||
example_data.iMac.iMac122_Upgraded_Nvidia,
|
||||
example_data.iMac.iMac151_Stock,
|
||||
|
||||
example_data.MacPro.MacPro31_Stock,
|
||||
example_data.MacPro.MacPro31_Upgrade,
|
||||
example_data.MacPro.MacPro31_Modern_AMD,
|
||||
example_data.MacPro.MacPro31_Modern_Kepler,
|
||||
example_data.MacPro.MacPro41_Upgrade,
|
||||
example_data.MacPro.MacPro41_Modern_AMD,
|
||||
example_data.MacPro.MacPro41_51__Flashed_Modern_AMD,
|
||||
example_data.MacPro.MacPro41_51_Flashed_NVIDIA_WEB_DRIVERS,
|
||||
]
|
||||
|
||||
self.valid_dumps_native = [
|
||||
example_data.iMac.iMac201_Stock,
|
||||
example_data.MacBookPro.MacBookPro141_SSD_Upgrade,
|
||||
]
|
||||
|
||||
self._validate_configs()
|
||||
self._validate_sys_patch()
|
||||
|
||||
|
||||
def _build_prebuilt(self):
|
||||
"""
|
||||
Generate a build for each predefined model
|
||||
Then validate against ocvalidate
|
||||
"""
|
||||
|
||||
def build_prebuilt():
|
||||
for model in model_array.SupportedSMBIOS:
|
||||
logging.info(f"Validating predefined model: {model}")
|
||||
settings.custom_model = model
|
||||
build.build_opencore(settings.custom_model, settings).build_opencore()
|
||||
result = subprocess.run([settings.ocvalidate_path, f"{settings.opencore_release_folder}/EFI/OC/config.plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
self.constants.custom_model = model
|
||||
build.build_opencore(self.constants.custom_model, self.constants).build_opencore()
|
||||
result = subprocess.run([self.constants.ocvalidate_path, f"{self.constants.opencore_release_folder}/EFI/OC/config.plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("Error on build!")
|
||||
logging.info(result.stdout.decode())
|
||||
@@ -53,24 +72,35 @@ def validate(settings):
|
||||
else:
|
||||
logging.info(f"Validation succeeded for predefined model: {model}")
|
||||
|
||||
def build_dumps():
|
||||
for model in valid_dumps:
|
||||
settings.computer = model
|
||||
settings.custom_model = ""
|
||||
logging.info(f"Validating dumped model: {settings.computer.real_model}")
|
||||
build.build_opencore(settings.computer.real_model, settings).build_opencore()
|
||||
result = subprocess.run([settings.ocvalidate_path, f"{settings.opencore_release_folder}/EFI/OC/config.plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
def _build_dumps(self):
|
||||
"""
|
||||
Generate a build for each predefined model
|
||||
Then validate against ocvalidate
|
||||
"""
|
||||
|
||||
for model in self.valid_dumps:
|
||||
self.constants.computer = model
|
||||
self.constants.custom_model = ""
|
||||
logging.info(f"Validating dumped model: {self.constants.computer.real_model}")
|
||||
build.build_opencore(self.constants.computer.real_model, self.constants).build_opencore()
|
||||
result = subprocess.run([self.constants.ocvalidate_path, f"{self.constants.opencore_release_folder}/EFI/OC/config.plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("Error on build!")
|
||||
logging.info(result.stdout.decode())
|
||||
raise Exception(f"Validation failed for predefined model: {settings.computer.real_model}")
|
||||
raise Exception(f"Validation failed for predefined model: {self.constants.computer.real_model}")
|
||||
else:
|
||||
logging.info(f"Validation succeeded for predefined model: {settings.computer.real_model}")
|
||||
logging.info(f"Validation succeeded for predefined model: {self.constants.computer.real_model}")
|
||||
|
||||
|
||||
def validate_root_patch_files(major_kernel, minor_kernel):
|
||||
patchset = sys_patch_dict.SystemPatchDictionary(major_kernel, minor_kernel, settings.legacy_accel_support)
|
||||
def _validate_root_patch_files(self, major_kernel, minor_kernel):
|
||||
"""
|
||||
Validate that all files in the patchset are present in the payload
|
||||
"""
|
||||
|
||||
patchset = sys_patch_dict.SystemPatchDictionary(major_kernel, minor_kernel, self.constants.legacy_accel_support)
|
||||
host_os_float = float(f"{major_kernel}.{minor_kernel}")
|
||||
|
||||
for patch_subject in patchset:
|
||||
for patch_core in patchset[patch_subject]:
|
||||
patch_os_min_float = float(f'{patchset[patch_subject][patch_core]["OS Support"]["Minimum OS Support"]["OS Major"]}.{patchset[patch_subject][patch_core]["OS Support"]["Minimum OS Support"]["OS Minor"]}')
|
||||
@@ -81,58 +111,83 @@ def validate(settings):
|
||||
if install_type in patchset[patch_subject][patch_core]:
|
||||
for install_directory in patchset[patch_subject][patch_core][install_type]:
|
||||
for install_file in patchset[patch_subject][patch_core][install_type][install_directory]:
|
||||
source_file = str(settings.payload_local_binaries_root_path) + "/" + patchset[patch_subject][patch_core][install_type][install_directory][install_file] + install_directory + "/" + install_file
|
||||
source_file = str(self.constants.payload_local_binaries_root_path) + "/" + patchset[patch_subject][patch_core][install_type][install_directory][install_file] + install_directory + "/" + install_file
|
||||
if not Path(source_file).exists():
|
||||
logging.info(f"File not found: {source_file}")
|
||||
raise Exception(f"Failed to find {source_file}")
|
||||
|
||||
logging.info(f"- Validating against Darwin {major_kernel}.{minor_kernel}")
|
||||
if not sys_patch_helpers.sys_patch_helpers(settings).generate_patchset_plist(patchset, f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist", None):
|
||||
if not sys_patch_helpers.SysPatchHelpers(self.constants).generate_patchset_plist(patchset, f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist", None):
|
||||
raise Exception("Failed to generate patchset plist")
|
||||
|
||||
# Remove the plist file after validation
|
||||
Path(settings.payload_path / f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist").unlink()
|
||||
Path(self.constants.payload_path / f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist").unlink()
|
||||
|
||||
|
||||
def validate_sys_patch():
|
||||
if Path(settings.payload_local_binaries_root_path_zip).exists():
|
||||
def _validate_sys_patch(self):
|
||||
"""
|
||||
Validates sys_patch modules
|
||||
"""
|
||||
|
||||
if Path(self.constants.payload_local_binaries_root_path_zip).exists():
|
||||
logging.info("Validating Root Patch File integrity")
|
||||
if not Path(settings.payload_local_binaries_root_path).exists():
|
||||
subprocess.run(["ditto", "-V", "-x", "-k", "--sequesterRsrc", "--rsrc", settings.payload_local_binaries_root_path_zip, settings.payload_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if not Path(self.constants.payload_local_binaries_root_path).exists():
|
||||
subprocess.run(
|
||||
[
|
||||
"ditto", "-V", "-x", "-k", "--sequesterRsrc", "--rsrc",
|
||||
self.constants.payload_local_binaries_root_path_zip,
|
||||
self.constants.payload_path
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
for supported_os in [os_data.os_data.big_sur, os_data.os_data.monterey, os_data.os_data.ventura]:
|
||||
for i in range(0, 10):
|
||||
validate_root_patch_files(supported_os, i)
|
||||
self._validate_root_patch_files(supported_os, i)
|
||||
logging.info("Validating SNB Board ID patcher")
|
||||
settings.computer.reported_board_id = "Mac-7BA5B2DFE22DDD8C"
|
||||
sys_patch_helpers.sys_patch_helpers(settings).snb_board_id_patch(settings.payload_local_binaries_root_path)
|
||||
self.constants.computer.reported_board_id = "Mac-7BA5B2DFE22DDD8C"
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).snb_board_id_patch(self.constants.payload_local_binaries_root_path)
|
||||
|
||||
# Clean up
|
||||
subprocess.run(
|
||||
[
|
||||
"rm", "-rf", self.constants.payload_local_binaries_root_path
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
else:
|
||||
logging.info("- Skipping Root Patch File integrity validation")
|
||||
|
||||
|
||||
def validate_configs():
|
||||
def _validate_configs(self):
|
||||
"""
|
||||
Validates build modules
|
||||
"""
|
||||
|
||||
# First run is with default settings
|
||||
build_prebuilt()
|
||||
build_dumps()
|
||||
self._build_prebuilt()
|
||||
self._build_dumps()
|
||||
|
||||
# Second run, flip all settings
|
||||
settings.verbose_debug = True
|
||||
settings.opencore_debug = True
|
||||
settings.opencore_build = "DEBUG"
|
||||
settings.kext_debug = True
|
||||
settings.kext_variant = "DEBUG"
|
||||
settings.kext_debug = True
|
||||
settings.showpicker = False
|
||||
settings.sip_status = False
|
||||
settings.secure_status = True
|
||||
settings.firewire_boot = True
|
||||
settings.nvme_boot = True
|
||||
settings.enable_wake_on_wlan = True
|
||||
settings.disable_tb = True
|
||||
settings.force_surplus = True
|
||||
settings.software_demux = True
|
||||
settings.serial_settings = "Minimal"
|
||||
build_prebuilt()
|
||||
build_dumps()
|
||||
self.constants.verbose_debug = True
|
||||
self.constants.opencore_debug = True
|
||||
self.constants.opencore_build = "DEBUG"
|
||||
self.constants.kext_debug = True
|
||||
self.constants.kext_variant = "DEBUG"
|
||||
self.constants.kext_debug = True
|
||||
self.constants.showpicker = False
|
||||
self.constants.sip_status = False
|
||||
self.constants.secure_status = True
|
||||
self.constants.firewire_boot = True
|
||||
self.constants.nvme_boot = True
|
||||
self.constants.enable_wake_on_wlan = True
|
||||
self.constants.disable_tb = True
|
||||
self.constants.force_surplus = True
|
||||
self.constants.software_demux = True
|
||||
self.constants.serial_settings = "Minimal"
|
||||
|
||||
self._build_prebuilt()
|
||||
self._build_dumps()
|
||||
|
||||
validate_configs()
|
||||
validate_sys_patch()
|
||||
subprocess.run(["rm", "-rf", self.constants.build_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
Reference in New Issue
Block a user