Merge pull request #1064 from dortania/gui-refactor

GUI Refactor: Implement modularized wxPython system
This commit is contained in:
Mykola Grymalyuk
2023-05-18 20:48:07 -07:00
committed by GitHub
36 changed files with 4581 additions and 4343 deletions

4
.gitignore vendored
View File

@@ -34,4 +34,6 @@ __pycache__/
/payloads/KDK.dmg
*.log
/Universal-Binaries.dmg
/payloads/Universal-Binaries_overlay
/payloads/KDKInfo.plist
/payloads/update.sh
/payloads/OpenCore-Patcher.app

View File

@@ -19,6 +19,26 @@
- Patch currently limited to Ventura and newer
- Restore Function Keys on MacBook5,2 and MacBook4,1
- Implementation by [@jazzzny](https://github.com/Jazzzny)
- Backend changes:
- Rename OCLP-Helper to OpenCore-Patcher
- Allows for better identification when displaying prompts
- Reimplement wxPython GUI into modularized system:
- Allows for easier maintenance and future expansion
- Changes include:
- Reworked settings UI
- Unified download UI with time remaining
- Implement in-app update system
- Guides users to update OpenCore and Root Patches once update's installed
- Expand app update checks to include nightly users
- ex. 0.6.6 nightly -> 0.6.6 release
- Implement macOS installer verification after flashing
- Implement proper UI call backs on long processes
- ex. Root patching
- Implement default selections for disks and installers
- Set about and quit items
- Utilize `py-applescript` for authorization prompts
- Avoids displaying prompts with `osascript` in the title
- Due to limitations, only used for installer creation and OpenCore installation
- Increment Binaries:
- PatcherSupportPkg 1.0.1 - release
- OpenCorePkg 0.9.2 - release

View File

@@ -49,6 +49,7 @@ app = BUNDLE(coll,
icon="payloads/OC-Patcher.icns",
bundle_identifier="com.dortania.opencore-legacy-patcher",
info_plist={
"CFBundleName": "OpenCore Legacy Patcher",
"CFBundleShortVersionString": constants.Constants().patcher_version,
"NSHumanReadableCopyright": constants.Constants().copyright_date,
"LSMinimumSystemVersion": "10.10.0",

View File

@@ -0,0 +1,28 @@
<?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>CFBundleDisplayName</key>
<string>OpenCore-Patcher</string>
<key>CFBundleExecutable</key>
<string>OpenCore-Patcher</string>
<key>CFBundleIconFile</key>
<string>OC-Patcher.icns</string>
<key>CFBundleIdentifier</key>
<string>com.dortania.opencore-legacy-patcher-helper</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>OpenCore-Patcher</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>10.10.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2020-2023 Dortania</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>

View File

@@ -3,4 +3,5 @@ pyobjc
wxpython
pyinstaller
packaging
py_sip_xnu
py_sip_xnu
py-applescript

View File

@@ -153,5 +153,3 @@ class BuildOpenCore:
logging.info(f"Your OpenCore EFI for {self.model} has been built at:")
logging.info(f" {self.constants.opencore_release_folder}")
logging.info("")
if self.constants.gui_mode is False:
input("Press [Enter] to continue\n")

View File

@@ -73,7 +73,7 @@ class BuildFirmware:
support.BuildSupport(self.model, self.constants, self.config).enable_kext("AppleIntelCPUPowerManagement.kext", self.constants.aicpupm_version, self.constants.aicpupm_path)
support.BuildSupport(self.model, self.constants, self.config).enable_kext("AppleIntelCPUPowerManagementClient.kext", self.constants.aicpupm_version, self.constants.aicpupm_client_path)
if smbios_data.smbios_dictionary[self.model]["CPU Generation"] <= cpu_data.cpu_data.sandy_bridge.value or self.constants.disable_xcpm is True:
if smbios_data.smbios_dictionary[self.model]["CPU Generation"] <= cpu_data.cpu_data.sandy_bridge.value or self.constants.disable_fw_throttle is True:
# With macOS 12.3 Beta 1, Apple dropped the 'plugin-type' check within X86PlatformPlugin
# Because of this, X86PP will match onto the CPU instead of ACPI_SMC_PlatformPlugin
# This causes power management to break on pre-Ivy Bridge CPUs as they don't have correct
@@ -81,11 +81,11 @@ class BuildFirmware:
# This patch will simply increase ASPP's 'IOProbeScore' to outmatch X86PP
logging.info("- Overriding ACPI SMC matching")
support.BuildSupport(self.model, self.constants, self.config).enable_kext("ASPP-Override.kext", self.constants.aspp_override_version, self.constants.aspp_override_path)
if self.constants.disable_xcpm is True:
if self.constants.disable_fw_throttle is True:
# Only inject on older OSes if user requests
support.BuildSupport(self.model, self.constants, self.config).get_item_by_kv(self.config["Kernel"]["Add"], "BundlePath", "ASPP-Override.kext")["MinKernel"] = ""
if self.constants.disable_msr_power_ctl is True and smbios_data.smbios_dictionary[self.model]["CPU Generation"] >= cpu_data.cpu_data.nehalem.value:
if self.constants.disable_fw_throttle is True and smbios_data.smbios_dictionary[self.model]["CPU Generation"] >= cpu_data.cpu_data.nehalem.value:
logging.info("- Disabling Firmware Throttling")
# Nehalem and newer systems force firmware throttling via MSR_POWER_CTL
support.BuildSupport(self.model, self.constants, self.config).enable_kext("SimpleMSR.kext", self.constants.simplemsr_version, self.constants.simplemsr_path)

View File

@@ -259,7 +259,7 @@ class BuildGraphicsAudio:
"CAIL,CAIL_DisableUVDPowerGating": 1,
"CAIL,CAIL_DisableVCEPowerGating": 1,
})
if self.constants.imac_model == "Legacy GCN":
if self.constants.imac_model == "GCN":
logging.info("- Adding Legacy GCN Power Gate Patches")
self.config["DeviceProperties"]["Add"][backlight_path].update({
"CAIL,CAIL_DisableDrmdmaPowerGating": 1,
@@ -274,7 +274,7 @@ class BuildGraphicsAudio:
"CAIL,CAIL_DisableUVDPowerGating": 1,
"CAIL,CAIL_DisableVCEPowerGating": 1,
})
elif self.constants.imac_model == "AMD Lexa":
elif self.constants.imac_model == "Lexa":
logging.info("- Adding Lexa Spoofing Patches")
self.config["DeviceProperties"]["Add"][backlight_path].update({
"model": "AMD Radeon Pro WX 3200",
@@ -285,7 +285,7 @@ class BuildGraphicsAudio:
"model": "AMD Radeon Pro WX 3200",
"device-id": binascii.unhexlify("FF67"),
})
elif self.constants.imac_model == "AMD Navi":
elif self.constants.imac_model == "Navi":
logging.info("- Adding Navi Spoofing Patches")
navi_backlight_path = backlight_path+"/Pci(0x0,0x0)/Pci(0x0,0x0)"
self.config["DeviceProperties"]["Add"][navi_backlight_path] = {

View File

@@ -15,6 +15,7 @@ class Constants:
self.patcher_version: str = "0.6.6" # OpenCore-Legacy-Patcher
self.patcher_support_pkg_version: str = "1.0.1" # PatcherSupportPkg
self.copyright_date: str = "Copyright © 2020-2023 Dortania"
self.patcher_name: str = "OpenCore Legacy Patcher"
# URLs
self.url_patcher_support_pkg: str = "https://github.com/dortania/PatcherSupportPkg/releases/download/"
@@ -132,6 +133,7 @@ class Constants:
self.launcher_script: str = None # Determine launch file path (None if PyInstaller)
self.booted_oc_disk: str = None # Determine current disk OCLP booted from
self.unpack_thread = None # Determine if unpack thread finished (threading.Thread)
self.update_stage: int = 0 # Determine update stage (see gui_support.py)
self.commit_info: tuple = (None, None, None) # Commit info (Branch, Commit Date, Commit URL)
@@ -206,12 +208,11 @@ class Constants:
self.dGPU_switch: bool = False # Set Display GPU Switching for Windows
self.force_surplus: bool = False # Force SurPlus patch in newer OSes
self.force_latest_psp: bool = False # Force latest PatcherSupportPkg
self.disable_msr_power_ctl: bool = False # Disable MSR Power Control (missing battery throttling)
self.disable_fw_throttle: bool = False # Disable MSR Power Control and XCPM
self.software_demux: bool = False # Enable Software Demux patch set
self.force_vmm: bool = False # Force VMM patch
self.disable_connectdrivers: bool = False # Disable ConnectDrivers (hibernation)
self.set_content_caching: bool = False # Set Content Caching
self.disable_xcpm: bool = False # Disable XCPM (X86PlatformPlugin.kext)
self.set_vmm_cpuid: bool = False # Set VMM bit inside CPUID
self.disable_cat_colorsync: bool = False # Disable the ColorSync patch to regain Display Profiles
self.set_alc_usage: bool = True # Set AppleALC usage
@@ -624,7 +625,7 @@ class Constants:
@property
def oclp_helper_path(self):
return self.payload_path / Path("Tools/OCLP-Helper")
return self.payload_path / Path("Tools/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher")
@property
def rsrrepair_userspace_path(self):

View File

@@ -1,104 +0,0 @@
import wx
import webbrowser
from resources import constants
from data import os_data
class gui_help_menu:
def __init__(self, versions, frame, frame_modal):
self.constants: constants.Constants = versions
self.frame = frame
self.frame_modal = frame_modal
# Define Window Size
self.WINDOW_WIDTH_MAIN = 300
def reset_frame_modal(self):
if not self.frame_modal:
self.frame_modal = wx.Dialog(self.frame)
else:
self.frame_modal.DestroyChildren()
self.frame_modal.Close()
if self.constants.detected_os >= os_data.os_data.big_sur:
self.frame_modal.ShowWithoutActivating()
def help_menu(self, event=None):
# Define Menu
# Header: Get help with OpenCore Legacy Patcher
# Subheader: Following resources are available:
# Button: Official Guide
# Button: Official Discord Server
self.reset_frame_modal()
self.frame_modal.SetSize((self.WINDOW_WIDTH_MAIN, -1))
# Header
self.header = wx.StaticText(self.frame_modal, label="Patcher Resources", pos=(10,10))
self.header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
self.header.Centre(wx.HORIZONTAL)
# Subheader
self.subheader = wx.StaticText(self.frame_modal, label="Following resources are available:")
self.subheader.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
self.subheader.SetPosition(
wx.Point(
self.header.GetPosition().x,
self.header.GetPosition().y + self.header.GetSize().height + 5
)
)
self.subheader.Centre(wx.HORIZONTAL)
# Official Guide
self.guide = wx.Button(self.frame_modal, label="Official Guide", size=(200,30))
self.guide.SetPosition(
wx.Point(
self.subheader.GetPosition().x,
self.subheader.GetPosition().y + self.subheader.GetSize().height + 5
)
)
self.guide.Bind(wx.EVT_BUTTON, lambda event: webbrowser.open(self.constants.guide_link))
self.guide.Centre(wx.HORIZONTAL)
# Official Discord Server
self.discord = wx.Button(self.frame_modal, label="Official Discord Server", size=(200,30))
self.discord.SetPosition(
wx.Point(
self.guide.GetPosition().x,
self.guide.GetPosition().y + self.guide.GetSize().height
)
)
self.discord.Bind(wx.EVT_BUTTON, lambda event: webbrowser.open(self.constants.discord_link))
self.discord.Centre(wx.HORIZONTAL)
# Overclock Button
self.overclock = wx.Button(self.frame_modal, label="Official Support Phone", size=(200,30))
self.overclock.SetPosition(
wx.Point(
self.discord.GetPosition().x,
self.discord.GetPosition().y + self.discord.GetSize().height
)
)
self.overclock.Bind(wx.EVT_BUTTON, lambda event: webbrowser.open("https://www.youtube.com/watch?v=dQw4w9WgXcQ"))
self.overclock.Centre(wx.HORIZONTAL)
self.return_to_main = wx.Button(self.frame_modal, label="Return to Main Menu", size=(150,30))
self.return_to_main.SetPosition(
wx.Point(
self.overclock.GetPosition().x,
self.overclock.GetPosition().y + self.overclock.GetSize().height + 5
)
)
self.return_to_main.Bind(wx.EVT_BUTTON, lambda event: self.frame_modal.Close())
self.return_to_main.Centre(wx.HORIZONTAL)
# Set Window Size to below Copyright Label
self.frame_modal.SetSize(
(
-1,
self.return_to_main.GetPosition().y + self.return_to_main.GetSize().height + 40
)
)
self.frame_modal.ShowWindowModal()

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
import wx
import time
class RedirectText(object):
def __init__(self,aWxTextCtrl, sleep):
self.out=aWxTextCtrl
self.sleep = sleep
def write(self,string):
self.out.WriteText(string)
wx.GetApp().Yield()
if self.sleep:
time.sleep(0.01)
def fileno(self):
return 1
def flush(self):
pass
class RedirectLabel(object):
def __init__(self,aWxTextCtrl):
self.out=aWxTextCtrl
def write(self,string):
if "MB/s" in string:
self.out.SetLabel(string)
self.out.Centre(wx.HORIZONTAL)
wx.GetApp().Yield()
time.sleep(0.01)
def fileno(self):
return 1
def flush(self):
pass
class RedirectLabelAll(object):
def __init__(self,aWxTextCtrl):
self.out=aWxTextCtrl
def write(self,string):
self.out.SetLabel(string)
self.out.Centre(wx.HORIZONTAL)
wx.GetApp().Yield()
time.sleep(0.01)

View File

@@ -2,15 +2,17 @@
# Usage solely for TUI
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
import logging
import plistlib
import subprocess
import shutil
import os
import logging
import applescript
from pathlib import Path
from resources import utilities, constants
from data import os_data
class tui_disk_installation:
def __init__(self, versions):
self.constants: constants.Constants = versions
@@ -75,49 +77,37 @@ class tui_disk_installation:
return supported_partitions
def install_opencore(self, full_disk_identifier):
def determine_sd_card(media_name):
# Array filled with common SD Card names
# Note most USB-based SD Card readers generally report as "Storage Device"
# Thus no reliable way to detect further without parsing IOService output (kUSBProductString)
if (
"SD Card" in media_name or \
"SD/MMC" in media_name or \
"SDXC Reader" in media_name or \
"SD Reader" in media_name or \
"Card Reader" in media_name
):
return True
return False
def _determine_sd_card(media_name: str):
# Array filled with common SD Card names
# Note most USB-based SD Card readers generally report as "Storage Device"
# Thus no reliable way to detect further without parsing IOService output (kUSBProductString)
if any(x in media_name for x in ("SD Card", "SD/MMC", "SDXC Reader", "SD Reader", "Card Reader")):
return True
return False
def install_opencore(self, full_disk_identifier: str):
# TODO: Apple Script fails in Yosemite(?) and older
args = [
"osascript",
"-e",
f'''do shell script "diskutil mount {full_disk_identifier}"'''
' with prompt "OpenCore Legacy Patcher needs administrator privileges to mount your EFI."'
" with administrator privileges"
" without altering line endings",
]
logging.info(f"- Mounting partition: {full_disk_identifier}")
if self.constants.detected_os >= os_data.os_data.el_capitan and not self.constants.recovery_status:
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
else:
result = subprocess.run(f"diskutil mount {full_disk_identifier}".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0:
if "execution error" in result.stderr.decode() and result.stderr.decode().strip()[-5:-1] == "-128":
# cancelled prompt
return
else:
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
try:
applescript.AppleScript(f'''do shell script "diskutil mount {full_disk_identifier}" with prompt "OpenCore Legacy Patcher needs administrator privileges to mount this volume." with administrator privileges without altering line endings''').run()
except applescript.ScriptError as e:
if "User canceled" in str(e):
logging.info("- Mount cancelled by user")
return
logging.info(f"An error occurred: {e}")
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
else:
result = subprocess.run(f"diskutil mount {full_disk_identifier}".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0:
logging.info("- Mount failed")
logging.info(result.stderr.decode())
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"]
drive_host_info = plistlib.loads(subprocess.run(f"diskutil info -plist {parent_disk}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
@@ -128,71 +118,57 @@ class tui_disk_installation:
ssd_type = False
mount_path = Path(partition_info["MountPoint"])
disk_type = partition_info["BusProtocol"]
utilities.cls()
utilities.header(["Copying OpenCore"])
if mount_path.exists():
if (mount_path / Path("EFI/Microsoft")).exists() and self.constants.gui_mode is False:
logging.info("- Found Windows Boot Loader")
logging.info("\nWould you like to continue installing OpenCore?")
logging.info("Installing OpenCore onto this drive may make Windows unbootable until OpenCore")
logging.info("is removed from the partition")
logging.info("We highly recommend users partition 200MB off their drive with Disk Utility")
logging.info(" Name:\t\t OPENCORE")
logging.info(" Format:\t\t FAT32")
logging.info(" Size:\t\t 200MB")
choice = input("\nWould you like to still install OpenCore to this drive?(y/n): ")
if not choice in ["y", "Y", "Yes", "yes"]:
subprocess.run(["diskutil", "umount", mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode()
return False
if (mount_path / Path("EFI/OC")).exists():
logging.info("- Removing preexisting EFI/OC folder")
shutil.rmtree(mount_path / Path("EFI/OC"), onerror=rmtree_handler)
if (mount_path / Path("System")).exists():
logging.info("- Removing preexisting System folder")
shutil.rmtree(mount_path / Path("System"), onerror=rmtree_handler)
if (mount_path / Path("boot.efi")).exists():
logging.info("- Removing preexisting boot.efi")
os.remove(mount_path / Path("boot.efi"))
logging.info("- Copying OpenCore onto EFI partition")
shutil.copytree(self.constants.opencore_release_folder / Path("EFI/OC"), mount_path / Path("EFI/OC"))
shutil.copytree(self.constants.opencore_release_folder / Path("System"), mount_path / Path("System"))
if Path(self.constants.opencore_release_folder / Path("boot.efi")).exists():
shutil.copy(self.constants.opencore_release_folder / Path("boot.efi"), mount_path / Path("boot.efi"))
if self.constants.boot_efi is True:
logging.info("- Converting Bootstrap to BOOTx64.efi")
if (mount_path / Path("EFI/BOOT")).exists():
shutil.rmtree(mount_path / Path("EFI/BOOT"), onerror=rmtree_handler)
Path(mount_path / Path("EFI/BOOT")).mkdir()
shutil.move(mount_path / Path("System/Library/CoreServices/boot.efi"), mount_path / Path("EFI/BOOT/BOOTx64.efi"))
shutil.rmtree(mount_path / Path("System"), onerror=rmtree_handler)
if determine_sd_card(sd_type) is True:
logging.info("- Adding SD Card icon")
shutil.copy(self.constants.icon_path_sd, mount_path)
elif ssd_type is True:
logging.info("- Adding SSD icon")
shutil.copy(self.constants.icon_path_ssd, mount_path)
elif disk_type == "USB":
logging.info("- Adding External USB Drive icon")
shutil.copy(self.constants.icon_path_external, mount_path)
else:
logging.info("- Adding Internal Drive icon")
shutil.copy(self.constants.icon_path_internal, mount_path)
logging.info("- Cleaning install location")
if not self.constants.recovery_status:
logging.info("- Unmounting EFI partition")
subprocess.run(["diskutil", "umount", mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode()
logging.info("- OpenCore transfer complete")
if self.constants.gui_mode is False:
logging.info("\nPress [Enter] to continue.\n")
input()
else:
if not mount_path.exists():
logging.info("EFI failed to mount!")
return False
return True
def rmtree_handler(func, path, exc_info):
if exc_info[0] == FileNotFoundError:
return
raise # pylint: disable=misplaced-bare-raise
if (mount_path / Path("EFI/OC")).exists():
logging.info("- Removing preexisting EFI/OC folder")
subprocess.run(["rm", "-rf", mount_path / Path("EFI/OC")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if (mount_path / Path("System")).exists():
logging.info("- Removing preexisting System folder")
subprocess.run(["rm", "-rf", mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if (mount_path / Path("boot.efi")).exists():
logging.info("- Removing preexisting boot.efi")
subprocess.run(["rm", mount_path / Path("boot.efi")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logging.info("- Copying OpenCore onto EFI partition")
subprocess.run(["mkdir", "-p", mount_path / Path("EFI")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(["cp", "-r", self.constants.opencore_release_folder / Path("EFI/OC"), mount_path / Path("EFI/OC")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(["cp", "-r", self.constants.opencore_release_folder / Path("System"), mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if Path(self.constants.opencore_release_folder / Path("boot.efi")).exists():
subprocess.run(["cp", self.constants.opencore_release_folder / Path("boot.efi"), mount_path / Path("boot.efi")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if self.constants.boot_efi is True:
logging.info("- Converting Bootstrap to BOOTx64.efi")
if (mount_path / Path("EFI/BOOT")).exists():
subprocess.run(["rm", "-rf", mount_path / Path("EFI/BOOT")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
Path(mount_path / Path("EFI/BOOT")).mkdir()
subprocess.run(["mv", mount_path / Path("System/Library/CoreServices/boot.efi"), mount_path / Path("EFI/BOOT/BOOTx64.efi")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(["rm", "-rf", mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if self._determine_sd_card(sd_type) is True:
logging.info("- Adding SD Card icon")
subprocess.run(["cp", self.constants.icon_path_sd, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
elif ssd_type is True:
logging.info("- Adding SSD icon")
subprocess.run(["cp", self.constants.icon_path_ssd, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
elif disk_type == "USB":
logging.info("- Adding External USB Drive icon")
subprocess.run(["cp", self.constants.icon_path_external, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
else:
logging.info("- Adding Internal Drive icon")
subprocess.run(["cp", self.constants.icon_path_internal, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logging.info("- Cleaning install location")
if not self.constants.recovery_status:
logging.info("- Unmounting EFI partition")
subprocess.run(["diskutil", "umount", mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode()
logging.info("- OpenCore transfer complete")
return True

View File

@@ -538,10 +538,12 @@ class KernelDebugKitObject:
logging.info(f"- {msg}")
self.error_msg = msg
return False
self._remove_unused_kdks()
self.success = True
logging.info("- Kernel Debug Kit checksum verified")
return True
class KernelDebugKitUtilities:

View File

@@ -6,6 +6,7 @@ import subprocess
import tempfile
import enum
import logging
import applescript
from data import os_data
from resources import network_handler, utilities
@@ -53,19 +54,16 @@ class InstallerCreation():
"""
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:
try:
applescript.AppleScript(
f'''do shell script "installer -pkg {Path(download_path)}/InstallAssistant.pkg -target /"'''
' with prompt "OpenCore Legacy Patcher needs administrator privileges to extract the installer."'
" with administrator privileges"
" without altering line endings",
).run()
except Exception as e:
logging.info("- Failed to install InstallAssistant")
logging.info(f" Error Code: {result.returncode}")
logging.info(f" Error Code: {e}")
return False
logging.info("- InstallAssistant installed")
@@ -400,6 +398,7 @@ class RemoteInstallerCatalog:
"integrity": integrity,
"Source": "Apple Inc.",
"Variant": catalog_url,
"OS": os_data.os_conversion.os_to_kernel(version)
}
})
@@ -585,6 +584,7 @@ class LocalInstallerCatalog:
"Build": app_sdk,
"Path": application,
"Minimum Host OS": min_required,
"OS": kernel
}
})

View File

@@ -6,7 +6,7 @@ import logging
import threading
from pathlib import Path
from resources.gui import gui_main
from resources.wx_gui import gui_entry
from resources import (
constants,
utilities,
@@ -38,7 +38,7 @@ class OpenCoreLegacyPatcher:
self._generate_base_data()
if utilities.check_cli_args() is None:
gui_main.wx_python_gui(self.constants).main_menu(None)
gui_entry.EntryPoint(self.constants).start()
def _generate_base_data(self) -> None:
@@ -99,7 +99,7 @@ class OpenCoreLegacyPatcher:
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"]
ignore_args = ["--auto_patch", "--gui_patch", "--gui_unpatch", "--update_installed"]
if not any(x in sys.argv for x in ignore_args):
self.constants.current_path = Path.cwd()
self.constants.cli_mode = True

View File

@@ -9,6 +9,7 @@ import threading
import logging
import enum
import hashlib
import atexit
from pathlib import Path
from resources import utilities
@@ -340,6 +341,7 @@ class DownloadObject:
response = NetworkUtilities().get(self.url, stream=True, timeout=10)
with open(self.filepath, 'wb') as file:
atexit.register(self.stop)
for i, chunk in enumerate(response.iter_content(1024 * 1024 * 4)):
if self.should_stop:
raise Exception("Download stopped")
@@ -380,7 +382,6 @@ class DownloadObject:
"""
if self.total_file_size == 0.0:
logging.error("- File size is 0, cannot calculate percent")
return -1
return self.downloaded_file_size / self.total_file_size * 100
@@ -405,9 +406,11 @@ class DownloadObject:
"""
if self.total_file_size == 0.0:
logging.error("- File size is 0, cannot calculate time remaining")
return -1
return (self.total_file_size - self.downloaded_file_size) / self.get_speed()
speed = self.get_speed()
if speed <= 0:
return -1
return (self.total_file_size - self.downloaded_file_size) / speed
def get_file_size(self) -> float:

View File

@@ -280,8 +280,6 @@ class PatchSysVolume:
if self.needs_kmutil_exemptions is True:
logging.info("Note: Apple will require you to open System Preferences -> Security to allow the new kernel extensions to be loaded")
self.constants.root_patcher_succeeded = True
if self.constants.gui_mode is False:
input("\nPress [ENTER] to continue")
def _rebuild_kernel_collection(self) -> bool:
@@ -365,8 +363,6 @@ class PatchSysVolume:
logging.info(result.stdout.decode())
logging.info("")
logging.info("\nPlease reboot the machine to avoid potential issues rerunning the patcher")
if self.constants.gui_mode is False:
input("Press [ENTER] to continue")
return False
if self.skip_root_kmutil_requirement is True:
@@ -378,8 +374,6 @@ class PatchSysVolume:
logging.info(result.stdout.decode())
logging.info("")
logging.info("\nPlease reboot the machine to avoid potential issues rerunning the patcher")
if self.constants.gui_mode is False:
input("Press [ENTER] to continue")
return False
for file in ["KextPolicy", "KextPolicy-shm", "KextPolicy-wal"]:
@@ -885,33 +879,18 @@ class PatchSysVolume:
self.patch_set_dictionary = sys_patch_generate.GenerateRootPatchSets(self.computer.real_model, self.constants, self.hardware_details).patchset
if self.patch_set_dictionary == {}:
change_menu = None
logging.info("- No Root Patches required for your machine!")
if self.constants.gui_mode is False:
input("\nPress [ENTER] to return to the main menu: ")
elif self.constants.gui_mode is False:
change_menu = input("Would you like to continue with Root Volume Patching?(y/n): ")
else:
change_menu = "y"
logging.info("- Continuing root patching")
if change_menu in ["y", "Y"]:
logging.info("- Verifying whether Root Patching possible")
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.constants.gui_mode is False:
input("\nPress [ENTER] to return to the main menu")
else:
logging.info("- Recommend rebooting the machine and trying to patch again")
if self.constants.gui_mode is False:
input("- Press [ENTER] to exit: ")
elif self.constants.gui_mode is False:
input("\nPress [ENTER] to return to the main menu: ")
return
logging.info("- Verifying whether Root Patching possible")
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()
else:
logging.info("- Recommend rebooting the machine and trying to patch again")
else:
logging.info("- Returning to main menu")
def start_unpatch(self) -> None:
"""
@@ -922,11 +901,5 @@ class PatchSysVolume:
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:
logging.info("- Recommend rebooting the machine and trying to patch again")
if self.constants.gui_mode is False:
input("- Press [ENTER] to exit: ")
elif self.constants.gui_mode is False:
input("\nPress [ENTER] to return to the main menu")

View File

@@ -1,14 +1,16 @@
# Copyright (C) 2022, Mykola Grymalyuk
import wx
import logging
import plistlib
import subprocess
import webbrowser
import logging
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
from resources.wx_gui import gui_entry
class AutomaticSysPatch:
@@ -42,6 +44,26 @@ class AutomaticSysPatch:
logging.info("- Auto Patch option is not supported on TUI, please use GUI")
return
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
if dict:
for key in dict:
version = dict[key]["Version"]
logging.info(f"- Found new version: {version}")
app = wx.App()
frame = wx.Frame(None, -1, "OpenCore Legacy Patcher")
dialog = wx.MessageDialog(
parent=frame,
message=f"Current Version: {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}\nNew version: {version}\nWould you like to update?",
caption="Update Available for OpenCore Legacy Patcher!",
style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
)
dialog.SetYesNoCancelLabels("Download and install", "Always Ignore", "Ignore Once")
response = dialog.ShowModal()
if response == wx.ID_YES:
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.UPDATE_APP)
return
if utilities.check_seal() is True:
logging.info("- Detected Snapshot seal intact, detecting patches")
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
@@ -58,69 +80,44 @@ class AutomaticSysPatch:
for patch in patches:
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.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:
args_string = f"'{self.constants.launcher_binary}' --gui_patch"
else:
args_string = f"{self.constants.launcher_binary} {self.constants.launcher_script} --gui_patch"
warning_str = ""
if network_handler.NetworkUtilities("https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest").verify_network_connection() is False:
warning_str = f"""\n\nWARNING: We're unable to verify whether there are any new releases of OpenCore Legacy Patcher on Github. Be aware that you may be using an outdated version for this OS. If you're unsure, verify on Github that OpenCore Legacy Patcher {self.constants.patcher_version} is the latest official release"""
args = [
"osascript",
"-e",
f"""display dialog "OpenCore Legacy Patcher has detected you're running without Root Patches, and would like to install them.\n\nmacOS wipes all root patches during OS installs and updates, so they need to be reinstalled.\n\nFollowing Patches have been detected for your system: \n{patch_string}\nWould you like to apply these patches?{warning_str}" """
f'with icon POSIX file "{self.constants.app_icon_path}"',
]
output = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
if output.returncode == 0:
args = [
"osascript",
"-e",
f'''do shell script "{args_string}"'''
f' with prompt "OpenCore Legacy Patcher would like to patch your root volume"'
" with administrator privileges"
" without altering line endings"
]
subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
return
logging.info("- No new binaries found on Github, proceeding with patching")
logging.info("- No new binaries found on Github, proceeding with patching")
if self.constants.launcher_script is None:
args_string = f"'{self.constants.launcher_binary}' --gui_patch"
else:
for key in dict:
version = dict[key]["Version"]
github_link = dict[key]["Github Link"]
logging.info(f"- Found new version: {version}")
args_string = f"{self.constants.launcher_binary} {self.constants.launcher_script} --gui_patch"
# launch osascript to ask user if they want to apply the update
# if yes, open the link in the default browser
# we never want to run the root patcher if there are updates available
warning_str = ""
if network_handler.NetworkUtilities("https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest").verify_network_connection() is False:
warning_str = f"""\n\nWARNING: We're unable to verify whether there are any new releases of OpenCore Legacy Patcher on Github. Be aware that you may be using an outdated version for this OS. If you're unsure, verify on Github that OpenCore Legacy Patcher {self.constants.patcher_version} is the latest official release"""
args = [
"osascript",
"-e",
f"""display dialog "OpenCore Legacy Patcher has detected you're running without Root Patches, and would like to install them.\n\nmacOS wipes all root patches during OS installs and updates, so they need to be reinstalled.\n\nFollowing Patches have been detected for your system: \n{patch_string}\nWould you like to apply these patches?{warning_str}" """
f'with icon POSIX file "{self.constants.app_icon_path}"',
]
output = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
if output.returncode == 0:
args = [
"osascript",
"-e",
f"""display dialog "OpenCore Legacy Patcher has detected you're running without Root Patches, and would like to install them.\n\nHowever we've detected a new version of OCLP on Github. Would you like to view this?\n\nCurrent Version: {self.constants.patcher_version}\nLatest Version: {version}\n\nNote: After downloading the latest OCLP version, open the app and run the 'Post Install Root Patcher' from the main menu." """
f'with icon POSIX file "{self.constants.app_icon_path}"',
f'''do shell script "{args_string}"'''
f' with prompt "OpenCore Legacy Patcher would like to patch your root volume"'
" with administrator privileges"
" without altering line endings"
]
output = subprocess.run(
subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
if output.returncode == 0:
webbrowser.open(github_link)
return
return
else:
logging.info("- No patches detected")
else:
@@ -170,7 +167,7 @@ class AutomaticSysPatch:
if output.returncode == 0:
logging.info("- Launching GUI's Build/Install menu")
self.constants.start_build_install = True
gui_main.wx_python_gui(self.constants).main_menu(None)
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.BUILD_OC)
return False
@@ -245,7 +242,7 @@ class AutomaticSysPatch:
if output.returncode == 0:
logging.info("- Launching GUI's Build/Install menu")
self.constants.start_build_install = True
gui_main.wx_python_gui(self.constants).main_menu(None)
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.BUILD_OC)
except KeyError:
logging.info("- Unable to determine if boot disk is removable, skipping prompt")

View File

@@ -2,7 +2,6 @@
# Check whether new updates are available for OpenCore Legacy Patcher binary
# Call check_binary_updates() to determine if any updates are available
# Returns dict with Link and Version of the latest binary update if available
import requests
import logging
from resources import network_handler, constants
@@ -35,6 +34,12 @@ class CheckBinaryUpdates:
if local_version is None:
local_version = self.binary_version_array
if local_version == remote_version:
if not self.constants.commit_info[0].startswith("refs/tags"):
# Check for nightly builds
return True
# Pad version numbers to match length (ie. 0.1.0 vs 0.1.0.1)
while len(remote_version) > len(local_version):
local_version.append(0)
@@ -99,6 +104,9 @@ class CheckBinaryUpdates:
response = network_handler.NetworkUtilities().get(REPO_LATEST_RELEASE_URL)
data_set = response.json()
if "tag_name" not in data_set:
return None
self.remote_version = data_set["tag_name"]
self.remote_version_array = self.remote_version.split(".")

View File

@@ -48,6 +48,44 @@ def human_fmt(num):
return "%.1f %s" % (num, "EB")
def seconds_to_readable_time(seconds) -> str:
"""
Convert seconds to a readable time format
Parameters:
seconds (int | float | str): Seconds to convert
Returns:
str: Readable time format
"""
seconds = int(seconds)
time = ""
if seconds == 0:
return "Almost done"
if seconds < 0:
return "Indeterminate"
years, seconds = divmod(seconds, 31536000)
days, seconds = divmod(seconds, 86400)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
if years > 0:
return "Over a year"
if days > 0:
if days > 31:
return "Over a month"
time += f"{days}d "
if hours > 0:
time += f"{hours}h "
if minutes > 0:
time += f"{minutes}m "
if seconds > 0:
time += f"{seconds}s"
return time
def header(lines):
lines = [i for i in lines if i is not None]
total_length = len(max(lines, key=len)) + 4
@@ -534,6 +572,7 @@ def check_cli_args():
parser.add_argument("--gui_patch", help="Starts GUI in Root Patcher", action="store_true", required=False)
parser.add_argument("--gui_unpatch", help="Starts GUI in Root Unpatcher", action="store_true", required=False)
parser.add_argument("--auto_patch", help="Check if patches are needed and prompt user", action="store_true", required=False)
parser.add_argument("--update_installed", help="Prompt user to finish updating via GUI", action="store_true", required=False)
args = parser.parse_args()
if not (args.build or args.patch_sys_vol or args.unpatch_sys_vol or args.validate or args.auto_patch):

View File

@@ -0,0 +1,63 @@
# About frame, just to sat
import wx
import wx.adv
from resources import constants
class AboutFrame(wx.Frame):
def __init__(self, global_constants: constants.Constants) -> None:
if wx.FindWindowByName("About"):
return
super(AboutFrame, self).__init__(None, title="About", size=(350, 350), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
self.constants: constants.Constants = global_constants
self.Centre()
self.hyperlink_colour = (25, 179, 231)
self._generate_elements(self)
self.Show()
def _generate_elements(self, frame: wx.Frame) -> None:
# Set title
title = wx.StaticText(frame, label="OpenCore Legacy Patcher", pos=(-1, 5))
title.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title.Centre(wx.HORIZONTAL)
# Set version
version = wx.StaticText(frame, label=f"Version: {self.constants.patcher_version}", pos=(-1, title.GetPosition()[1] + title.GetSize()[1] + 5))
version.SetFont(wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
version.Centre(wx.HORIZONTAL)
# Description
description = [
"Written by a small group of Mac hobbyists who just",
"want to keep old machines out of the landfill!",
]
spacer = 5
for line in description:
desc = wx.StaticText(frame, label=line, pos=(-1, version.GetPosition()[1] + version.GetSize()[1] + 5 + spacer))
desc.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
desc.Centre(wx.HORIZONTAL)
spacer += 20
# Set icon
icon_mac = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/com.apple.macbook-unibody-plastic.icns"
icon_mac = wx.StaticBitmap(frame, bitmap=wx.Bitmap(icon_mac, wx.BITMAP_TYPE_ICON), pos=(5, desc.GetPosition()[1] - 15))
icon_mac.SetSize((160, 160))
icon_mac.Centre(wx.HORIZONTAL)
icon_path = str(self.constants.app_icon_path)
icon = wx.StaticBitmap(frame, bitmap=wx.Bitmap(icon_path, wx.BITMAP_TYPE_ICON), pos=(5, desc.GetPosition()[1] + desc.GetSize()[1] + 17))
icon.SetSize((64, 64))
icon.Centre(wx.HORIZONTAL)
# Set frame size
frame.SetSize((-1, icon.GetPosition()[1] + icon.GetSize()[1] + 60))

View File

@@ -0,0 +1,158 @@
# Generate UI for Building OpenCore
import wx
import logging
import threading
import traceback
from resources import constants
from resources.build import build
from resources.wx_gui import (
gui_main_menu,
gui_install_oc,
gui_support
)
class BuildFrame(wx.Frame):
"""
Create a frame for building OpenCore
Uses a Modal Dialog for smoother transition from other frames
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None) -> None:
super(BuildFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
self.install_button: wx.Button = None
self.text_box: wx.TextCtrl = None
self.frame_modal: wx.Dialog = None
self.constants: constants.Constants = global_constants
self.title: str = title
self.stock_output = logging.getLogger().handlers[0].stream
self.frame_modal = wx.Dialog(self, title=title, size=(400, 200))
self._generate_elements(self.frame_modal)
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
self.constants.update_stage = gui_support.AutoUpdateStages.BUILDING
self.SetPosition(screen_location) if screen_location else self.Centre()
self.frame_modal.ShowWindowModal()
self._invoke_build()
def _generate_elements(self, frame: wx.Frame = None) -> None:
"""
Generate UI elements for build frame
Format:
- Title label: Build and Install OpenCore
- Text: Model: {Build or Host Model}
- Button: Install OpenCore
- Read-only text box: {empty}
- Button: Return to Main Menu
"""
frame = self if not frame else frame
title_label = wx.StaticText(frame, label="Build and Install OpenCore", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
model_label = wx.StaticText(frame, label=f"Model: {self.constants.custom_model or self.constants.computer.real_model}", pos=(-1,30))
model_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
model_label.Centre(wx.HORIZONTAL)
# Button: Install OpenCore
install_button = wx.Button(frame, label="🔩 Install OpenCore", pos=(-1, model_label.GetPosition()[1] + model_label.GetSize()[1]), size=(150, 30))
install_button.Bind(wx.EVT_BUTTON, self.on_install)
install_button.Centre(wx.HORIZONTAL)
install_button.Disable()
self.install_button = install_button
# Read-only text box: {empty}
text_box = wx.TextCtrl(frame, value="", pos=(-1, install_button.GetPosition()[1] + install_button.GetSize()[1] + 10), size=(400, 350), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2)
text_box.Centre(wx.HORIZONTAL)
self.text_box = text_box
# Button: Return to Main Menu
return_button = wx.Button(frame, label="Return to Main Menu", pos=(-1, text_box.GetPosition()[1] + text_box.GetSize()[1] + 5), size=(200, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
return_button.Centre(wx.HORIZONTAL)
return_button.Disable()
self.return_button = return_button
# Adjust window size to fit all elements
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
def _invoke_build(self) -> None:
"""
Invokes build function and waits for it to finish
"""
while gui_support.PayloadMount(self.constants, self).is_unpack_finished() is False:
wx.Yield()
thread = threading.Thread(target=self._build)
thread.start()
while thread.is_alive():
wx.Yield()
self.return_button.Enable()
dialog = wx.MessageDialog(
parent=self,
message=f"Would you like to install OpenCore now?",
caption="Finished building your OpenCore configuration!",
style=wx.YES_NO | wx.ICON_QUESTION
)
dialog.SetYesNoLabels("Install to disk", "View build log")
self.on_install() if dialog.ShowModal() == wx.ID_YES else self.install_button.Enable()
def _build(self) -> None:
"""
Calls build function and redirects stdout to the text box
"""
logger = logging.getLogger()
logger.addHandler(gui_support.ThreadHandler(self.text_box))
try:
build.BuildOpenCore(self.constants.custom_model or self.constants.computer.real_model, self.constants)
except:
logging.error("- An internal error occurred while building:\n")
logging.error(traceback.format_exc())
logger.removeHandler(logger.handlers[2])
def on_return_to_main_menu(self, event: wx.Event = None) -> None:
"""
Return to main menu
"""
self.frame_modal.Hide()
main_menu_frame = gui_main_menu.MainFrame(
None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition()
)
main_menu_frame.Show()
self.frame_modal.Destroy()
self.Destroy()
def on_install(self, event: wx.Event = None) -> None:
"""
Launch install frame
"""
self.frame_modal.Destroy()
self.Destroy()
install_oc_frame = gui_install_oc.InstallOCFrame(
None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition()
)
install_oc_frame.Show()

View File

@@ -0,0 +1,98 @@
# Generate UI for downloading files
import wx
from resources import (
constants,
network_handler,
utilities
)
class DownloadFrame(wx.Frame):
"""
Update provided frame with download stats
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, download_obj: network_handler.DownloadObject, item_name: str) -> None:
self.constants: constants.Constants = global_constants
self.title: str = title
self.parent: wx.Frame = parent
self.download_obj: network_handler.DownloadObject = download_obj
self.item_name: str = item_name
self.user_cancelled: bool = False
self.frame_modal = wx.Dialog(parent, title=title, size=(400, 200))
self._generate_elements(self.frame_modal)
def _generate_elements(self, frame: wx.Dialog = None) -> None:
"""
Generate elements for download frame
"""
frame = self if not frame else frame
title_label = wx.StaticText(frame, label=f"Downloading: {self.item_name}", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
label_amount = wx.StaticText(frame, label="0.00 B downloaded of 0.00B (0.00%)", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
label_amount.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
label_amount.Centre(wx.HORIZONTAL)
label_speed = wx.StaticText(frame, label="Average download speed: Unknown", pos=(-1, label_amount.GetPosition()[1] + label_amount.GetSize()[1] + 5))
label_speed.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
label_speed.Centre(wx.HORIZONTAL)
label_est_time = wx.StaticText(frame, label="Estimated time remaining: Unknown", pos=(-1, label_speed.GetPosition()[1] + label_speed.GetSize()[1] + 5))
label_est_time.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
label_est_time.Centre(wx.HORIZONTAL)
progress_bar = wx.Gauge(frame, range=100, pos=(-1, label_est_time.GetPosition()[1] + label_est_time.GetSize()[1] + 5), size=(300, 20))
progress_bar.Centre(wx.HORIZONTAL)
return_button = wx.Button(frame, label="Return", pos=(-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 5))
return_button.Bind(wx.EVT_BUTTON, lambda event: self.terminate_download())
return_button.Centre(wx.HORIZONTAL)
# Set size of frame
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
frame.ShowWindowModal()
self.download_obj.download()
while self.download_obj.is_active():
if self.download_obj.get_percent() == -1:
amount_str = f"{utilities.human_fmt(self.download_obj.downloaded_file_size)} downloaded"
else:
amount_str = f"{utilities.human_fmt(self.download_obj.downloaded_file_size)} downloaded of {utilities.human_fmt(self.download_obj.total_file_size)} ({self.download_obj.get_percent():.2f}%)"
label_amount.SetLabel(amount_str)
label_amount.Centre(wx.HORIZONTAL)
label_speed.SetLabel(
f"Average download speed: {utilities.human_fmt(self.download_obj.get_speed())}/s"
)
label_est_time.SetLabel(
f"Estimated time remaining: {utilities.seconds_to_readable_time(self.download_obj.get_time_remaining())}"
)
progress_bar.SetValue(int(self.download_obj.get_percent()))
wx.Yield()
if self.download_obj.download_complete is False and self.user_cancelled is False:
wx.MessageBox(f"Download failed: \n{self.download_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
frame.Destroy()
def terminate_download(self) -> None:
"""
Terminate download
"""
if wx.MessageBox("Are you sure you want to cancel the download?", "Cancel Download", wx.YES_NO | wx.ICON_QUESTION | wx.NO_DEFAULT) == wx.YES:
self.user_cancelled = True
self.download_obj.stop()

View File

@@ -0,0 +1,91 @@
# Entry point for the wxPython GUI
import wx
import sys
import atexit
import logging
from resources import constants
from resources.wx_gui import (
gui_main_menu,
gui_build,
gui_install_oc,
gui_sys_patch,
gui_support,
gui_update,
)
from resources.sys_patch import sys_patch_detect
class SupportedEntryPoints:
"""
Enum for supported entry points
"""
MAIN_MENU = gui_main_menu.MainFrame
BUILD_OC = gui_build.BuildFrame
INSTALL_OC = gui_install_oc.InstallOCFrame
SYS_PATCH = gui_sys_patch.SysPatchFrame
UPDATE_APP = gui_update.UpdateFrame
class EntryPoint:
def __init__(self, global_constants: constants.Constants) -> None:
self.app: wx.App = None
self.main_menu_frame: gui_main_menu.MainFrame = None
self.constants: constants.Constants = global_constants
self.constants.gui_mode = True
def _generate_base_data(self) -> None:
self.app = wx.App()
self.app.SetAppName(self.constants.patcher_name)
def start(self, entry: SupportedEntryPoints = gui_main_menu.MainFrame) -> None:
"""
Launches entry point for the wxPython GUI
"""
self._generate_base_data()
if "--gui_patch" in sys.argv or "--gui_unpatch" in sys.argv:
entry = gui_sys_patch.SysPatchFrame
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
logging.info(f"- Loading wxPython GUI: {entry.__name__}")
self.frame: wx.Frame = entry(
None,
title=f"{self.constants.patcher_name} ({self.constants.patcher_version})",
global_constants=self.constants,
screen_location=None,
**({"patches": patches} if "--gui_patch" in sys.argv or "--gui_unpatch" in sys.argv else {})
)
if self.frame:
gui_support.GenerateMenubar(self.frame, self.constants).generate()
atexit.register(self.OnCloseFrame)
if "--gui_patch" in sys.argv:
self.frame.start_root_patching(patches)
elif "--gui_unpatch" in sys.argv:
self.frame.revert_root_patching(patches)
self.app.MainLoop()
def OnCloseFrame(self, event: wx.Event = None) -> None:
"""
Closes the wxPython GUI
"""
if not self.frame:
return
logging.info("- Cleaning up wxPython GUI")
self.frame.SetTransparent(0)
wx.Yield()
self.frame.DestroyChildren()
self.frame.Destroy()
self.app.ExitMainLoop()

View File

@@ -0,0 +1,66 @@
# Generate UI for help menu
import wx
import webbrowser
from resources import constants
class HelpFrame(wx.Frame):
"""
Append to main menu through a modal dialog
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None) -> None:
self.dialog = wx.Dialog(parent, title=title, size=(300, 200))
self.constants: constants.Constants = global_constants
self.title: str = title
self._generate_elements(self.dialog)
self.dialog.ShowWindowModal()
def _generate_elements(self, frame: wx.Frame = None) -> None:
"""
Format:
- Title: Patcher Resources
- Text: Following resources are available:
- Button: Official Guide
- Button: Community Discord Server
- Button: Official Phone Support
- Button: Return to Main Menu
"""
frame = self if not frame else frame
title_label = wx.StaticText(frame, label="Patcher Resources", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
text_label = wx.StaticText(frame, label="Following resources are available:", pos=(-1,30))
text_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
text_label.Centre(wx.HORIZONTAL)
buttons = {
"Official Guide": self.constants.guide_link,
"Official Phone Support": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"Community Discord Server": self.constants.discord_link,
}
for button in buttons:
help_button = wx.Button(frame, label=button, pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + (list(buttons.keys()).index(button) * 30)), size=(200, 30))
help_button.Bind(wx.EVT_BUTTON, lambda event, temp=buttons[button]: webbrowser.open(temp))
help_button.Centre(wx.HORIZONTAL)
# Button: Return to Main Menu
return_button = wx.Button(frame, label="Return to Main Menu", pos=(-1, help_button.GetPosition()[1] + help_button.GetSize()[1]), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, lambda event: frame.Close())
return_button.Centre(wx.HORIZONTAL)
# Set size of frame
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))

View File

@@ -0,0 +1,347 @@
import wx
import threading
import logging
import traceback
from resources.wx_gui import gui_main_menu, gui_support, gui_sys_patch
from resources import constants, install
class InstallOCFrame(wx.Frame):
"""
Create a frame for installing OpenCore to disk
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
super(InstallOCFrame, self).__init__(parent, title=title, size=(300, 120), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
self.constants: constants.Constants = global_constants
self.title: str = title
self.result: bool = False
self.available_disks: dict = None
self.stock_output = logging.getLogger().handlers[0].stream
self.progress_bar_animation: gui_support.GaugePulseCallback = None
self.hyperlink_colour = (25, 179, 231)
self._generate_elements()
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
self.constants.update_stage = gui_support.AutoUpdateStages.INSTALLING
self.SetPosition(screen_location) if screen_location else self.Centre()
self.Show()
self._display_disks()
def _generate_elements(self) -> None:
"""
Display indeterminate progress bar while collecting disk information
Format:
- Title label: Install OpenCore
- Text: Fetching information on local disks...
- Progress bar: {indeterminate}
"""
# Title label: Install OpenCore
title_label = wx.StaticText(self, label="Install OpenCore", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Text: Parsing local disks...
text_label = wx.StaticText(self, label="Fetching information on local disks...", pos=(-1,30))
text_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
text_label.Centre(wx.HORIZONTAL)
self.text_label = text_label
# Progress bar: {indeterminate}
progress_bar = wx.Gauge(self, range=100, pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1]), size=(150, 30), style=wx.GA_HORIZONTAL | wx.GA_SMOOTH)
progress_bar.Centre(wx.HORIZONTAL)
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
progress_bar_animation.start_pulse()
self.progress_bar_animation = progress_bar_animation
self.progress_bar = progress_bar
def _fetch_disks(self) -> None:
"""
Fetch information on local disks
"""
self.available_disks = install.tui_disk_installation(self.constants).list_disks()
# Need to clean up output on pre-Sierra
# Disk images are mixed in with regular disks (ex. payloads.dmg)
ignore = ["disk image", "read-only", "virtual"]
for disk in self.available_disks.copy():
if any(string in self.available_disks[disk]['name'].lower() for string in ignore):
del self.available_disks[disk]
def _display_disks(self) -> None:
"""
Display disk selection dialog
"""
thread = threading.Thread(target=self._fetch_disks)
thread.start()
while thread.is_alive():
wx.Yield()
continue
self.progress_bar_animation.stop_pulse()
self.progress_bar.Hide()
# Create wxDialog for disk selection
dialog = wx.Dialog(self, title=self.title, size=(380, -1))
# Title label: Install OpenCore
title_label = wx.StaticText(dialog, label="Install OpenCore", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Text: select disk to install OpenCore onto
text_label = wx.StaticText(dialog, label="Select disk to install OpenCore onto:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
text_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
text_label.Centre(wx.HORIZONTAL)
# Add note: "Missing disks? Ensure they're FAT32 or formatted as GUID/GPT"
gpt_note = wx.StaticText(dialog, label="Missing disks? Ensure they're FAT32 or formatted as GUID/GPT", pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 5))
gpt_note.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
gpt_note.Centre(wx.HORIZONTAL)
# Add buttons for each disk
if self.available_disks:
# Only show booted disk if building for host
disk_root = self.constants.booted_oc_disk if self.constants.custom_model is None else None
if disk_root:
# disk6s1 -> disk6
disk_root = self.constants.booted_oc_disk.strip("disk")
disk_root = "disk" + disk_root.split("s")[0]
logging.info(f"- Checking if booted disk is present: {disk_root}")
# Add buttons for each disk
items = len(self.available_disks)
longest_label = max((len(self.available_disks[disk]['disk']) + len(self.available_disks[disk]['name']) + len(str(self.available_disks[disk]['size']))) for disk in self.available_disks)
longest_label = longest_label * 9
spacer = 0
for disk in self.available_disks:
# Create a button for each disk
logging.info(f"- {self.available_disks[disk]['disk']} - {self.available_disks[disk]['name']} - {self.available_disks[disk]['size']}")
disk_button = wx.Button(dialog, label=f"{self.available_disks[disk]['disk']} - {self.available_disks[disk]['name']} - {self.available_disks[disk]['size']}", size=(longest_label ,30), pos=(-1, gpt_note.GetPosition()[1] + gpt_note.GetSize()[1] + 5 + spacer))
disk_button.Centre(wx.HORIZONTAL)
disk_button.Bind(wx.EVT_BUTTON, lambda event, disk=disk: self._display_volumes(disk, self.available_disks))
if disk_root == self.available_disks[disk]['disk'] or items == 1:
disk_button.SetDefault()
spacer += 25
if disk_root:
# Add note: "Note: Blue represent the disk OpenCore is currently booted from"
disk_label = wx.StaticText(dialog, label="Note: Blue represent the disk OpenCore is currently booted from", pos=(-1, disk_button.GetPosition()[1] + disk_button.GetSize()[1] + 5))
disk_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
disk_label.Centre(wx.HORIZONTAL)
else:
disk_label = wx.StaticText(dialog, label="", pos=(-1, disk_button.GetPosition()[1] + 15))
disk_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
else:
# Text: Failed to find any applicable disks
disk_label = wx.StaticText(dialog, label="Failed to find any applicable disks", pos=(-1, gpt_note.GetPosition()[1] + gpt_note.GetSize()[1] + 5))
disk_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
disk_label.Centre(wx.HORIZONTAL)
# Add button: Search for disks again
search_button = wx.Button(dialog, label="Search for disks again", size=(160,30), pos=(-1, disk_label.GetPosition()[1] + disk_label.GetSize()[1] + 5))
search_button.Centre(wx.HORIZONTAL)
search_button.Bind(wx.EVT_BUTTON, self.on_reload_frame)
# Add button: Return to main menu
return_button = wx.Button(dialog, label="Return to main menu", size=(160,30), pos=(-1, search_button.GetPosition()[1] + 20))
return_button.Centre(wx.HORIZONTAL)
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
# Set size
dialog.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
dialog.ShowWindowModal()
self.dialog = dialog
def _display_volumes(self, disk: str, dataset: dict) -> None:
"""
List volumes on disk
"""
self.dialog.Close()
# Create dialog
dialog = wx.Dialog(
self,
title=f"Volumes on {disk}",
style=wx.CAPTION | wx.CLOSE_BOX,
size=(300, 300)
)
# Add text: "Volumes on {disk}"
text_label = wx.StaticText(dialog, label=f"Volumes on {disk}", pos=(-1, 10))
text_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
text_label.Centre(wx.HORIZONTAL)
partitions = install.tui_disk_installation(self.constants).list_partitions(disk, dataset)
items = len(partitions)
longest_label = max((len(partitions[partition]['partition']) + len(partitions[partition]['name']) + len(str(partitions[partition]['size']))) for partition in partitions)
longest_label = longest_label * 10
for partition in partitions:
logging.info(f"- {partitions[partition]['partition']} - {partitions[partition]['name']} - {partitions[partition]['size']}")
disk_button = wx.Button(dialog, label=f"{partitions[partition]['partition']} - {partitions[partition]['name']} - {partitions[partition]['size']}", size=(longest_label,30), pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 5))
disk_button.Centre(wx.HORIZONTAL)
disk_button.Bind(wx.EVT_BUTTON, lambda event, partition=partition: self._install_oc_process(partition))
if items == 1 or self.constants.booted_oc_disk == partitions[partition]['partition']:
disk_button.SetDefault()
# Add button: Return to main menu
return_button = wx.Button(dialog, label="Return to main menu", size=(150,30), pos=(-1, disk_button.GetPosition()[1] + disk_button.GetSize()[1]))
return_button.Centre(wx.HORIZONTAL)
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
# Set size
dialog.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
# Show dialog
dialog.ShowWindowModal()
self.dialog = dialog
def _install_oc_process(self, partition: dict) -> None:
"""
Install OpenCore to disk
"""
self.dialog.Close()
# Create dialog
dialog = wx.Dialog(
self,
title=f"Installing OpenCore to {partition}",
style=wx.CAPTION | wx.CLOSE_BOX,
size=(370, 200)
)
# Add text: "Installing OpenCore to {partition}"
text_label = wx.StaticText(dialog, label=f"Installing OpenCore to {partition}", pos=(-1, 10))
text_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
text_label.Centre(wx.HORIZONTAL)
# Read-only text box: {empty}
text_box = wx.TextCtrl(dialog, value="", pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 10), size=(370, 200), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2)
text_box.Centre(wx.HORIZONTAL)
self.text_box = text_box
# Add button: Return to main menu
return_button = wx.Button(dialog, label="Return to main menu", size=(200,30), pos=(-1, text_box.GetPosition()[1] + text_box.GetSize()[1] + 10))
return_button.Centre(wx.HORIZONTAL)
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
return_button.Disable()
# Set size
dialog.SetSize((370, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
# Show dialog
dialog.ShowWindowModal()
self.dialog = dialog
# Install OpenCore
self._invoke_install_oc(partition)
return_button.Enable()
def _invoke_install_oc(self, partition: dict) -> None:
"""
Invoke OpenCore installation
"""
thread = threading.Thread(target=self._install_oc, args=(partition,))
thread.start()
while thread.is_alive():
wx.Yield()
if self.result is True:
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
self.constants.update_stage = gui_support.AutoUpdateStages.ROOT_PATCHING
popup_message = wx.MessageDialog(
self,
f"OpenCore has finished installing to disk.\n\nWould you like to update your root patches next?", "Success",
wx.YES_NO | wx.YES_DEFAULT
)
popup_message.ShowModal()
if popup_message.GetReturnCode() == wx.ID_YES:
gui_sys_patch.SysPatchFrame(
parent=None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition()
)
self.Destroy()
return
elif not self.constants.custom_model:
gui_support.RestartHost(self).restart(message="OpenCore has finished installing to disk.\n\nYou will need to reboot and hold the Option key and select OpenCore/Boot EFI's option.\n\nWould you like to reboot?")
else:
popup_message = wx.MessageDialog(
self,
f"OpenCore has finished installing to disk.\n\nYou can eject the drive, insert it into the {self.constants.custom_model}, reboot, hold the Option key and select OpenCore/Boot EFI's option.", "Success",
wx.OK
)
popup_message.ShowModal()
else:
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
self.constants.update_stage = gui_support.AutoUpdateStages.FINISHED
def _install_oc(self, partition: dict) -> None:
"""
Install OpenCore to disk
"""
logging.info(f"- Installing OpenCore to {partition}")
logger = logging.getLogger()
logger.addHandler(gui_support.ThreadHandler(self.text_box))
try:
self.result = install.tui_disk_installation(self.constants).install_opencore(partition)
except:
logging.error("- An internal error occurred while installing:\n")
logging.error(traceback.format_exc())
logger.removeHandler(logger.handlers[2])
def on_reload_frame(self, event: wx.Event = None) -> None:
"""
Reload frame
"""
self.Destroy()
frame = InstallOCFrame(
None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition()
)
frame.Show()
def on_return_to_main_menu(self, event: wx.Event = None) -> None:
"""
Return to main menu
"""
main_menu_frame = gui_main_menu.MainFrame(
None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition()
)
main_menu_frame.Show()
self.Destroy()

View File

@@ -0,0 +1,379 @@
import wx
import logging
import threading
import webbrowser
from pathlib import Path
from resources.wx_gui import (
gui_main_menu,
gui_support,
gui_download,
gui_macos_installer_flash
)
from resources import (
constants,
macos_installer_handler,
utilities,
network_handler,
integrity_verification
)
from data import os_data, smbios_data, cpu_data
class macOSInstallerDownloadFrame(wx.Frame):
"""
Create a frame for downloading and creating macOS installers
Uses a Modal Dialog for smoother transition from other frames
Note: Flashing installers is passed to gui_macos_installer_flash.py
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
self.constants: constants.Constants = global_constants
self.title: str = title
self.parent: wx.Frame = parent
self.available_installers = None
self.available_installers_latest = None
self.frame_modal = wx.Dialog(parent, title=title, size=(330, 200))
self._generate_elements(self.frame_modal)
self.frame_modal.ShowWindowModal()
def _generate_elements(self, frame: wx.Frame = None) -> None:
"""
Format:
- Title: Create macOS Installer
- Button: Download macOS Installer
- Button: Use existing macOS Installer
- Button: Return to Main Menu
"""
frame = self if not frame else frame
title_label = wx.StaticText(frame, label="Create macOS Installer", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Button: Download macOS Installer
download_button = wx.Button(frame, label="Download macOS Installer", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5), size=(200, 30))
download_button.Bind(wx.EVT_BUTTON, self.on_download)
download_button.Centre(wx.HORIZONTAL)
# Button: Use existing macOS Installer
existing_button = wx.Button(frame, label="Use existing macOS Installer", pos=(-1, download_button.GetPosition()[1] + download_button.GetSize()[1] - 5), size=(200, 30))
existing_button.Bind(wx.EVT_BUTTON, self.on_existing)
existing_button.Centre(wx.HORIZONTAL)
# Button: Return to Main Menu
return_button = wx.Button(frame, label="Return to Main Menu", pos=(-1, existing_button.GetPosition()[1] + existing_button.GetSize()[1] + 5), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return)
return_button.Centre(wx.HORIZONTAL)
# Set size of frame
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
def _generate_catalog_frame(self) -> None:
"""
Generate frame to display available installers
"""
super(macOSInstallerDownloadFrame, self).__init__(None, title=self.title, size=(300, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
self.SetPosition((self.parent.GetPosition()[0], self.parent.GetPosition()[1]))
# Title: Pulling installer catalog
title_label = wx.StaticText(self, label="Pulling installer catalog", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Progress bar
progress_bar = wx.Gauge(self, range=100, pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5), size=(250, 30))
progress_bar.Centre(wx.HORIZONTAL)
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
progress_bar_animation.start_pulse()
# Set size of frame
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
self.Show()
# Grab installer catalog
def _fetch_installers():
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
thread = threading.Thread(target=_fetch_installers)
thread.start()
while thread.is_alive():
wx.Yield()
progress_bar_animation.stop_pulse()
progress_bar.Hide()
self._display_available_installers()
def _display_available_installers(self, event: wx.Event = None, show_full: bool = False) -> None:
"""
Display available installers in frame
"""
self.frame_modal.Destroy()
dialog = wx.Dialog(self, title="Select macOS Installer", size=(300, 200))
# Title: Select macOS Installer
title_label = wx.StaticText(dialog, label="Select macOS Installer", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Subtitle: Installers currently available from Apple:
subtitle_label = wx.StaticText(dialog, label="Installers currently available from Apple:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
subtitle_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
subtitle_label.Centre(wx.HORIZONTAL)
# List of installers
installers = self.available_installers_latest if show_full is False else self.available_installers
if installers:
spacer = 0
for app in installers:
logging.info(f"macOS {installers[app]['Version']} ({installers[app]['Build']}):\n - Size: {utilities.human_fmt(installers[app]['Size'])}\n - Source: {installers[app]['Source']}\n - Variant: {installers[app]['Variant']}\n - Link: {installers[app]['Link']}\n")
extra = " Beta" if installers[app]['Variant'] in ["DeveloperSeed" , "PublicSeed"] else ""
installer_button = wx.Button(dialog, label=f"macOS {installers[app]['Version']}{extra} ({installers[app]['Build']} - {utilities.human_fmt(installers[app]['Size'])})", pos=(-1, subtitle_label.GetPosition()[1] + subtitle_label.GetSize()[1] + 5 + spacer), size=(270, 30))
installer_button.Bind(wx.EVT_BUTTON, lambda event, temp=app: self.on_download_installer(installers[temp]))
installer_button.Centre(wx.HORIZONTAL)
spacer += 25
# Since installers are sorted by version, set the latest installer as the default button
# Note that on full display, the last installer is generally a beta
if show_full is False and app == list(installers.keys())[-1]:
installer_button.SetDefault()
# Show all available installers
show_all_button = wx.Button(dialog, label="Show all available installers" if show_full is False else "Show only latest installers", pos=(-1, installer_button.GetPosition()[1] + installer_button.GetSize()[1]), size=(200, 30))
show_all_button.Bind(wx.EVT_BUTTON, lambda event: self._display_available_installers(event, not show_full))
show_all_button.Centre(wx.HORIZONTAL)
# Return to Main Menu
return_button = wx.Button(dialog, label="Return to Main Menu", pos=(-1, show_all_button.GetPosition()[1] + show_all_button.GetSize()[1] - 7), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
return_button.Centre(wx.HORIZONTAL)
# Set size of frame
dialog.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
dialog.ShowWindowModal()
self.frame_modal = dialog
def on_download_installer(self, app: dict) -> None:
"""
Download macOS installer
"""
# Notify user whether their model is compatible with the selected installer
problems = []
model = self.constants.custom_model or self.constants.computer.real_model
if model in smbios_data.smbios_dictionary:
if app["OS"] >= os_data.os_data.ventura:
if smbios_data.smbios_dictionary[model]["CPU Generation"] <= cpu_data.cpu_data.penryn or model in ["MacPro4,1", "MacPro5,1", "Xserve3,1"]:
if model.startswith("MacBook"):
problems.append("Lack of internal Keyboard/Trackpad in macOS installer.")
else:
problems.append("Lack of internal Keyboard/Mouse in macOS installer.")
if problems:
problems = "\n".join(problems)
dlg = wx.MessageDialog(self.frame_modal, f"Your model ({model}) may not be fully supported by this installer. You may encounter the following issues:\n\n{problems}\n\nFor more information, see associated page.", "Potential Issues", wx.YES_NO | wx.CANCEL | wx.ICON_WARNING)
dlg.SetYesNoCancelLabels("View Github Issue", "Download Anyways", "Cancel")
result = dlg.ShowModal()
if result == wx.ID_CANCEL:
return
elif result == wx.ID_YES:
webbrowser.open("https://github.com/dortania/OpenCore-Legacy-Patcher/issues/1021")
return
host_space = utilities.get_free_space()
needed_space = app['Size'] * 2
if host_space < needed_space:
dlg = wx.MessageDialog(self.frame_modal, f"You do not have enough free space to download and extract this installer. Please free up some space and try again\n\n{utilities.human_fmt(host_space)} available vs {utilities.human_fmt(needed_space)} required", "Insufficient Space", wx.OK | wx.ICON_WARNING)
dlg.ShowModal()
return
self.frame_modal.Close()
download_obj = network_handler.DownloadObject(app['Link'], self.constants.payload_path / "InstallAssistant.pkg")
gui_download.DownloadFrame(
self,
title=self.title,
global_constants=self.constants,
download_obj=download_obj,
item_name=f"macOS {app['Version']} ({app['Build']})",
)
if download_obj.download_complete is False:
self.on_return_to_main_menu()
return
self._validate_installer(app['integrity'])
def _validate_installer(self, chunklist_link: str) -> None:
"""
Validate macOS installer
"""
self.SetSize((300, 200))
for child in self.GetChildren():
child.Destroy()
# Title: Validating macOS Installer
title_label = wx.StaticText(self, label="Validating macOS Installer", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Label: Validating chunk 0 of 0
chunk_label = wx.StaticText(self, label="Validating chunk 0 of 0", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
chunk_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
chunk_label.Centre(wx.HORIZONTAL)
# Progress bar
progress_bar = wx.Gauge(self, range=100, pos=(-1, chunk_label.GetPosition()[1] + chunk_label.GetSize()[1] + 5), size=(270, 30))
progress_bar.Centre(wx.HORIZONTAL)
# Set size of frame
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
self.Show()
chunklist_stream = network_handler.NetworkUtilities().get(chunklist_link).content
if chunklist_stream:
utilities.disable_sleep_while_running()
chunk_obj = integrity_verification.ChunklistVerification(self.constants.payload_path / Path("InstallAssistant.pkg"), chunklist_stream)
if chunk_obj.chunks:
progress_bar.SetValue(chunk_obj.current_chunk)
progress_bar.SetRange(chunk_obj.total_chunks)
wx.App.Get().Yield()
chunk_obj.validate()
while chunk_obj.status == integrity_verification.ChunklistStatus.IN_PROGRESS:
progress_bar.SetValue(chunk_obj.current_chunk)
chunk_label.SetLabel(f"Validating chunk {chunk_obj.current_chunk} of {chunk_obj.total_chunks}")
chunk_label.Centre(wx.HORIZONTAL)
wx.App.Get().Yield()
if chunk_obj.status == integrity_verification.ChunklistStatus.FAILURE:
wx.MessageBox("Chunklist validation failed.\n\nThis generally happens when downloading on unstable connections such as WiFi or cellular.\n\nPlease try redownloading again on a stable connection (ie. Ethernet)", "Corrupted Installer!", wx.OK | wx.ICON_ERROR)
self.on_return_to_main_menu()
return
# Extract installer
title_label.SetLabel("Extracting macOS Installer")
title_label.Centre(wx.HORIZONTAL)
chunk_label.SetLabel("May take a few minutes...")
chunk_label.Centre(wx.HORIZONTAL)
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
progress_bar_animation.start_pulse()
# Start thread to extract installer
self.result = False
def extract_installer():
self.result = macos_installer_handler.InstallerCreation().install_macOS_installer(self.constants.payload_path)
thread = threading.Thread(target=extract_installer)
thread.start()
# Show frame
self.Show()
# Wait for thread to finish
while thread.is_alive():
wx.Yield()
progress_bar_animation.stop_pulse()
progress_bar.Hide()
chunk_label.SetLabel("Successfully extracted macOS installer" if self.result is True else "Failed to extract macOS installer")
chunk_label.Centre(wx.HORIZONTAL)
# Create macOS Installer button
create_installer_button = wx.Button(self, label="Create macOS Installer", pos=(-1, progress_bar.GetPosition()[1]), size=(170, 30))
create_installer_button.Bind(wx.EVT_BUTTON, self.on_existing)
create_installer_button.Centre(wx.HORIZONTAL)
if self.result is False:
create_installer_button.Disable()
# Return to main menu button
return_button = wx.Button(self, label="Return to Main Menu", pos=(-1, create_installer_button.GetPosition()[1] + create_installer_button.GetSize()[1]), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
return_button.Centre(wx.HORIZONTAL)
# Set size of frame
self.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
# Show frame
self.Show()
if self.result is False:
wx.MessageBox("An error occurred while extracting the macOS installer. Could be due to a corrupted installer", "Error", wx.OK | wx.ICON_ERROR)
return
user_input = wx.MessageBox("Finished extracting the installer, would you like to continue and create a macOS installer?", "Create macOS Installer?", wx.YES_NO | wx.ICON_QUESTION)
if user_input == wx.YES:
self.on_existing()
def on_download(self, event: wx.Event) -> None:
"""
Display available macOS versions to download
"""
self.frame_modal.Close()
self.parent.Hide()
self._generate_catalog_frame()
self.parent.Close()
def on_existing(self, event: wx.Event = None) -> None:
"""
Display local macOS installers
"""
frames = [self, self.frame_modal, self.parent]
for frame in frames:
if frame:
frame.Close()
gui_macos_installer_flash.macOSInstallerFlashFrame(
None,
title=self.title,
global_constants=self.constants,
**({"screen_location": self.GetScreenPosition()} if self else {})
)
for frame in frames:
if frame:
frame.Destroy()
def on_return(self, event: wx.Event) -> None:
"""
Return to main menu (dismiss frame)
"""
self.frame_modal.Close()
def on_return_to_main_menu(self, event: wx.Event = None) -> None:
"""
Return to main menu
"""
if self.frame_modal:
self.frame_modal.Hide()
main_menu_frame = gui_main_menu.MainFrame(
None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition()
)
main_menu_frame.Show()
if self.frame_modal:
self.frame_modal.Destroy()
self.Destroy()

View File

@@ -0,0 +1,571 @@
import wx
import time
import logging
import plistlib
import tempfile
import threading
import subprocess
from pathlib import Path
from resources.wx_gui import gui_main_menu, gui_build, gui_support
from resources import (
constants,
macos_installer_handler,
utilities,
network_handler,
kdk_handler,
)
from data import os_data
class macOSInstallerFlashFrame(wx.Frame):
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
super(macOSInstallerFlashFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
self.constants: constants.Constants = global_constants
self.title: str = title
self.available_installers_local: dict = {}
self.available_disks: dict = {}
self.prepare_result: bool = False
self.progress_bar_animation: gui_support.GaugePulseCallback = None
self.frame_modal: wx.Dialog = None
self._generate_elements()
self.SetPosition(screen_location) if screen_location else self.Centre()
self.Show()
self._populate_installers()
def _generate_elements(self) -> None:
"""
Fetches local macOS Installers for users to select from
"""
# Title: Fetching local macOS Installers
title_label = wx.StaticText(self, label="Fetching local macOS Installers", pos=(-1,1))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Progress bar
progress_bar = wx.Gauge(self, range=100, pos=(-1, 30), size=(200, 30))
progress_bar.Centre(wx.HORIZONTAL)
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
progress_bar_animation.start_pulse()
self.progress_bar_animation = progress_bar_animation
# Set size of frame
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
def _populate_installers(self) -> None:
# Grab installer catalog
def fetch_installers():
self.available_installers_local = macos_installer_handler.LocalInstallerCatalog().available_apps
thread = threading.Thread(target=fetch_installers)
thread.start()
while thread.is_alive():
wx.Yield()
frame_modal = wx.Dialog(self, title=self.title, size=(350, 200))
frame_modal.Centre(wx.HORIZONTAL)
# Title: Select macOS Installer
title_label = wx.StaticText(frame_modal, label="Select local macOS Installer", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# List of installers
if self.available_installers_local:
logging.info("Installer(s) found:")
spacer = 10
entries = len(self.available_installers_local)
for app in self.available_installers_local:
logging.info(f"- {self.available_installers_local[app]['Short Name']}: {self.available_installers_local[app]['Version']} ({self.available_installers_local[app]['Build']})")
app_str = f"{self.available_installers_local[app]['Short Name']}"
unsupported: bool = self.available_installers_local[app]['Minimum Host OS'] > self.constants.detected_os
if unsupported:
min_str = os_data.os_conversion.convert_kernel_to_marketing_name(self.available_installers_local[app]['Minimum Host OS'])
app_str += f" (Requires {min_str})"
else:
app_str += f": {self.available_installers_local[app]['Version']} ({self.available_installers_local[app]['Build']})"
installer_button = wx.Button(frame_modal, label=app_str, pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + spacer), size=(300, 30))
installer_button.Bind(wx.EVT_BUTTON, lambda event, temp=app: self.on_select(self.available_installers_local[temp]))
installer_button.Centre(wx.HORIZONTAL)
spacer += 25
if unsupported:
installer_button.Disable()
elif entries == 1:
installer_button.SetDefault()
else:
installer_button = wx.StaticText(frame_modal, label="No installers found in '/Applications'", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
installer_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
installer_button.Centre(wx.HORIZONTAL)
# Button: Return to Main Menu
cancel_button = wx.Button(frame_modal, label="Return to Main Menu", pos=(-1, installer_button.GetPosition()[1] + installer_button.GetSize()[1]), size=(150, 30))
cancel_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
cancel_button.Centre(wx.HORIZONTAL)
# Set size of frame
frame_modal.SetSize((-1, cancel_button.GetPosition()[1] + cancel_button.GetSize()[1] + 40))
self.progress_bar_animation.stop_pulse()
frame_modal.ShowWindowModal()
self.frame_modal = frame_modal
def on_select(self, installer: dict) -> None:
self.frame_modal.Destroy()
for child in self.GetChildren():
child.Destroy()
# Fetching information on local disks
title_label = wx.StaticText(self, label="Fetching information on local disks", pos=(-1,1))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Progress bar
progress_bar = wx.Gauge(self, range=100, pos=(-1, 30), size=(200, 30))
progress_bar.Centre(wx.HORIZONTAL)
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
progress_bar_animation.start_pulse()
# Set size of frame
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
# Fetch local disks
def _fetch_disks():
self.available_disks = macos_installer_handler.InstallerCreation().list_disk_to_format()
# Need to clean up output on pre-Sierra
# Disk images are mixed in with regular disks (ex. payloads.dmg)
ignore = ["disk image", "read-only", "virtual"]
for disk in self.available_disks.copy():
if any(string in self.available_disks[disk]['name'].lower() for string in ignore):
del self.available_disks[disk]
thread = threading.Thread(target=_fetch_disks)
thread.start()
while thread.is_alive():
wx.Yield()
self.frame_modal = wx.Dialog(self, title=self.title, size=(350, 200))
# Title: Select local disk
title_label = wx.StaticText(self.frame_modal, label="Select local disk", pos=(-1,5))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Label: Selected USB will be erased, please backup any data
warning_label = wx.StaticText(self.frame_modal, label="Selected USB will be erased, please backup any data", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
warning_label.SetFont(wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
warning_label.Centre(wx.HORIZONTAL)
# List of disks
if self.available_disks:
spacer = 5
entries = len(self.available_disks)
for disk in self.available_disks:
logging.info(f"{disk}: {self.available_disks[disk]['name']} - {utilities.human_fmt(self.available_disks[disk]['size'])}")
disk_button = wx.Button(self.frame_modal, label=f"{disk}: {self.available_disks[disk]['name']} - {utilities.human_fmt(self.available_disks[disk]['size'])}", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + spacer), size=(300, 30))
disk_button.Bind(wx.EVT_BUTTON, lambda event, temp=disk: self.on_select_disk(self.available_disks[temp], installer))
disk_button.Centre(wx.HORIZONTAL)
if entries == 1:
disk_button.SetDefault()
spacer += 25
else:
disk_button = wx.StaticText(self.frame_modal, label="No disks found", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + 5))
disk_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
disk_button.Centre(wx.HORIZONTAL)
# Search for disks again
search_button = wx.Button(self.frame_modal, label="Search for disks again", pos=(-1, disk_button.GetPosition()[1] + disk_button.GetSize()[1]), size=(160, 30))
search_button.Bind(wx.EVT_BUTTON, lambda event, temp=installer: self.on_select(temp))
search_button.Centre(wx.HORIZONTAL)
# Button: Return to Main Menu
cancel_button = wx.Button(self.frame_modal, label="Return to Main Menu", pos=(-1, search_button.GetPosition()[1] + search_button.GetSize()[1] - 10), size=(160, 30))
cancel_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
cancel_button.Centre(wx.HORIZONTAL)
# Set size of frame
self.frame_modal.SetSize((-1, cancel_button.GetPosition()[1] + cancel_button.GetSize()[1] + 40))
progress_bar_animation.stop_pulse()
self.frame_modal.ShowWindowModal()
def on_select_disk(self, disk: dict, installer: dict) -> None:
answer = wx.MessageBox(f"Are you sure you want to erase '{disk['name']}'?\nAll data will be lost, this cannot be undone.", "Confirmation", wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
if answer != wx.YES:
return
self.frame_modal.Destroy()
for child in self.GetChildren():
child.Destroy()
self.SetSize((450, -1))
# Title: Creating Installer: {installer_name}
title_label = wx.StaticText(self, label=f"Creating Installer: {installer['Short Name']}", pos=(-1,1))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Label: Creating macOS installers can take 30min+ on slower USB drives.
warning_label = wx.StaticText(self, label="Creating macOS installers can take 30min+ on slower USB drives.", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
warning_label.SetFont(wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
warning_label.Centre(wx.HORIZONTAL)
# Label: We will notify you when the installer is ready.
warning_label = wx.StaticText(self, label="We will notify you when the installer is ready.", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + 5))
warning_label.SetFont(wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
warning_label.Centre(wx.HORIZONTAL)
# Label: Bytes Written: 0 MB
bytes_written_label = wx.StaticText(self, label="Bytes Written: 0000.0 MB", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + 5))
bytes_written_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
bytes_written_label.Centre(wx.HORIZONTAL)
# Progress bar
progress_bar = wx.Gauge(self, range=100, pos=(-1, bytes_written_label.GetPosition()[1] + bytes_written_label.GetSize()[1] + 5), size=(300, 30))
progress_bar.Centre(wx.HORIZONTAL)
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
progress_bar_animation.start_pulse()
# Set size of frame
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
self.Show()
# Prepare resources
if self._prepare_resources(installer['Path'], disk['identifier']) is False:
wx.MessageBox("Failed to prepare resources, cannot continue.", "Error", wx.OK | wx.ICON_ERROR)
self.on_return_to_main_menu()
return
# Base Size
estimated_size = 16000
# AutoPkg (700MB~)
estimated_size += 700 if installer['OS'] >= os_data.os_data.big_sur else 0
# KDK (700MB~, and overhead for copying to installer)
estimated_size += 700 * 2 if installer['OS'] >= os_data.os_data.ventura else 0
progress_bar_animation.stop_pulse()
progress_bar.SetRange(estimated_size)
root_disk = disk['identifier'][5:]
initial_bytes_written = float(utilities.monitor_disk_output(root_disk))
self.result = False
def _flash():
self.result = self._flash_installer(root_disk)
thread = threading.Thread(target=_flash)
thread.start()
# Wait for installer to be created
while thread.is_alive():
try:
total_bytes_written = float(utilities.monitor_disk_output(root_disk))
except:
pass
bytes_written = total_bytes_written - initial_bytes_written
wx.CallAfter(bytes_written_label.SetLabel, f"Bytes Written: {bytes_written:.2f} MB")
wx.CallAfter(progress_bar.SetValue, int(bytes_written))
wx.Yield()
if self.result is False:
self.on_return_to_main_menu()
return
# Next verify the installer
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
progress_bar_animation.start_pulse()
bytes_written_label.SetLabel("Validating Installer Integrity...")
error_message = self._validate_installer_pkg(disk['identifier'])
progress_bar_animation.stop_pulse()
if error_message != "":
progress_bar.SetValue(0)
wx.MessageBox(f"Failed to validate installer, cannot continue.\n This can generally happen due to a faulty USB drive, as flashing is an intensive process that can trigger hardware faults not normally seen. \n\n{error_message}", "Corrupted Installer!", wx.OK | wx.ICON_ERROR)
self.on_return_to_main_menu()
return
progress_bar.SetValue(estimated_size)
if gui_support.CheckProperties(self.constants).host_can_build() is False:
wx.MessageBox("Installer created successfully! If you want to install OpenCore to this USB, you will need to change the Target Model in settings", "Successfully created the macOS installer!", wx.OK | wx.ICON_INFORMATION)
self.on_return_to_main_menu()
return
answer = wx.MessageBox("Installer created successfully, would you like to continue and Install OpenCore to this disk?", "Successfully created the macOS installer!", wx.YES_NO | wx.ICON_QUESTION)
if answer != wx.YES:
self.on_return_to_main_menu()
return
# Install OpenCore
self.Hide()
gui_build.BuildFrame(
parent=None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition()
)
self.Destroy()
def _prepare_resources(self, installer_path: str, disk: str) -> None:
def prepare_script(self, installer_path: str, disk: str, constants: constants.Constants):
self.prepare_result = macos_installer_handler.InstallerCreation().generate_installer_creation_script(constants.payload_path, installer_path, disk)
thread = threading.Thread(target=prepare_script, args=(self, installer_path, disk, self.constants))
thread.start()
while thread.is_alive():
wx.Yield()
return self.prepare_result
def _flash_installer(self, disk) -> bool:
utilities.disable_sleep_while_running()
logging.info("- Creating macOS installer")
thread = threading.Thread(target=self._auto_package_handler)
thread.start()
# print contents of installer.sh
with open(self.constants.installer_sh_path, "r") as f:
logging.info(f"- installer.sh contents:\n{f.read()}")
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" not in output:
logging.info("- Failed to create macOS installer")
popup = wx.MessageDialog(self, f"Failed to create macOS installer\n\nOutput: {output}\n\nError: {error}", "Error", wx.OK | wx.ICON_ERROR)
popup.ShowModal()
return False
logging.info("- Successfully created macOS installer")
while thread.is_alive():
# wait for download_thread to finish
# though highly unlikely this thread is still alive (flashing an Installer will take a while)
time.sleep(0.1)
logging.info("- Installing Root Patcher to drive")
self._install_installer_pkg(disk)
utilities.enable_sleep_after_running()
return True
def _auto_package_handler(self):
"""
Function's main goal is to grab the correct AutoPkg-Assets.pkg and unzip it
Note the following:
- When running a release build, pull from Github's release page with the same versioning
- When running from source/unable to find on Github, use the nightly.link variant
- If nightly also fails, fall back to the manually uploaded variant
"""
link = self.constants.installer_pkg_url
if network_handler.NetworkUtilities(link).validate_link() is False:
logging.info("- Stock Install.pkg is missing on Github, falling back to Nightly")
link = self.constants.installer_pkg_url_nightly
if link.endswith(".zip"):
path = self.constants.installer_pkg_zip_path
else:
path = self.constants.installer_pkg_path
autopkg_download = network_handler.DownloadObject(link, path)
autopkg_download.download(spawn_thread=False)
if autopkg_download.download_complete is False:
logging.warning("- Failed to download Install.pkg")
logging.warning(autopkg_download.error_msg)
return
# Download thread will re-enable Idle Sleep after downloading
utilities.disable_sleep_while_running()
if not str(path).endswith(".zip"):
return
if Path(self.constants.installer_pkg_path).exists():
subprocess.run(["rm", self.constants.installer_pkg_path])
subprocess.run(["ditto", "-V", "-x", "-k", "--sequesterRsrc", "--rsrc", self.constants.installer_pkg_zip_path, self.constants.payload_path])
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 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 _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 _validate_installer_pkg(self, disk: str) -> bool:
verification_success = False
error_message = ""
def _integrity_check():
nonlocal error_message
path = utilities.grab_mount_point_from_disk(disk + "s2")
dmg_path = path + f"/{path.split('/')[2]}.app/Contents/SharedSupport/SharedSupport.dmg"
if not Path(dmg_path).exists():
logging.error(f"Failed to find {dmg_path}")
error_message = f"Failed to find {dmg_path}"
return error_message
result = subprocess.run(["hdiutil", "verify", dmg_path],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0:
if result.stdout:
logging.error(result.stdout.decode("utf-8"))
error_message = "STDOUT: " + result.stdout.decode("utf-8")
if result.stderr:
logging.error(result.stderr.decode("utf-8"))
error_message += "\n\nSTDERR: " + result.stderr.decode("utf-8")
thread = threading.Thread(target=_integrity_check)
thread.start()
while thread.is_alive():
wx.Yield()
if verification_success:
return error_message
logging.error(error_message)
return error_message
def on_return_to_main_menu(self, event: wx.Event = None):
if self.frame_modal:
self.frame_modal.Hide()
if self:
if isinstance(self, wx.Frame):
self.Hide()
main_menu_frame = gui_main_menu.MainFrame(
None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition()
)
main_menu_frame.Show()
if self.frame_modal:
self.frame_modal.Destroy()
if self:
if isinstance(self, wx.Frame):
self.Destroy()

View File

@@ -0,0 +1,241 @@
# Generate GUI for main menu
import wx
import sys
import logging
import threading
from resources.wx_gui import (
gui_build,
gui_macos_installer_download,
gui_sys_patch,
gui_support,
gui_help,
gui_settings,
gui_update,
)
from resources import (
constants,
global_settings,
updates
)
from data import os_data
class MainFrame(wx.Frame):
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
super(MainFrame, self).__init__(parent, title=title, size=(350, 300), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
self.constants: constants.Constants = global_constants
self.title: str = title
self.model_label: wx.StaticText = None
self.build_button: wx.Button = None
self.constants.update_stage = gui_support.AutoUpdateStages.INACTIVE
self._generate_elements()
self.SetPosition(screen_location) if screen_location else self.Centre()
self.Show()
self._preflight_checks()
def _generate_elements(self) -> None:
"""
Generate UI elements for the main menu
Format:
- Title label: OpenCore Legacy Patcher v{X.Y.Z}
- Text: Model: {Build or Host Model}
- Buttons:
- Build and Install OpenCore
- Post-Install Root Patch
- Create macOS Installer
- Settings
- Help
- Text: Copyright
"""
# Title label: OpenCore Legacy Patcher v{X.Y.Z}
title_label = wx.StaticText(self, label=f"OpenCore Legacy Patcher v{self.constants.patcher_version}", pos=(-1,1))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Text: Model: {Build or Host Model}
model_label = wx.StaticText(self, label=f"Model: {self.constants.custom_model or self.constants.computer.real_model}", pos=(-1,30))
model_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
model_label.Centre(wx.HORIZONTAL)
self.model_label = model_label
# Buttons:
menu_buttons = {
"Build and Install OpenCore": self.on_build_and_install,
"Post-Install Root Patch": self.on_post_install_root_patch,
"Create macOS Installer": self.on_create_macos_installer,
"Settings": self.on_settings,
"Help": self.on_help
}
button_y = model_label.GetPosition()[1] + 20
for button_name, button_function in menu_buttons.items():
button = wx.Button(self, label=button_name, pos=(-1, button_y), size=(200, 30))
button.Bind(wx.EVT_BUTTON, button_function)
button.Centre(wx.HORIZONTAL)
button_y += 30
if button_name == "Build and Install OpenCore":
self.build_button = button
if gui_support.CheckProperties(self.constants).host_can_build() is False:
button.Disable()
elif button_name == "Post-Install Root Patch":
if self.constants.detected_os < os_data.os_data.big_sur:
button.Disable()
# Text: Copyright
copy_label = wx.StaticText(self, label=self.constants.copyright_date, pos=(-1, button_y + 10))
copy_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
copy_label.Centre(wx.HORIZONTAL)
# Set window size
self.SetSize((350, copy_label.GetPosition()[1] + 50))
def _preflight_checks(self):
if (
self.constants.computer.build_model != None and
self.constants.computer.build_model != self.constants.computer.real_model and
self.constants.host_is_hackintosh is False
):
# Notify user they're booting an unsupported configuration
pop_up = wx.MessageDialog(
self,
f"We found you are currently booting OpenCore built for a different unit: {self.constants.computer.build_model}\n\nWe builds configs to match individual units and cannot be mixed or reused with different Macs.\n\nPlease Build and Install a new OpenCore config, and reboot your Mac.",
"Unsupported Configuration Detected!",
style=wx.OK | wx.ICON_EXCLAMATION
)
pop_up.ShowModal()
self.on_build_and_install()
return
if "--update_installed" in sys.argv and self.constants.has_checked_updates is False and gui_support.CheckProperties(self.constants).host_can_build():
# Notify user that the update has been installed
self.constants.has_checked_updates = True
pop_up = wx.MessageDialog(
self,
f"OpenCore Legacy Patcher has been updated to the latest version: {self.constants.patcher_version}\n\nWould you like to update OpenCore and your root volume patches?",
"Update successful!",
style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_INFORMATION
)
pop_up.ShowModal()
if pop_up.GetReturnCode() != wx.ID_YES:
print("- Skipping OpenCore and root volume patch update...")
return
print("- Updating OpenCore and root volume patches...")
self.constants.update_stage = gui_support.AutoUpdateStages.CHECKING
self.Hide()
pos = self.GetPosition()
gui_build.BuildFrame(
parent=None,
title=self.title,
global_constants=self.constants,
screen_location=pos
)
self.Close()
threading.Thread(target=self._check_for_updates).start()
def _check_for_updates(self):
if self.constants.has_checked_updates is True:
return
ignore_updates = global_settings.GlobalEnviromentSettings().read_property("IgnoreAppUpdates")
if ignore_updates is True:
self.constants.ignore_updates = True
return
self.constants.ignore_updates = False
self.constants.has_checked_updates = True
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
if not dict:
return
for entry in dict:
version = dict[entry]["Version"]
logging.info(f"New version: {version}")
dialog = wx.MessageDialog(
parent=self,
message=f"Current Version: {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}\nNew version: {version}\nWould you like to update?",
caption="Update Available for OpenCore Legacy Patcher!",
style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
)
dialog.SetYesNoCancelLabels("Download and install", "Always Ignore", "Ignore Once")
response = dialog.ShowModal()
if response == wx.ID_YES:
wx.CallAfter(self.on_update, dict[entry]["Link"], version)
elif response == wx.ID_NO:
logging.info("- Setting IgnoreAppUpdates to True")
self.constants.ignore_updates = True
global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", True)
def on_build_and_install(self, event: wx.Event = None):
self.Hide()
gui_build.BuildFrame(
parent=None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition()
)
self.Destroy()
def on_post_install_root_patch(self, event: wx.Event = None):
self.Hide()
gui_sys_patch.SysPatchFrame(
parent=None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition()
)
self.Destroy()
def on_create_macos_installer(self, event: wx.Event = None):
gui_macos_installer_download.macOSInstallerDownloadFrame(
parent=self,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition()
)
def on_settings(self, event: wx.Event = None):
gui_settings.SettingsFrame(
parent=self,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition()
)
def on_help(self, event: wx.Event = None):
gui_help.HelpFrame(
parent=self,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition()
)
def on_update(self, oclp_url: str, oclp_version: str):
gui_update.UpdateFrame(
parent=self,
title=self.title,
global_constants=self.constants,
screen_location=self.GetPosition(),
url=oclp_url,
version_label=oclp_version
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,296 @@
import wx
import sys
import time
import logging
import threading
import subprocess
import applescript
from pathlib import Path
from resources.wx_gui import gui_about
from resources import constants
from data import model_array, os_data, smbios_data
class AutoUpdateStages:
INACTIVE = 0
CHECKING = 1
BUILDING = 2
INSTALLING = 3
ROOT_PATCHING = 4
FINISHED = 5
class GenerateMenubar:
def __init__(self, frame: wx.Frame, global_constants: constants.Constants) -> None:
self.frame: wx.Frame = frame
self.constants: constants.Constants = global_constants
def generate(self) -> wx.MenuBar:
menubar = wx.MenuBar()
fileMenu = wx.Menu()
aboutItem = fileMenu.Append(wx.ID_ABOUT, "&About OpenCore Legacy Patcher")
fileMenu.AppendSeparator()
menubar.Append(fileMenu, "&File")
self.frame.SetMenuBar(menubar)
self.frame.Bind(wx.EVT_MENU, lambda event: gui_about.AboutFrame(self.constants), aboutItem)
class GaugePulseCallback:
"""
Uses an alternative Pulse() method for wx.Gauge() on macOS Monterey+
Dirty hack, however better to display some form of animation than none at all
"""
def __init__(self, global_constants: constants.Constants, gauge: wx.Gauge) -> None:
self.gauge: wx.Gauge = gauge
self.pulse_thread: threading.Thread = None
self.pulse_thread_active: bool = False
self.gauge_value: int = 0
self.pulse_forward: bool = True
self.max_value: int = gauge.GetRange()
self.non_metal_alternative: bool = CheckProperties(global_constants).host_is_non_metal()
def start_pulse(self) -> None:
if self.non_metal_alternative is False:
self.gauge.Pulse()
return
self.pulse_thread_active = True
self.pulse_thread = threading.Thread(target=self._pulse)
self.pulse_thread.start()
def stop_pulse(self) -> None:
if self.non_metal_alternative is False:
return
self.pulse_thread_active = False
self.pulse_thread.join()
def _pulse(self) -> None:
while self.pulse_thread_active:
if self.gauge_value == 0:
self.pulse_forward = True
elif self.gauge_value == self.max_value:
self.pulse_forward = False
if self.pulse_forward:
self.gauge_value += 1
else:
self.gauge_value -= 1
wx.CallAfter(self.gauge.SetValue, self.gauge_value)
time.sleep(0.005)
class CheckProperties:
def __init__(self, global_constants: constants.Constants) -> None:
self.constants: constants.Constants = global_constants
def host_can_build(self):
"""
Check if host supports building OpenCore configs
"""
if self.constants.custom_model:
return True
if self.constants.host_is_hackintosh is True:
return False
if self.constants.allow_oc_everywhere is True:
return True
if self.constants.computer.real_model in model_array.SupportedSMBIOS:
return True
return False
def host_is_non_metal(self, general_check: bool = False):
"""
Check if host is non-metal
Primarily for wx.Gauge().Pulse() workaround (where animation doesn't work on Monterey+)
"""
if self.constants.detected_os < os_data.os_data.monterey and general_check is False:
return False
if self.constants.detected_os < os_data.os_data.big_sur and general_check is True:
return False
if not Path("/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLightOld.dylib").exists():
# SkyLight stubs are only used on non-Metal
return False
return True
def host_has_cpu_gen(self, gen: int) -> bool:
"""
Check if host has a CPU generation equal to or greater than the specified generation
"""
model = self.constants.custom_model if self.constants.custom_model else self.constants.computer.real_model
if model in smbios_data.smbios_dictionary:
if smbios_data.smbios_dictionary[model]["CPU Generation"] >= gen:
return True
return False
class PayloadMount:
def __init__(self, global_constants: constants.Constants, frame: wx.Frame) -> None:
self.constants: constants.Constants = global_constants
self.frame: wx.Frame = frame
def is_unpack_finished(self):
if self.constants.unpack_thread.is_alive():
return False
if Path(self.constants.payload_kexts_path).exists():
return True
# Raise error to end program
popup = wx.MessageDialog(
self.frame,
f"During unpacking of our internal files, we seemed to have encountered an error.\n\nIf you keep seeing this error, please try rebooting and redownloading the application.",
"Internal Error occurred!",
style=wx.OK | wx.ICON_EXCLAMATION
)
popup.ShowModal()
self.frame.Freeze()
sys.exit(1)
class ThreadHandler(logging.Handler):
"""
Reroutes logging output to a wx.TextCtrl using UI callbacks
"""
def __init__(self, text_box: wx.TextCtrl):
logging.Handler.__init__(self)
self.text_box = text_box
def emit(self, record: logging.LogRecord):
wx.CallAfter(self.text_box.AppendText, self.format(record) + '\n')
class RestartHost:
"""
Restarts the host machine
"""
def __init__(self, frame: wx.Frame) -> None:
self.frame: wx.Frame = frame
def restart(self, event: wx.Event = None, message: str = ""):
self.popup = wx.MessageDialog(
self.frame,
message,
"Reboot to apply?",
wx.YES_NO | wx.ICON_INFORMATION
)
self.popup.SetYesNoLabels("Reboot", "Ignore")
answer = self.popup.ShowModal()
if answer == wx.ID_YES:
# Reboots with Count Down prompt (user can still dismiss if needed)
self.frame.Hide()
wx.Yield()
try:
applescript.AppleScript('tell app "loginwindow" to «event aevtrrst»').run()
except applescript.ScriptError as e:
logging.error(f"Error while trying to reboot: {e}")
sys.exit(0)
class RelaunchApplicationAsRoot:
"""
Relaunches the application as root
"""
def __init__(self, frame: wx.Frame, global_constants: constants.Constants) -> None:
self.constants = global_constants
self.frame: wx.Frame = frame
def relaunch(self, event: wx.Event):
self.dialog = wx.MessageDialog(
self.frame,
"OpenCore Legacy Patcher needs to relaunch as admin to continue. You will be prompted to enter your password.",
"Relaunch as root?",
wx.YES_NO | wx.ICON_QUESTION
)
# Show Dialog Box
if self.dialog.ShowModal() != wx.ID_YES:
logging.info("User cancelled relaunch")
return
timer: int = 5
program_arguments: str = ""
if event:
if event.GetEventObject() != wx.Menu:
try:
if event.GetEventObject().GetLabel() in ["Start Root Patching", "Reinstall Root Patches"]:
program_arguments = " --gui_patch"
elif event.GetEventObject().GetLabel() == "Revert Root Patches":
program_arguments = " --gui_unpatch"
except TypeError:
pass
if self.constants.launcher_script is None:
program_arguments = f"'{self.constants.launcher_binary}'{program_arguments}"
else:
program_arguments = f"{self.constants.launcher_binary} {self.constants.launcher_script}{program_arguments}"
# Relaunch as root
args = [
"osascript",
"-e",
f'''do shell script "{program_arguments}"'''
' with prompt "OpenCore Legacy Patcher needs administrator privileges to relaunch as admin."'
" with administrator privileges"
" without altering line endings",
]
self.frame.DestroyChildren()
self.frame.SetSize(400, 300)
# Header
header = wx.StaticText(self.frame, label="Relaunching as root")
header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
header.Centre(wx.HORIZONTAL)
# Add count down label
countdown_label = wx.StaticText(self.frame, label=f"Closing old process in {timer} seconds", pos=(0, header.GetPosition().y + header.GetSize().height + 3))
countdown_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
countdown_label.Centre(wx.HORIZONTAL)
# Set size of frame
self.frame.SetSize((-1, countdown_label.GetPosition().y + countdown_label.GetSize().height + 40))
wx.Yield()
logging.info(f"- Relaunching as root with command: {program_arguments}")
subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
wx.Yield()
countdown_label.SetLabel(f"Closing old process in {timer} seconds")
time.sleep(1)
timer -= 1
if timer == 0:
break
sys.exit(0)

View File

@@ -0,0 +1,508 @@
import wx
import os
import sys
import time
import logging
import plistlib
import traceback
import threading
import subprocess
from pathlib import Path
from resources import (
constants,
kdk_handler,
)
from resources.sys_patch import (
sys_patch,
sys_patch_detect
)
from resources.wx_gui import (
gui_main_menu,
gui_support,
gui_download,
)
from data import os_data
class SysPatchFrame(wx.Frame):
"""
Create a frame for root patching
Uses a Modal Dialog for smoother transition from other frames
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None, patches: dict = {}):
super(SysPatchFrame, self).__init__(parent, title=title, size=(350, 260), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
self.title = title
self.constants: constants.Constants = global_constants
self.frame_modal: wx.Dialog = None
self.return_button: wx.Button = None
self.available_patches: bool = False
self.frame_modal = wx.Dialog(self, title=title, size=(360, 200))
self.SetPosition(screen_location) if screen_location else self.Centre()
if patches:
return
self._generate_elements_display_patches(self.frame_modal, patches)
self.frame_modal.ShowWindowModal()
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
if self.available_patches is False:
gui_support.RestartHost(self).restart(message="No root patch updates needed!\n\nWould you like to reboot to apply the new OpenCore build?")
def _kdk_download(self, frame: wx.Frame = None) -> bool:
frame = self if not frame else frame
logging.info("KDK missing, generating KDK download frame")
header = wx.StaticText(frame, label="Downloading Kernel Debug Kit", pos=(-1,5))
header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
header.Centre(wx.HORIZONTAL)
subheader = wx.StaticText(frame, label="Fetching KDK database...", pos=(-1, header.GetPosition()[1] + header.GetSize()[1] + 5))
subheader.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
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()
# Generate KDK object
self.kdk_obj: kdk_handler.KernelDebugKitObject = 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():
wx.Yield()
if self.kdk_obj.success is False:
progress_bar_animation.stop_pulse()
progress_bar.SetValue(0)
wx.MessageBox(f"KDK download failed: {self.kdk_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
return False
kdk_download_obj = self.kdk_obj.retrieve_download()
if not kdk_download_obj:
# KDK is already downloaded
return True
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:
return False
header.SetLabel(f"Validating KDK: {self.kdk_obj.kdk_url_build}")
header.Centre(wx.HORIZONTAL)
subheader.SetLabel("Checking if checksum is valid...")
subheader.Centre(wx.HORIZONTAL)
wx.Yield()
progress_bar_animation.stop_pulse()
if self.kdk_obj.validate_kdk_checksum() is False:
progress_bar.SetValue(0)
logging.error("KDK checksum validation failed")
logging.error(self.kdk_obj.error_msg)
msg = wx.MessageDialog(frame, f"KDK checksum validation failed: {self.kdk_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
msg.ShowModal()
return False
progress_bar.SetValue(100)
logging.info("KDK download complete")
return True
def _generate_elements_display_patches(self, frame: wx.Frame = None, patches: dict = {}) -> None:
"""
Generate UI elements for root patching frame
Format:
- Title label: Post-Install Menu
- Label: Available patches:
- Labels: {patch name}
- Button: Start Root Patching
- Button: Revert Root Patches
- Button: Return to Main Menu
"""
frame = self if not frame else frame
title_label = wx.StaticText(frame, label="Post-Install Menu", pos=(-1, 10))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Label: Available patches:
available_label = wx.StaticText(frame, label="Available patches for your system:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 10))
available_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
available_label.Centre(wx.HORIZONTAL)
# Labels: {patch name}
patches: dict = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set() if not patches else patches
can_unpatch: bool = 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):
logging.info("- No applicable patches available")
patches = []
# Check if OCLP has already applied the same patches
no_new_patches = not self._check_if_new_patches_needed(patches) if patches else False
if not patches:
# Prompt user with no patches found
patch_label = wx.StaticText(frame, label="No patches required", pos=(-1, available_label.GetPosition()[1] + 20))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
patch_label.Centre(wx.HORIZONTAL)
else:
# Add Label for each patch
i = 0
if no_new_patches is True:
patch_label = wx.StaticText(frame, label="All applicable patches already installed", pos=(-1, available_label.GetPosition()[1] + 20))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
patch_label.Centre(wx.HORIZONTAL)
i = i + 20
else:
longest_patch = ""
for patch in patches:
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
if len(patch) > len(longest_patch):
longest_patch = patch
anchor = wx.StaticText(frame, label=longest_patch, pos=(-1, available_label.GetPosition()[1] + 20))
anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
anchor.Centre(wx.HORIZONTAL)
anchor.Hide()
for patch in patches:
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
i = i + 20
logging.info(f"- Adding patch: {patch} - {patches[patch]}")
patch_label = wx.StaticText(frame, label=f"- {patch}", pos=(anchor.GetPosition()[0], available_label.GetPosition()[1] + i))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
if i == 20:
patch_label.SetLabel(patch_label.GetLabel().replace("-", ""))
patch_label.Centre(wx.HORIZONTAL)
if patches["Validation: Patching Possible"] is False:
# Cannot patch due to the following reasons:
patch_label = wx.StaticText(frame, label="Cannot patch due to the following reasons:", pos=(-1, patch_label.GetPosition().y + 25))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
patch_label.Centre(wx.HORIZONTAL)
for patch in patches:
if not patch.startswith("Validation"):
continue
if patches[patch] is False:
continue
if patch == "Validation: Unpatching Possible":
continue
patch_label = wx.StaticText(frame, label=f"- {patch.split('Validation: ')[1]}", pos=(available_label.GetPosition().x - 10, patch_label.GetPosition().y + 20))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
else:
if self.constants.computer.oclp_sys_version and self.constants.computer.oclp_sys_date:
date = self.constants.computer.oclp_sys_date.split(" @")
date = date[0] if len(date) == 2 else ""
patch_text = f"{self.constants.computer.oclp_sys_version}, {date}"
patch_label = wx.StaticText(frame, label="Root Volume last patched:", pos=(-1, patch_label.GetPosition().y + 25))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
patch_label.Centre(wx.HORIZONTAL)
patch_label = wx.StaticText(frame, label=patch_text, pos=(available_label.GetPosition().x - 10, patch_label.GetPosition().y + 20))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
patch_label.Centre(wx.HORIZONTAL)
# Button: Start Root Patching
start_button = wx.Button(frame, label="Start Root Patching", pos=(10, patch_label.GetPosition().y + 25), size=(170, 30))
start_button.Bind(wx.EVT_BUTTON, lambda event: self.start_root_patching(frame, patches, no_new_patches))
start_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
start_button.Centre(wx.HORIZONTAL)
# Button: Revert Root Patches
revert_button = wx.Button(frame, label="Revert Root Patches", pos=(10, start_button.GetPosition().y + start_button.GetSize().height - 5), size=(170, 30))
revert_button.Bind(wx.EVT_BUTTON, lambda event: self.revert_root_patching(frame, patches, can_unpatch))
revert_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
revert_button.Centre(wx.HORIZONTAL)
# Button: Return to Main Menu
return_button = wx.Button(frame, label="Return to Main Menu", pos=(10, revert_button.GetPosition().y + revert_button.GetSize().height), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
return_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
return_button.Centre(wx.HORIZONTAL)
self.return_button = return_button
# Disable buttons if unsupported
if not patches:
start_button.Disable()
else:
self.available_patches = True
if patches["Validation: Patching Possible"] is False:
start_button.Disable()
elif no_new_patches is False:
start_button.SetDefault()
if can_unpatch is False:
revert_button.Disable()
# Relaunch as root if not root
uid = os.geteuid()
if uid != 0:
start_button.Bind(wx.EVT_BUTTON, gui_support.RelaunchApplicationAsRoot(frame, self.constants).relaunch)
revert_button.Bind(wx.EVT_BUTTON, gui_support.RelaunchApplicationAsRoot(frame, self.constants).relaunch)
# Set frame size
frame.SetSize((-1, return_button.GetPosition().y + return_button.GetSize().height + 35))
def _generate_modal(self, patches: dict = {}, variant: str = "Root Patching"):
"""
Create UI for root patching/unpatching
"""
supported_variants = ["Root Patching", "Revert Root Patches"]
if variant not in supported_variants:
logging.error(f"Unsupported variant: {variant}")
return
self.frame_modal.Close()
dialog = wx.Dialog(self, title=self.title, size=(400, 200))
# Title
title = wx.StaticText(dialog, label=variant, pos=(-1, 10))
title.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title.Centre(wx.HORIZONTAL)
if variant == "Root Patching":
# Label
label = wx.StaticText(dialog, label="Root Patching will patch the following:", pos=(-1, title.GetPosition()[1] + 30))
label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
label.Centre(wx.HORIZONTAL)
# Get longest patch label, then create anchor for patch labels
longest_patch = ""
for patch in patches:
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
if len(patch) > len(longest_patch):
longest_patch = patch
anchor = wx.StaticText(dialog, label=longest_patch, pos=(label.GetPosition()[0], label.GetPosition()[1] + 20))
anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
anchor.Centre(wx.HORIZONTAL)
anchor.Hide()
# Labels
i = 0
for patch in patches:
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
logging.info(f"- Adding patch: {patch} - {patches[patch]}")
patch_label = wx.StaticText(dialog, label=f"- {patch}", pos=(anchor.GetPosition()[0], label.GetPosition()[1] + 20 + i))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
i = i + 20
if i == 20:
patch_label.SetLabel(patch_label.GetLabel().replace("-", ""))
patch_label.Centre(wx.HORIZONTAL)
elif i == 0:
patch_label = wx.StaticText(dialog, label="No patches to apply", pos=(label.GetPosition()[0], label.GetPosition()[1] + 20))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
patch_label.Centre(wx.HORIZONTAL)
else:
patch_label = wx.StaticText(dialog, label="Reverting to last sealed snapshot", pos=(-1, title.GetPosition()[1] + 30))
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
patch_label.Centre(wx.HORIZONTAL)
# Text box
text_box = wx.TextCtrl(dialog, pos=(10, patch_label.GetPosition()[1] + 30), size=(400, 400), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2)
text_box.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
text_box.Centre(wx.HORIZONTAL)
self.text_box = text_box
# Button: Return to Main Menu
return_button = wx.Button(dialog, label="Return to Main Menu", pos=(10, text_box.GetPosition()[1] + text_box.GetSize()[1] + 5), size=(150, 30))
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
return_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
return_button.Centre(wx.HORIZONTAL)
self.return_button = return_button
# Set frame size
dialog.SetSize((-1, return_button.GetPosition().y + return_button.GetSize().height + 33))
dialog.ShowWindowModal()
def start_root_patching(self, patches: dict):
logging.info("Starting root patching")
while gui_support.PayloadMount(self.constants, self).is_unpack_finished() is False:
wx.Yield()
if patches["Settings: Kernel Debug Kit missing"] is True:
if self._kdk_download(self) is False:
self.on_return_to_main_menu()
return
self._generate_modal(patches, "Root Patching")
self.return_button.Disable()
thread = threading.Thread(target=self._start_root_patching, args=(patches,))
thread.start()
while thread.is_alive():
wx.Yield()
self._post_patch()
self.return_button.Enable()
def _start_root_patching(self, patches: dict):
logger = logging.getLogger()
logger.addHandler(gui_support.ThreadHandler(self.text_box))
try:
sys_patch.PatchSysVolume(self.constants.computer.real_model, self.constants, patches).start_patch()
except:
logging.error("- An internal error occurred while running the Root Patcher:\n")
logging.error(traceback.format_exc())
logger.removeHandler(logger.handlers[2])
def revert_root_patching(self, patches: dict):
logging.info("Reverting root patches")
self._generate_modal(patches, "Revert Root Patches")
self.return_button.Disable()
thread = threading.Thread(target=self._revert_root_patching, args=(patches,))
thread.start()
while thread.is_alive():
wx.Yield()
self._post_patch()
self.return_button.Enable()
def _revert_root_patching(self, patches: dict):
logger = logging.getLogger()
logger.addHandler(gui_support.ThreadHandler(self.text_box))
try:
sys_patch.PatchSysVolume(self.constants.computer.real_model, self.constants, patches).start_unpatch()
except:
logging.error("- An internal error occurred while running the Root Patcher:\n")
logging.error(traceback.format_exc())
logger.removeHandler(logger.handlers[2])
def on_return_to_main_menu(self, event: wx.Event = None):
self.frame_modal.Hide()
main_menu_frame = gui_main_menu.MainFrame(
None,
title=self.title,
global_constants=self.constants,
screen_location=self.GetScreenPosition()
)
main_menu_frame.Show()
self.frame_modal.Destroy()
self.Destroy()
def _post_patch(self):
if self.constants.root_patcher_succeeded is False:
return
if self.constants.needs_to_open_preferences is False:
gui_support.RestartHost(self).restart(message="Root Patcher finished successfully!\n\nWould you like to reboot now?")
return
if self.constants.detected_os >= os_data.os_data.ventura:
gui_support.RestartHost(self).restart(message="Root Patcher finished successfully!\nIf you were prompted to open System Settings to authorize new kexts, this can be ignored. Your system is ready once restarted.\n\nWould you like to reboot now?")
return
# Create dialog box to open System Preferences -> Security and Privacy
self.popup = wx.MessageDialog(
self.frame_modal,
"We just finished installing the patches to your Root Volume!\n\nHowever, Apple requires users to manually approve the kernel extensions installed before they can be used next reboot.\n\nWould you like to open System Preferences?",
"Open System Preferences?",
wx.YES_NO | wx.ICON_INFORMATION
)
self.popup.SetYesNoLabels("Open System Preferences", "Ignore")
answer = self.popup.ShowModal()
if answer == wx.ID_YES:
output =subprocess.run(
[
"osascript", "-e",
'tell app "System Preferences" to activate',
"-e", 'tell app "System Preferences" to reveal anchor "General" of pane id "com.apple.preference.security"',
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
if output.returncode != 0:
# Some form of fallback if unaccelerated state errors out
subprocess.run(["open", "-a", "System Preferences"])
time.sleep(5)
sys.exit(0)
def _check_if_new_patches_needed(self, patches: dict) -> bool:
"""
Checks if any new patches are needed for the user to install
Newer users will assume the root patch menu will present missing patches.
Thus we'll need to see if the exact same OCLP build was used already
"""
if self.constants.commit_info[0] in ["Running from source", "Built from source"]:
return True
if self.constants.computer.oclp_sys_url != self.constants.commit_info[2]:
# If commits are different, assume patches are as well
return True
oclp_plist = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist"
if not Path(oclp_plist).exists():
# If it doesn't exist, no patches were ever installed
# ie. all patches applicable
return True
oclp_plist_data = plistlib.load(open(oclp_plist, "rb"))
for patch in patches:
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
# Patches should share the same name as the plist key
# See sys_patch_dict.py for more info
patch_installed = False
for key in oclp_plist_data:
if "Display Name" not in oclp_plist_data[key]:
continue
if oclp_plist_data[key]["Display Name"] == patch:
patch_installed = True
break
if patch_installed is False:
logging.info(f"- Patch {patch} not installed")
return True
logging.info("- No new patches detected for system")
return False

View File

@@ -0,0 +1,254 @@
# Generate UI for updating the patcher
import wx
import sys
import time
import logging
import datetime
import threading
import subprocess
from pathlib import Path
from resources.wx_gui import gui_download
from resources import (
constants,
network_handler,
updates
)
class UpdateFrame(wx.Frame):
"""
Create a frame for updating the patcher
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: wx.Point, url: str = "", version_label: str = "") -> None:
if parent:
self.parent: wx.Frame = parent
for child in self.parent.GetChildren():
child.Hide()
parent.Hide()
else:
super(UpdateFrame, self).__init__(parent, title=title, size=(350, 300), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
self.title: str = title
self.constants: constants.Constants = global_constants
self.application_path = self.constants.payload_path / "OpenCore-Patcher.app"
self.screen_location: wx.Point = screen_location
if parent:
self.parent.Centre()
self.screen_location = parent.GetScreenPosition()
else:
self.Centre()
self.screen_location = self.GetScreenPosition()
if url == "" or version_label == "":
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
if dict:
for key in dict:
version_label = dict[key]["Version"]
url = dict[key]["Link"]
break
self.version_label = version_label
self.url = url
self.frame: wx.Frame = wx.Frame(
parent=parent if parent else self,
title=self.title,
size=(350, 130),
pos=self.screen_location,
style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX
)
# Title: Preparing update
title_label = wx.StaticText(self.frame, label="Preparing download...", pos=(-1,1))
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
title_label.Centre(wx.HORIZONTAL)
# Progress bar
progress_bar = wx.Gauge(self.frame, range=100, pos=(10, 50), size=(300, 20))
progress_bar.Centre(wx.HORIZONTAL)
progress_bar.Pulse()
self.progress_bar = progress_bar
self.frame.Show()
wx.Yield()
download_obj = network_handler.DownloadObject(url, self.constants.payload_path / "OpenCore-Patcher-GUI.app.zip")
gui_download.DownloadFrame(
self.frame,
title=self.title,
global_constants=self.constants,
download_obj=download_obj,
item_name=f"OpenCore Patcher {version_label}"
)
if download_obj.download_complete is False:
progress_bar.SetValue(0)
wx.MessageBox("Failed to download update. If you continue to have this issue, please manually download OpenCore Legacy Patcher off Github", "Critical Error!", wx.OK | wx.ICON_ERROR)
sys.exit(1)
# Title: Extracting update
title_label.SetLabel("Extracting update...")
title_label.Centre(wx.HORIZONTAL)
wx.Yield()
thread = threading.Thread(target=self._extract_update)
thread.start()
while thread.is_alive():
wx.Yield()
# Title: Installing update
title_label.SetLabel("Installing update...")
title_label.Centre(wx.HORIZONTAL)
thread = threading.Thread(target=self._install_update)
thread.start()
while thread.is_alive():
wx.Yield()
# Title: Update complete
title_label.SetLabel("Update complete!")
title_label.Centre(wx.HORIZONTAL)
# Progress bar
progress_bar.Hide()
# Label: 0.6.6 has been installed to:
installed_label = wx.StaticText(self.frame, label=f"{version_label} has been installed:", pos=(-1, progress_bar.GetPosition().y - 15))
installed_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
installed_label.Centre(wx.HORIZONTAL)
# Label: '/Library/Application Support/Dortania'
installed_path_label = wx.StaticText(self.frame, label='/Library/Application Support/Dortania', pos=(-1, installed_label.GetPosition().y + 20))
installed_path_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
installed_path_label.Centre(wx.HORIZONTAL)
# Label: Launching update shortly...
launch_label = wx.StaticText(self.frame, label="Launching update shortly...", pos=(-1, installed_path_label.GetPosition().y + 30))
launch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
launch_label.Centre(wx.HORIZONTAL)
# Adjust frame size
self.frame.SetSize((-1, launch_label.GetPosition().y + 60))
thread = threading.Thread(target=self._launch_update)
thread.start()
while thread.is_alive():
wx.Yield()
timer = 5
while True:
launch_label.SetLabel(f"Closing old process in {timer} seconds")
launch_label.Centre(wx.HORIZONTAL)
wx.Yield()
time.sleep(1)
timer -= 1
if timer == 0:
break
sys.exit(0)
def _extract_update(self) -> None:
"""
Extracts the update
"""
logging.info("Extracting update")
if Path(self.application_path).exists():
subprocess.run(["rm", "-rf", str(self.application_path)])
# Some hell spawn at Github decided to double zip our Github Actions artifacts
# So we need to unzip it twice
for i in range(2):
result = subprocess.run(
["ditto", "-xk", str(self.constants.payload_path / "OpenCore-Patcher-GUI.app.zip"), str(self.constants.payload_path)], capture_output=True
)
if result.returncode != 0:
wx.CallAfter(self.progress_bar.SetValue, 0)
wx.CallAfter(wx.MessageBox, f"Failed to extract update. Error: {result.stderr.decode('utf-8')}", "Critical Error!", wx.OK | wx.ICON_ERROR)
wx.CallAfter(sys.exit, 1)
break
if Path(self.application_path).exists():
break
if i == 1:
wx.CallAfter(self.progress_bar.SetValue, 0)
wx.CallAfter(wx.MessageBox, "Failed to extract update. Error: Update file does not exist", "Critical Error!", wx.OK | wx.ICON_ERROR)
wx.CallAfter(sys.exit, 1)
break
def _install_update(self) -> None:
"""
Installs update to '/Library/Application Support/Dortania/OpenCore-Patcher.app'
"""
logging.info(f"Installing update: {self.application_path}")
# Create bash script to run as root
script = f"""#!/bin/bash
# Check if '/Library/Application Support/Dortania' exists
if [ ! -d "/Library/Application Support/Dortania" ]; then
mkdir -p "/Library/Application Support/Dortania"
fi
# Check if '/Library/Application Support/Dortania/OpenCore-Patcher.app' exists
if [ -d "/Library/Application Support/Dortania/OpenCore-Patcher.app" ]; then
rm -rf "/Library/Application Support/Dortania/OpenCore-Patcher.app"
fi
# Move '/tmp/OpenCore-Patcher.app' to '/Library/Application Support/Dortania'
mv "{str(self.application_path)}" "/Library/Application Support/Dortania/OpenCore-Patcher.app"
# Check if '/Applications/OpenCore-Patcher.app' exists
if [ ! -d "/Applications/OpenCore-Patcher.app" ]; then
ln -s "/Library/Application Support/Dortania/OpenCore-Patcher.app" "/Applications/OpenCore-Patcher.app"
fi
# Create update.plist with info about update
cat << EOF > "/Library/Application Support/Dortania/update.plist"
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>{self.version_label}</string>
<key>CFBundleVersion</key>
<string>{self.version_label}</string>
<key>InstallationDate</key>
<date>{datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")}</date>
<key>InstallationSource</key>
<string>{self.url}</string>
</dict>
</plist>
EOF
"""
# Write script to file
with open(self.constants.payload_path / "update.sh", "w") as f:
f.write(script)
# Execute script
args = [self.constants.oclp_helper_path, "/bin/sh", str(self.constants.payload_path / "update.sh")]
result = subprocess.run(args, capture_output=True)
if result.returncode != 0:
wx.CallAfter(self.progress_bar.SetValue, 0)
if "User cancelled" in result.stderr.decode("utf-8"):
wx.CallAfter(wx.MessageBox, "User cancelled update", "Update Cancelled", wx.OK | wx.ICON_INFORMATION)
else:
wx.CallAfter(wx.MessageBox, f"Failed to install update. Error: {result.stderr.decode('utf-8')}", "Critical Error!", wx.OK | wx.ICON_ERROR)
wx.CallAfter(sys.exit, 1)
def _launch_update(self) -> None:
"""
Launches newly installed update
"""
logging.info("Launching update: '/Library/Application Support/Dortania/OpenCore-Patcher.app'")
subprocess.Popen(["/Library/Application Support/Dortania/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher", "--update_installed"])