mirror of
https://github.com/dortania/OpenCore-Legacy-Patcher.git
synced 2026-04-11 16:27:19 +10:00
Add support for KDK caching
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -9,9 +9,19 @@
|
||||
- Resolve Photos app crash
|
||||
- Workaround tile window popup freezing apps by disabling the feature
|
||||
- Workaround monochrome desktop widgets rendering issues by enforcing full color (can be disabled in OCLP settings)
|
||||
- Add new Launch Daemon for clean up on macOS updates
|
||||
- Resolves KDKless Macs failing to boot after updating from 14.0 to 14.x
|
||||
- `/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.macos-update.plist`
|
||||
- Add new arguments:
|
||||
- `--cache_os`: Cache nessasary patcher files for OS to be installed (ex. KDKs)
|
||||
- `--prepare_for_update`: Clean up patcher files for OS to be installed (ex. /Library/Extensions)
|
||||
- Add new Launch Daemons for handling macOS updates:
|
||||
- `macos-update.plist`:
|
||||
- Resolves KDKless Macs failing to boot after updating from 14.0 to 14.x
|
||||
- Adds support for KDK caching for OS to be installed
|
||||
- Invoked when update is staged
|
||||
- `/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.macos-update.plist`
|
||||
- `os-caching.plist`
|
||||
- Resolves unsupported/old KDKs from being used post-update
|
||||
- Invoked when update is downloading
|
||||
- `/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.os-caching.plist`
|
||||
- Load UI icons from local path
|
||||
- Resolves macOS downloader crash on slower machines
|
||||
- Resolve iMac18,2 internal 4K display support
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AssociatedBundleIdentifiers</key>
|
||||
<string>com.dortania.opencore-legacy-patcher</string>
|
||||
<key>Label</key>
|
||||
<string>com.dortania.opencore-legacy-patcher.rsr-monitor</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Library/Application Support/Dortania/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher</string>
|
||||
<string>--cache_os</string>
|
||||
</array>
|
||||
<key>WatchPaths</key>
|
||||
<array>
|
||||
<string>/System/Volumes/Update/Preflight.plist</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -11,6 +11,7 @@ from data import model_array, os_data
|
||||
from resources.build import build
|
||||
from resources.sys_patch import sys_patch, sys_patch_auto
|
||||
from resources import defaults, utilities, validation, constants
|
||||
from resources.wx_gui import gui_entry
|
||||
|
||||
|
||||
# Generic building args
|
||||
@@ -46,8 +47,11 @@ class arguments:
|
||||
return
|
||||
|
||||
if self.args.prepare_for_update:
|
||||
logging.info("Preparing host for macOS update")
|
||||
self._clean_le_handler()
|
||||
self._prepare_for_update_handler()
|
||||
return
|
||||
|
||||
if self.args.cache_os:
|
||||
self._cache_os_handler()
|
||||
return
|
||||
|
||||
if self.args.auto_patch:
|
||||
@@ -97,31 +101,47 @@ class arguments:
|
||||
sys_patch_auto.AutomaticSysPatch(self.constants).start_auto_patch()
|
||||
|
||||
|
||||
def _prepare_for_update_handler(self) -> None:
|
||||
"""
|
||||
Prepare host for macOS update
|
||||
"""
|
||||
logging.info("Preparing host for macOS update")
|
||||
|
||||
os_data = utilities.fetch_staged_update(variant="Update")
|
||||
if os_data[0] is None:
|
||||
logging.info("No update staged, skipping")
|
||||
return
|
||||
|
||||
os_version = os_data[0]
|
||||
os_build = os_data[1]
|
||||
|
||||
logging.info(f"Preparing for update to {os_version} ({os_build})")
|
||||
|
||||
self._clean_le_handler()
|
||||
|
||||
|
||||
def _cache_os_handler(self) -> None:
|
||||
"""
|
||||
Fetch KDK for incoming OS
|
||||
"""
|
||||
results = subprocess.run(["ps", "-ax"], stdout=subprocess.PIPE)
|
||||
if results.stdout.decode("utf-8").count("OpenCore-Patcher --cache_os") > 1:
|
||||
logging.info("Another instance of OS caching is running, exiting")
|
||||
return
|
||||
|
||||
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.OS_CACHE)
|
||||
|
||||
|
||||
def _clean_le_handler(self) -> None:
|
||||
"""
|
||||
Check if software update is staged
|
||||
If so, clean /Library/Extensions
|
||||
Clean /Library/Extensions of problematic kexts
|
||||
Note macOS Ventura and older do this automatically
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.sonoma:
|
||||
logging.info("Host doesn't require cleaning, skipping")
|
||||
return
|
||||
|
||||
update_config = "/System/Volumes/Update/Update.plist"
|
||||
if not Path(update_config).exists():
|
||||
logging.info("No update staged, skipping")
|
||||
return
|
||||
|
||||
try:
|
||||
update_staged = plistlib.load(open(update_config, "rb"))
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to load update config: {e}")
|
||||
return
|
||||
if "update-asset-attributes" not in update_staged:
|
||||
logging.info("No update staged, skipping")
|
||||
return
|
||||
|
||||
logging.info("Update staged, cleaning /Library/Extensions")
|
||||
logging.info("Cleaning /Library/Extensions")
|
||||
|
||||
for kext in Path("/Library/Extensions").glob("*.kext"):
|
||||
if not Path(f"{kext}/Contents/Info.plist").exists():
|
||||
|
||||
@@ -296,6 +296,10 @@ class Constants:
|
||||
def update_launch_daemon_path(self):
|
||||
return self.launch_services_path / Path("com.dortania.opencore-legacy-patcher.macos-update.plist")
|
||||
|
||||
@property
|
||||
def kdk_launch_daemon_path(self):
|
||||
return self.launch_services_path / Path("com.dortania.opencore-legacy-patcher.os-caching.plist")
|
||||
|
||||
# ACPI
|
||||
@property
|
||||
def pci_ssdt_path(self):
|
||||
|
||||
@@ -648,7 +648,10 @@ class PatchSysVolume:
|
||||
self._execute_patchset(sys_patch_generate.GenerateRootPatchSets(self.computer.real_model, self.constants, self.hardware_details).patchset)
|
||||
|
||||
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()
|
||||
needs_daemon = False
|
||||
if self.constants.detected_os >= os_data.os_data.ventura and self.skip_root_kmutil_requirement is False:
|
||||
needs_daemon = True
|
||||
sys_patch_auto.AutomaticSysPatch(self.constants).install_auto_patcher_launch_agent(kdk_caching_needed=needs_daemon)
|
||||
|
||||
self._rebuild_root_volume()
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import logging
|
||||
import plistlib
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import hashlib
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -331,7 +332,7 @@ Please check the Github page for more information about this release."""
|
||||
logging.info("- Unable to determine if boot disk is removable, skipping prompt")
|
||||
|
||||
|
||||
def install_auto_patcher_launch_agent(self):
|
||||
def install_auto_patcher_launch_agent(self, kdk_caching_needed: bool = False):
|
||||
"""
|
||||
Install the Auto Patcher Launch Agent
|
||||
|
||||
@@ -350,12 +351,16 @@ Please check the Github page for more information about this release."""
|
||||
self.constants.auto_patch_launch_agent_path: "/Library/LaunchAgents/com.dortania.opencore-legacy-patcher.auto-patch.plist",
|
||||
self.constants.update_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.macos-update.plist",
|
||||
**({ self.constants.rsr_monitor_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.rsr-monitor.plist" } if self._create_rsr_monitor_daemon() else {}),
|
||||
**({ self.constants.kdk_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.os-caching.plist" } if kdk_caching_needed is True else {} ),
|
||||
}
|
||||
|
||||
for service in services:
|
||||
name = Path(service).name
|
||||
logging.info(f"- Installing {name}")
|
||||
if Path(services[service]).exists():
|
||||
if hashlib.sha256(open(service, "rb").read()).hexdigest() == hashlib.sha256(open(services[service], "rb").read()).hexdigest():
|
||||
logging.info(f" - {name} checksums match, skipping")
|
||||
continue
|
||||
logging.info(f" - Existing service found, removing")
|
||||
utilities.process_status(utilities.elevated(["rm", services[service]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
# Create parent directories
|
||||
|
||||
@@ -553,6 +553,33 @@ def elevated(*args, **kwargs) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(["sudo"] + [args[0][0]] + args[0][1:], **kwargs)
|
||||
|
||||
|
||||
def fetch_staged_update(variant: str = "Update") -> (str, str):
|
||||
"""
|
||||
Check for staged macOS update
|
||||
Supported variants:
|
||||
- Preflight
|
||||
- Update
|
||||
"""
|
||||
|
||||
os_build = None
|
||||
os_version = None
|
||||
|
||||
update_config = f"/System/Volumes/Update/{variant}.plist"
|
||||
if not Path(update_config).exists():
|
||||
return (None, None)
|
||||
try:
|
||||
update_staged = plistlib.load(open(update_config, "rb"))
|
||||
except:
|
||||
return (None, None)
|
||||
if "update-asset-attributes" not in update_staged:
|
||||
return (None, None)
|
||||
|
||||
os_build = update_staged["update-asset-attributes"]["Build"]
|
||||
os_version = update_staged["update-asset-attributes"]["OSVersion"]
|
||||
|
||||
return os_version, os_build
|
||||
|
||||
|
||||
def check_cli_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build", help="Build OpenCore", action="store_true", required=False)
|
||||
@@ -581,6 +608,7 @@ def check_cli_args():
|
||||
parser.add_argument("--patch_sys_vol", help="Patches root volume", action="store_true", required=False)
|
||||
parser.add_argument("--unpatch_sys_vol", help="Unpatches root volume, EXPERIMENTAL", action="store_true", required=False)
|
||||
parser.add_argument("--prepare_for_update", help="Prepares host for macOS update, ex. clean /Library/Extensions", action="store_true", required=False)
|
||||
parser.add_argument("--cache_os", help="Caches patcher files (ex. KDKs) for incoming OS in Preflight.plist", action="store_true", required=False)
|
||||
|
||||
# validation args
|
||||
parser.add_argument("--validate", help="Runs Validation Tests for CI", action="store_true", required=False)
|
||||
@@ -598,7 +626,8 @@ def check_cli_args():
|
||||
args.unpatch_sys_vol or
|
||||
args.validate or
|
||||
args.auto_patch or
|
||||
args.prepare_for_update
|
||||
args.prepare_for_update or
|
||||
args.cache_os
|
||||
):
|
||||
return None
|
||||
else:
|
||||
|
||||
188
resources/wx_gui/gui_cache_os_update.py
Normal file
188
resources/wx_gui/gui_cache_os_update.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
UI to display to users before a macOS update is applied
|
||||
Primarily for caching updates required for incoming OS (ex. KDKs)
|
||||
"""
|
||||
|
||||
import wx
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from resources import constants, kdk_handler, utilities
|
||||
from resources.wx_gui import gui_support, gui_download
|
||||
|
||||
|
||||
class OSUpdateFrame(wx.Frame):
|
||||
"""
|
||||
Create a modal frame for displaying information to the user before an update is applied
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing Prepare Update Frame")
|
||||
|
||||
if parent:
|
||||
self.frame = parent
|
||||
else:
|
||||
super().__init__(parent, title=title, size=(360, 140), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX)
|
||||
self.frame = self
|
||||
self.frame.Centre()
|
||||
|
||||
self.title = title
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
os_data = utilities.fetch_staged_update(variant="Preflight")
|
||||
if os_data[0] is None:
|
||||
logging.info("No staged update found")
|
||||
self._exit()
|
||||
logging.info(f"Staged update found: {os_data[0]} ({os_data[1]})")
|
||||
self.os_data = os_data
|
||||
|
||||
self._generate_ui()
|
||||
|
||||
self.kdk_obj: kdk_handler.KernelDebugKitObject = None
|
||||
def _kdk_thread_spawn():
|
||||
self.kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.os_data[1], self.os_data[0], passive=True)
|
||||
|
||||
kdk_thread = threading.Thread(target=_kdk_thread_spawn)
|
||||
kdk_thread.start()
|
||||
|
||||
while kdk_thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if self.kdk_obj.success is False:
|
||||
self._exit()
|
||||
|
||||
kdk_download_obj = self.kdk_obj.retrieve_download()
|
||||
if not kdk_download_obj:
|
||||
# KDK is already downloaded
|
||||
# Return false since we didn't display anything
|
||||
self._exit()
|
||||
|
||||
self.kdk_download_obj = kdk_download_obj
|
||||
|
||||
self.frame.Show()
|
||||
|
||||
self.did_cancel = False
|
||||
self._notifyUser()
|
||||
|
||||
# Allow 10 seconds for the user to cancel the download
|
||||
# If nothing, continue
|
||||
for i in range(0, 10):
|
||||
if self.did_cancel is True:
|
||||
self._exit()
|
||||
time.sleep(1)
|
||||
|
||||
gui_download.DownloadFrame(
|
||||
self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
download_obj=kdk_download_obj,
|
||||
item_name=f"KDK Build {self.kdk_obj.kdk_url_build}"
|
||||
)
|
||||
if kdk_download_obj.download_complete is False:
|
||||
self._exit()
|
||||
|
||||
logging.info("KDK download complete, validating with hdiutil")
|
||||
self.kdk_checksum_result = False
|
||||
def _validate_kdk_checksum_thread():
|
||||
self.kdk_checksum_result = self.kdk_obj.validate_kdk_checksum()
|
||||
|
||||
kdk_checksum_thread = threading.Thread(target=_validate_kdk_checksum_thread)
|
||||
kdk_checksum_thread.start()
|
||||
|
||||
while kdk_checksum_thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if self.kdk_checksum_result is False:
|
||||
logging.error("KDK checksum validation failed")
|
||||
logging.error(self.kdk_obj.error_msg)
|
||||
self._exit()
|
||||
|
||||
logging.info("KDK checksum validation passed")
|
||||
|
||||
logging.info("Mounting KDK")
|
||||
if not Path(self.constants.kdk_download_path).exists():
|
||||
logging.error("KDK download path does not exist")
|
||||
self._exit()
|
||||
|
||||
self.kdk_install_result = False
|
||||
def _install_kdk_thread():
|
||||
self.kdk_install_result = kdk_handler.KernelDebugKitUtilities().install_kdk_dmg(self.constants.kdk_download_path, only_install_backup=True)
|
||||
|
||||
kdk_install_thread = threading.Thread(target=_install_kdk_thread)
|
||||
kdk_install_thread.start()
|
||||
|
||||
while kdk_install_thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if self.kdk_install_result is False:
|
||||
logging.info("Failed to install KDK")
|
||||
self._exit()
|
||||
|
||||
logging.info("KDK installed successfully")
|
||||
self._exit()
|
||||
|
||||
|
||||
def _generate_ui(self) -> None:
|
||||
"""
|
||||
Display frame
|
||||
|
||||
|
||||
Title: OpenCore Legacy Patcher is preparing to update your system
|
||||
Body: Please wait while we prepare your system for the update.
|
||||
This may take a few minutes.
|
||||
"""
|
||||
|
||||
header = wx.StaticText(self.frame, label="Preparing for macOS Software Update", pos=(-1,5))
|
||||
header.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
header.Centre(wx.HORIZONTAL)
|
||||
|
||||
# list OS
|
||||
label = wx.StaticText(self.frame, label=f"macOS {self.os_data[0]} ({self.os_data[1]})", pos=(-1, 35))
|
||||
label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# this may take a few minutes
|
||||
label = wx.StaticText(self.frame, label="This may take a few minutes.", pos=(-1, 55))
|
||||
label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Add a progress bar
|
||||
self.progress_bar = wx.Gauge(self.frame, range=100, pos=(10, 75), size=(340, 20))
|
||||
self.progress_bar.SetValue(0)
|
||||
self.progress_bar.Pulse()
|
||||
|
||||
# Set frame size below progress bar
|
||||
self.frame.SetSize((360, 140))
|
||||
|
||||
|
||||
def _notifyUser(self) -> None:
|
||||
"""
|
||||
Notify user of what OCLP is doing
|
||||
Note will be spawned through wx.CallAfter
|
||||
"""
|
||||
threading.Thread(target=self._notifyUserThread).start()
|
||||
|
||||
|
||||
def _notifyUserThread(self) -> None:
|
||||
"""
|
||||
Notify user of what OCLP is doing
|
||||
"""
|
||||
message=f"OpenCore Legacy Patcher has detected that a macOS update is being downloaded:\n{self.os_data[0]} ({self.os_data[1]})\n\nThe patcher needs to prepare the system for the update, and will download any additional resources it may need post-update.\n\nThis may take a few minutes, the patcher will exit when it is done."
|
||||
# Yes/No for caching
|
||||
dlg = wx.MessageDialog(self.frame, message=message, caption="OpenCore Legacy Patcher", style=wx.YES_NO | wx.ICON_INFORMATION)
|
||||
dlg.SetYesNoLabels("&Ok", "&Cancel")
|
||||
result = dlg.ShowModal()
|
||||
if result == wx.ID_NO:
|
||||
logging.info("User cancelled OS caching")
|
||||
self.kdk_download_obj.stop()
|
||||
self.did_cancel = True
|
||||
|
||||
def _exit(self):
|
||||
"""
|
||||
Exit the frame
|
||||
"""
|
||||
self.frame.Close()
|
||||
sys.exit()
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
|
||||
from resources import constants
|
||||
from resources.wx_gui import (
|
||||
gui_cache_os_update,
|
||||
gui_main_menu,
|
||||
gui_build,
|
||||
gui_install_oc,
|
||||
@@ -24,6 +25,7 @@ class SupportedEntryPoints:
|
||||
INSTALL_OC = gui_install_oc.InstallOCFrame
|
||||
SYS_PATCH = gui_sys_patch_start.SysPatchStartFrame
|
||||
UPDATE_APP = gui_update.UpdateFrame
|
||||
OS_CACHE = gui_cache_os_update.OSUpdateFrame
|
||||
|
||||
|
||||
class EntryPoint:
|
||||
|
||||
Reference in New Issue
Block a user