diff --git a/opencore_legacy_patcher/constants.py b/opencore_legacy_patcher/constants.py index df85f673e..46b4c2ea2 100644 --- a/opencore_legacy_patcher/constants.py +++ b/opencore_legacy_patcher/constants.py @@ -812,6 +812,10 @@ class Constants: def kdk_download_path(self): return self.payload_path / Path("KDK.dmg") + @property + def metallib_download_path(self): + return self.payload_path / Path("MetallibSupportPkg.pkg") + @property def icons_path(self): return [ diff --git a/opencore_legacy_patcher/datasets/sys_patch_dict.py b/opencore_legacy_patcher/datasets/sys_patch_dict.py index adafb9a6b..13009cab6 100644 --- a/opencore_legacy_patcher/datasets/sys_patch_dict.py +++ b/opencore_legacy_patcher/datasets/sys_patch_dict.py @@ -2,11 +2,16 @@ sys_patch_dict.py: Dictionary defining patch sets used during Root Volume patching (sys_patch.py) """ +import enum import packaging.version from . import os_data +class DynamicPatchset(enum.Enum): + MetallibSupportPkg = "MetallibSupportPkg" + + class SystemPatchDictionary(): """ Library for generating patch sets for sys_patch.py and supporting modules @@ -70,8 +75,8 @@ class SystemPatchDictionary(): self.patchset_dict: dict = {} self.marketing_version: str = marketing_version - self.affected_by_cve_2024_23227: bool = self.__is_affect_by_cve_2024_23227() - self.metallib_directory: str = self.__resolve_metallibsupportpkg() + self.affected_by_cve_2024_23227: bool = self.__is_affect_by_cve_2024_23227() + self.metallib_directory: DynamicPatchset = DynamicPatchset.MetallibSupportPkg # XNU Kernel versions self.macOS_12_0_B7: float = 21.1 @@ -149,13 +154,6 @@ class SystemPatchDictionary(): return False - def __resolve_metallibsupportpkg(self) -> str: - """ - Temporarily hard coded - """ - return "/Library/Application Support/Dortania/MetallibSupportPkg/15.0-24A5327a" - - def _generate_sys_patch_dict(self): """ Generates the sys_patch_dict dictionary @@ -804,6 +802,12 @@ class SystemPatchDictionary(): "default.metallib": self.metallib_directory, }, }, + "Remove": { + "/System/Library/PrivateFrameworks/RenderBox.framework/Versions/A/Resources": [ + # For some reason Ivy Bridge can't tell the metallib lacks AIR64 support, and errors out + "archive.metallib", + ], + }, }, # Primarily for AMD GCN GPUs diff --git a/opencore_legacy_patcher/support/metallib_handler.py b/opencore_legacy_patcher/support/metallib_handler.py new file mode 100644 index 000000000..a8e2d14cc --- /dev/null +++ b/opencore_legacy_patcher/support/metallib_handler.py @@ -0,0 +1,271 @@ +""" +metallib_handler.py: Library for handling Metal libraries +""" + +import logging +import requests +import subprocess +import packaging.version + +from typing import cast +from pathlib import Path + +from . import network_handler, subprocess_wrapper +from .. import constants + +from ..datasets import os_data + + +METALLIB_INSTALL_PATH: str = "/Library/Application Support/Dortania/MetallibSupportPkg" +METALLIB_API_LINK: str = "https://dortania.github.io/MetallibSupportPkg/manifest.json" + +METALLIB_ASSET_LIST: list = None + + +class MetalLibraryObject: + + def __init__(self, global_constants: constants.Constants, + host_build: str, host_version: str, + ignore_installed: bool = False, passive: bool = False + ) -> None: + + 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 MetallibSupportPkg PKGs and download the latest + self.metallib_already_installed: bool = False + + self.metallib_installed_path: str = "" + + self.metallib_url: str = "" + self.metallib_url_build: str = "" + self.metallib_url_version: str = "" + + self.metallib_url_is_exactly_match: bool = False + + self.metallib_closest_match_url: str = "" + self.metallib_closest_match_url_build: str = "" + self.metallib_closest_match_url_version: str = "" + + self.success: bool = False + + self.error_msg: str = "" + + self._get_latest_metallib() + + + def _get_remote_metallibs(self) -> dict: + """ + Get the MetallibSupportPkg list from the API + """ + + global METALLIB_ASSET_LIST + + logging.info("Pulling metallib list from MetallibSupportPkg API") + if METALLIB_ASSET_LIST: + return METALLIB_ASSET_LIST + + try: + results = network_handler.NetworkUtilities().get( + METALLIB_API_LINK, + headers={ + "User-Agent": f"OCLP/{self.constants.patcher_version}" + }, + timeout=5 + ) + except (requests.exceptions.Timeout, requests.exceptions.TooManyRedirects, requests.exceptions.ConnectionError): + logging.info("Could not contact MetallibSupportPkg API") + return None + + if results.status_code != 200: + logging.info("Could not fetch Metallib list") + return None + + METALLIB_ASSET_LIST = results.json() + + return METALLIB_ASSET_LIST + + + def _get_latest_metallib(self) -> None: + """ + Get the latest MetallibSupportPkg PKG + """ + + parsed_version = cast(packaging.version.Version, packaging.version.parse(self.host_version)) + + if os_data.os_conversion.os_to_kernel(str(parsed_version.major)) < os_data.os_data.sequoia: + self.error_msg = "MetallibSupportPkg is not required for macOS Sonoma or older" + logging.warning(f"{self.error_msg}") + return + + self.metallib_installed_path = self._local_metallib_installed() + if self.metallib_installed_path: + logging.info(f"metallib already installed ({Path(self.metallib_installed_path).name}), skipping") + self.metallib_already_installed = True + self.success = True + return + + remote_metallib_version = self._get_remote_metallibs() + + if remote_metallib_version is None: + logging.warning("Failed to fetch metallib list, falling back to local metallib matching") + + # First check if a metallib matching the current macOS version is installed + # ex. 13.0.1 vs 13.0 + loose_version = f"{parsed_version.major}.{parsed_version.minor}" + logging.info(f"Checking for metallibs loosely matching {loose_version}") + self.metallib_installed_path = self._local_metallib_installed(match=loose_version, check_version=True) + if self.metallib_installed_path: + logging.info(f"Found matching metallib: {Path(self.metallib_installed_path).name}") + self.metallib_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 metallibs matching {older_version}") + self.metallib_installed_path = self._local_metallib_installed(match=older_version, check_version=True) + if self.metallib_installed_path: + logging.info(f"Found matching metallib: {Path(self.metallib_installed_path).name}") + self.metallib_already_installed = True + self.success = True + return + + logging.warning(f"Couldn't find metallib matching {self.host_version} or {older_version}, please install one manually") + + self.error_msg = f"Could not contact MetallibSupportPkg API, and no metallib matching {self.host_version} ({self.host_build}) or {older_version} was installed.\nPlease ensure you have a network connection or manually install a metallib." + + return + + + # First check exact match + for metallib in remote_metallib_version: + if (metallib["build"] != self.host_build): + continue + self.metallib_url = metallib["url"] + self.metallib_url_build = metallib["build"] + self.metallib_url_version = metallib["version"] + self.metallib_url_is_exactly_match = True + break + + # If no exact match, check for closest match + if self.metallib_url == "": + for metallib in remote_metallib_version: + metallib_version = cast(packaging.version.Version, packaging.version.parse(metallib["version"])) + if metallib_version > parsed_version: + continue + if metallib_version.major != parsed_version.major: + continue + if metallib_version.minor not in range(parsed_version.minor - 1, parsed_version.minor + 1): + continue + + # The metallib list is already sorted by version then date, so the first match is the closest + self.metallib_closest_match_url = metallib["url"] + self.metallib_closest_match_url_build = metallib["build"] + self.metallib_closest_match_url_version = metallib["version"] + self.metallib_url_is_exactly_match = False + break + + if self.metallib_url == "": + if self.metallib_closest_match_url == "": + logging.warning(f"No metallibs found for {self.host_build} ({self.host_version})") + self.error_msg = f"No metallibs found for {self.host_build} ({self.host_version})" + return + logging.info(f"No direct match found for {self.host_build}, falling back to closest match") + logging.info(f"Closest Match: {self.metallib_closest_match_url_build} ({self.metallib_closest_match_url_version})") + + self.metallib_url = self.metallib_closest_match_url + self.metallib_url_build = self.metallib_closest_match_url_build + self.metallib_url_version = self.metallib_closest_match_url_version + else: + logging.info(f"Direct match found for {self.host_build} ({self.host_version})") + + + # Check if this metallib is already installed + self.metallib_installed_path = self._local_metallib_installed(match=self.metallib_url_build) + if self.metallib_installed_path: + logging.info(f"metallib already installed ({Path(self.metallib_installed_path).name}), skipping") + self.metallib_already_installed = True + self.success = True + return + + logging.info("Following metallib is recommended:") + logging.info(f"- metallib Build: {self.metallib_url_build}") + logging.info(f"- metallib Version: {self.metallib_url_version}") + logging.info(f"- metallib URL: {self.metallib_url}") + + self.success = True + + + def _local_metallib_installed(self, match: str = None, check_version: bool = False) -> str: + """ + Check if a metallib is already installed + """ + + if not Path(METALLIB_INSTALL_PATH).exists(): + return None + + for metallib_folder in Path(METALLIB_INSTALL_PATH).iterdir(): + if not metallib_folder.is_dir(): + continue + if check_version: + if match not in metallib_folder.name: + continue + else: + if not metallib_folder.name.endswith(f"-{match}"): + continue + + return metallib_folder + + return None + + + def retrieve_download(self, override_path: str = "") -> network_handler.DownloadObject: + """ + Retrieve MetallibSupportPkg PKG download object + """ + + self.success = False + self.error_msg = "" + + if self.metallib_already_installed: + logging.info("No download required, metallib already installed") + self.success = True + return None + + if self.metallib_url == "": + self.error_msg = "Could not retrieve metallib catalog, no metallib to download" + logging.error(self.error_msg) + return None + + logging.info(f"Returning DownloadObject for metallib: {Path(self.metallib_url).name}") + self.success = True + + metallib_download_path = self.constants.metallib_download_path if override_path == "" else Path(override_path) + return network_handler.DownloadObject(self.metallib_url, metallib_download_path) + + + def install_metallib(self, metallib: str = None) -> None: + """ + Install MetallibSupportPkg PKG + """ + + if not self.success: + logging.error("Cannot install metallib, no metallib was successfully retrieved") + return False + + if self.metallib_already_installed: + logging.info("No installation required, metallib already installed") + return True + + result = subprocess_wrapper.run_as_root([ + "/usr/sbin/installer", "-pkg", metallib if metallib else self.constants.metallib_download_path, "-target", "/" + ]) + if result.returncode != 0: + subprocess_wrapper.log(result) + return False + + return True \ No newline at end of file diff --git a/opencore_legacy_patcher/support/validation.py b/opencore_legacy_patcher/support/validation.py index ead7b0807..fc68969cb 100644 --- a/opencore_legacy_patcher/support/validation.py +++ b/opencore_legacy_patcher/support/validation.py @@ -132,6 +132,8 @@ class PatcherValidation: 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]: + if patchset[patch_subject][patch_core][install_type][install_directory][install_file] in sys_patch_dict.DynamicPatchset: + continue 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}") diff --git a/opencore_legacy_patcher/sys_patch/detections/detect.py b/opencore_legacy_patcher/sys_patch/detections/detect.py index 9332d3e3c..dc13265ab 100644 --- a/opencore_legacy_patcher/sys_patch/detections/detect.py +++ b/opencore_legacy_patcher/sys_patch/detections/detect.py @@ -660,13 +660,14 @@ class DetectRootPatch: "Settings: Requires AMFI exemption": self.amfi_must_disable, "Settings: Supports Auxiliary Cache": not self.requires_root_kc, "Settings: Kernel Debug Kit missing": self.missing_kdk if self.os_major >= os_data.os_data.ventura.value else False, + "Settings: MetallibSupportPkg missing": self.os_major >= os_data.os_data.sequoia.value and any([self.kepler_gpu, self.ivy_gpu, self.haswell_gpu]), "Validation: Patching Possible": self.verify_patch_allowed(), "Validation: Unpatching Possible": self._verify_unpatch_allowed(), f"Validation: Unsupported Host OS": self.unsupported_os, 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, @@ -719,9 +720,7 @@ class DetectRootPatch: return True if any([self.kepler_gpu, self.ivy_gpu, self.haswell_gpu]): if Path("~/.dortania_developer").expanduser().exists(): - # Temporarily hard coded - if Path("/Library/Application Support/Dortania/MetallibSupportPkg/15.0-24A5327a").exists(): - return True + return True return False return True diff --git a/opencore_legacy_patcher/sys_patch/sys_patch.py b/opencore_legacy_patcher/sys_patch/sys_patch.py index 3a36860a4..60f352db2 100644 --- a/opencore_legacy_patcher/sys_patch/sys_patch.py +++ b/opencore_legacy_patcher/sys_patch/sys_patch.py @@ -39,7 +39,8 @@ import logging import plistlib import subprocess -from pathlib import Path +from pathlib import Path +from functools import cache from .mount import ( RootVolumeMount, @@ -54,12 +55,16 @@ from .utilities import ( from .. import constants -from ..datasets import os_data from ..volume import generate_copy_arguments +from ..datasets import ( + os_data, + sys_patch_dict +) from ..support import ( utilities, - subprocess_wrapper + subprocess_wrapper, + metallib_handler ) from . import ( sys_patch_helpers, @@ -357,7 +362,7 @@ class PatchSysVolume: ) source_files_path = str(self.constants.payload_local_binaries_root_path) - self._preflight_checks(required_patches, source_files_path) + required_patches = self._preflight_checks(required_patches, source_files_path) for patch in required_patches: logging.info("- Installing Patchset: " + patch) for method_remove in ["Remove", "Remove Non-Root"]: @@ -429,13 +434,56 @@ class PatchSysVolume: self._write_patchset(required_patches) - def _preflight_checks(self, required_patches: dict, source_files_path: Path) -> None: + def _resolve_metallib_support_pkg(self) -> str: + """ + Resolves MetalLibSupportPkg + """ + metallib_obj = metallib_handler.MetalLibraryObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version) + if metallib_obj.success is False: + logging.error(f"Failed to find MetalLibSupportPkg: {metallib_obj.error_msg}") + raise Exception(f"Failed to find MetalLibSupportPkg: {metallib_obj.error_msg}") + + metallib_download_obj = metallib_obj.retrieve_download() + if not metallib_download_obj: + # Already downloaded, return path + logging.info(f"Using MetalLibSupportPkg: {metallib_obj.metallib_installed_path}") + return str(metallib_obj.metallib_installed_path) + + metallib_download_obj.download(spawn_thread=False) + if metallib_download_obj.download_complete is False: + error_msg = metallib_download_obj.error_msg + logging.error(f"Could not download MetalLibSupportPkg: {error_msg}") + raise Exception(f"Could not download MetalLibSupportPkg: {error_msg}") + + if metallib_obj.install_metallib() is False: + logging.error("Failed to install MetalLibSupportPkg") + raise Exception("Failed to install MetalLibSupportPkg") + + # After install, check if it's present + return self._resolve_metallib_support_pkg() + + + @cache + def _resolve_dynamic_patchset(self, variant: sys_patch_dict.DynamicPatchset) -> str: + """ + Resolves dynamic patchset to a path + """ + if variant == sys_patch_dict.DynamicPatchset.MetallibSupportPkg: + return self._resolve_metallib_support_pkg() + + raise Exception(f"Unknown Dynamic Patchset: {variant}") + + + def _preflight_checks(self, required_patches: dict, source_files_path: Path) -> dict: """ Runs preflight checks before patching Parameters: required_patches (dict): Patchset dictionary (from sys_patch_generate.GenerateRootPatchSets) source_files_path (Path): Path to the source files (PatcherSupportPkg) + + Returns: + dict: Updated patchset dictionary """ logging.info("- Running Preflight Checks before patching") @@ -447,6 +495,9 @@ class PatchSysVolume: continue for install_patch_directory in required_patches[patch][method_type]: for install_file in required_patches[patch][method_type][install_patch_directory]: + if required_patches[patch][method_type][install_patch_directory][install_file] in sys_patch_dict.DynamicPatchset: + required_patches[patch][method_type][install_patch_directory][install_file] = self._resolve_dynamic_patchset(required_patches[patch][method_type][install_patch_directory][install_file]) + source_file = required_patches[patch][method_type][install_patch_directory][install_file] + install_patch_directory + "/" + install_file # Check whether to source from root @@ -477,6 +528,8 @@ class PatchSysVolume: logging.info("- Finished Preflight, starting patching") + return required_patches + # Entry Function def start_patch(self): diff --git a/opencore_legacy_patcher/wx_gui/gui_sys_patch_start.py b/opencore_legacy_patcher/wx_gui/gui_sys_patch_start.py index 8f4afa434..17b8e908e 100644 --- a/opencore_legacy_patcher/wx_gui/gui_sys_patch_start.py +++ b/opencore_legacy_patcher/wx_gui/gui_sys_patch_start.py @@ -16,8 +16,11 @@ from pathlib import Path from .. import constants from ..datasets import os_data -from ..support import kdk_handler +from ..support import ( + kdk_handler, + metallib_handler +) from ..sys_patch import ( sys_patch, ) @@ -137,6 +140,95 @@ class SysPatchStartFrame(wx.Frame): return True + def _metallib_download(self, frame: wx.Frame = None) -> bool: + frame = self if not frame else frame + + logging.info("MetallibSupportPkg missing, generating Metallib download frame") + + header = wx.StaticText(frame, label="Downloading Metal Libraries", pos=(-1,5)) + header.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD)) + header.Centre(wx.HORIZONTAL) + + subheader = wx.StaticText(frame, label="Fetching MetallibSupportPkg database...", pos=(-1, header.GetPosition()[1] + header.GetSize()[1] + 5)) + subheader.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL)) + subheader.Centre(wx.HORIZONTAL) + + progress_bar = wx.Gauge(frame, range=100, pos=(-1, subheader.GetPosition()[1] + subheader.GetSize()[1] + 5), size=(250, 20)) + progress_bar.Centre(wx.HORIZONTAL) + + progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar) + progress_bar_animation.start_pulse() + + # Set size of frame + frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 35)) + frame.Show() + + self.metallib_obj: metallib_handler.MetalLibraryObject = None + def _metallib_thread_spawn(): + self.metallib_obj = metallib_handler.MetalLibraryObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version) + + metallib_thread = threading.Thread(target=_metallib_thread_spawn) + metallib_thread.start() + + while metallib_thread.is_alive(): + wx.Yield() + + if self.metallib_obj.success is False: + progress_bar_animation.stop_pulse() + progress_bar.SetValue(0) + wx.MessageBox(f"Metallib download failed: {self.metallib_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR) + return False + + self.metallib_download_obj = self.metallib_obj.retrieve_download() + if not self.metallib_download_obj: + # Metallib is already downloaded + return True + + gui_download.DownloadFrame( + self, + title=self.title, + global_constants=self.constants, + download_obj=self.metallib_download_obj, + item_name=f"Metallib Build {self.metallib_obj.metallib_url_build}" + ) + if self.metallib_download_obj.download_complete is False: + return False + + logging.info("Metallib download complete, installing Metallib PKG") + + header.SetLabel(f"Installing Metallib: {self.metallib_obj.metallib_url_build}") + header.Centre(wx.HORIZONTAL) + + subheader.SetLabel("Installing MetallibSupportPkg PKG...") + subheader.Centre(wx.HORIZONTAL) + + self.result = False + def _install_metallib(): + self.result = self.metallib_obj.install_metallib() + + install_thread = threading.Thread(target=_install_metallib) + install_thread.start() + + while install_thread.is_alive(): + wx.Yield() + + if self.result is False: + progress_bar_animation.stop_pulse() + progress_bar.SetValue(0) + wx.MessageBox(f"Metallib installation failed: {self.metallib_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR) + return False + + progress_bar_animation.stop_pulse() + progress_bar.SetValue(100) + + logging.info("Metallib installation complete") + + for child in frame.GetChildren(): + child.Destroy() + + return True + + def _generate_modal(self, patches: dict = {}, variant: str = "Root Patching"): """ Create UI for root patching/unpatching @@ -227,6 +319,10 @@ class SysPatchStartFrame(wx.Frame): if self._kdk_download(self) is False: sys.exit(1) + if self.patches["Settings: MetallibSupportPkg missing"] is True: + if self._metallib_download(self) is False: + sys.exit(1) + self._generate_modal(self.patches, "Root Patching") self.return_button.Disable()