mirror of
https://github.com/dortania/OpenCore-Legacy-Patcher.git
synced 2026-04-23 11:30:15 +10:00
Restructure into package format
This commit is contained in:
69
opencore_legacy_patcher/wx_gui/gui_about.py
Normal file
69
opencore_legacy_patcher/wx_gui/gui_about.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
gui_about.py: About frame
|
||||
"""
|
||||
|
||||
import wx
|
||||
import wx.adv
|
||||
import logging
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..wx_gui import gui_support
|
||||
|
||||
|
||||
class AboutFrame(wx.Frame):
|
||||
|
||||
def __init__(self, global_constants: constants.Constants) -> None:
|
||||
if wx.FindWindowByName("About"):
|
||||
return
|
||||
|
||||
logging.info("Generating About frame")
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(11, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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))
|
||||
165
opencore_legacy_patcher/wx_gui/gui_build.py
Normal file
165
opencore_legacy_patcher/wx_gui/gui_build.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
gui_build.py: Generate UI for Building OpenCore
|
||||
"""
|
||||
|
||||
import wx
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..efi_builder import build
|
||||
|
||||
from ..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:
|
||||
logging.info("Initializing Build Frame")
|
||||
super(BuildFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
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.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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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=(150, 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()
|
||||
|
||||
|
||||
192
opencore_legacy_patcher/wx_gui/gui_cache_os_update.py
Normal file
192
opencore_legacy_patcher/wx_gui/gui_cache_os_update.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
gui_cache_os_update.py: UI to display to users before a macOS update is applied
|
||||
Primarily for caching updates required for incoming OS (ex. KDKs)
|
||||
"""
|
||||
|
||||
import wx
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import constants
|
||||
from ..utilities import kdk_handler, utilities
|
||||
from ..wx_gui import gui_support, gui_download
|
||||
|
||||
|
||||
class OSUpdateFrame(wx.Frame):
|
||||
"""
|
||||
Create a modal frame for displaying information to the user before an update is applied
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing Prepare Update Frame")
|
||||
|
||||
if parent:
|
||||
self.frame = parent
|
||||
else:
|
||||
super().__init__(parent, title=title, size=(360, 140), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX)
|
||||
self.frame = self
|
||||
self.frame.Centre()
|
||||
|
||||
self.title = title
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
os_data = utilities.fetch_staged_update(variant="Preflight")
|
||||
if os_data[0] is None:
|
||||
logging.info("No staged update found")
|
||||
self._exit()
|
||||
logging.info(f"Staged update found: {os_data[0]} ({os_data[1]})")
|
||||
self.os_data = os_data
|
||||
|
||||
self._generate_ui()
|
||||
|
||||
self.kdk_obj: kdk_handler.KernelDebugKitObject = None
|
||||
def _kdk_thread_spawn():
|
||||
self.kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.os_data[1], self.os_data[0], passive=True, check_backups_only=True)
|
||||
|
||||
kdk_thread = threading.Thread(target=_kdk_thread_spawn)
|
||||
kdk_thread.start()
|
||||
|
||||
while kdk_thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if self.kdk_obj.success is False:
|
||||
self._exit()
|
||||
|
||||
kdk_download_obj = self.kdk_obj.retrieve_download()
|
||||
if not kdk_download_obj:
|
||||
# KDK is already downloaded
|
||||
# Return false since we didn't display anything
|
||||
self._exit()
|
||||
|
||||
self.kdk_download_obj = kdk_download_obj
|
||||
|
||||
self.frame.Show()
|
||||
|
||||
self.did_cancel = -1
|
||||
self._notifyUser()
|
||||
|
||||
# Allow 10 seconds for the user to cancel the download
|
||||
# If nothing, continue
|
||||
for i in range(0, 10):
|
||||
if self.did_cancel == 1:
|
||||
self._exit()
|
||||
if self.did_cancel == -1:
|
||||
time.sleep(1)
|
||||
|
||||
gui_download.DownloadFrame(
|
||||
self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
download_obj=kdk_download_obj,
|
||||
item_name=f"KDK Build {self.kdk_obj.kdk_url_build}"
|
||||
)
|
||||
if kdk_download_obj.download_complete is False:
|
||||
self._exit()
|
||||
|
||||
logging.info("KDK download complete, validating with hdiutil")
|
||||
self.kdk_checksum_result = False
|
||||
def _validate_kdk_checksum_thread():
|
||||
self.kdk_checksum_result = self.kdk_obj.validate_kdk_checksum()
|
||||
|
||||
kdk_checksum_thread = threading.Thread(target=_validate_kdk_checksum_thread)
|
||||
kdk_checksum_thread.start()
|
||||
|
||||
while kdk_checksum_thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if self.kdk_checksum_result is False:
|
||||
logging.error("KDK checksum validation failed")
|
||||
logging.error(self.kdk_obj.error_msg)
|
||||
self._exit()
|
||||
|
||||
logging.info("KDK checksum validation passed")
|
||||
|
||||
logging.info("Mounting KDK")
|
||||
if not Path(self.constants.kdk_download_path).exists():
|
||||
logging.error("KDK download path does not exist")
|
||||
self._exit()
|
||||
|
||||
self.kdk_install_result = False
|
||||
def _install_kdk_thread():
|
||||
self.kdk_install_result = kdk_handler.KernelDebugKitUtilities().install_kdk_dmg(self.constants.kdk_download_path, only_install_backup=True)
|
||||
|
||||
kdk_install_thread = threading.Thread(target=_install_kdk_thread)
|
||||
kdk_install_thread.start()
|
||||
|
||||
while kdk_install_thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if self.kdk_install_result is False:
|
||||
logging.info("Failed to install KDK")
|
||||
self._exit()
|
||||
|
||||
logging.info("KDK installed successfully")
|
||||
self._exit()
|
||||
|
||||
|
||||
def _generate_ui(self) -> None:
|
||||
"""
|
||||
Display frame
|
||||
|
||||
|
||||
Title: OpenCore Legacy Patcher is preparing to update your system
|
||||
Body: Please wait while we prepare your system for the update.
|
||||
This may take a few minutes.
|
||||
"""
|
||||
|
||||
header = wx.StaticText(self.frame, label="Preparing for macOS Software Update", pos=(-1,5))
|
||||
header.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
header.Centre(wx.HORIZONTAL)
|
||||
|
||||
# list OS
|
||||
label = wx.StaticText(self.frame, label=f"macOS {self.os_data[0]} ({self.os_data[1]})", pos=(-1, 35))
|
||||
label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# this may take a few minutes
|
||||
label = wx.StaticText(self.frame, label="This may take a few minutes.", pos=(-1, 55))
|
||||
label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Add a progress bar
|
||||
self.progress_bar = wx.Gauge(self.frame, range=100, pos=(10, 75), size=(340, 20))
|
||||
self.progress_bar.SetValue(0)
|
||||
self.progress_bar.Pulse()
|
||||
|
||||
# Set frame size below progress bar
|
||||
self.frame.SetSize((360, 140))
|
||||
|
||||
|
||||
def _notifyUser(self) -> None:
|
||||
"""
|
||||
Notify user of what OCLP is doing
|
||||
Note will be spawned through wx.CallAfter
|
||||
"""
|
||||
threading.Thread(target=self._notifyUserThread).start()
|
||||
|
||||
|
||||
def _notifyUserThread(self) -> None:
|
||||
"""
|
||||
Notify user of what OCLP is doing
|
||||
"""
|
||||
message=f"OpenCore Legacy Patcher has detected that a macOS update is being downloaded:\n{self.os_data[0]} ({self.os_data[1]})\n\nThe patcher needs to prepare the system for the update, and will download any additional resources it may need post-update.\n\nThis may take a few minutes, the patcher will exit when it is done."
|
||||
# Yes/No for caching
|
||||
dlg = wx.MessageDialog(self.frame, message=message, caption="OpenCore Legacy Patcher", style=wx.YES_NO | wx.ICON_INFORMATION)
|
||||
dlg.SetYesNoLabels("&Ok", "&Cancel")
|
||||
result = dlg.ShowModal()
|
||||
if result == wx.ID_NO:
|
||||
logging.info("User cancelled OS caching")
|
||||
self.kdk_download_obj.stop()
|
||||
self.did_cancel = 1
|
||||
else:
|
||||
self.did_cancel = 0
|
||||
|
||||
def _exit(self):
|
||||
"""
|
||||
Exit the frame
|
||||
"""
|
||||
self.frame.Close()
|
||||
sys.exit()
|
||||
106
opencore_legacy_patcher/wx_gui/gui_download.py
Normal file
106
opencore_legacy_patcher/wx_gui/gui_download.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
gui_download.py: Generate UI for downloading files
|
||||
"""
|
||||
|
||||
import wx
|
||||
import logging
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..wx_gui import gui_support
|
||||
|
||||
from ..utilities import (
|
||||
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, download_icon = None) -> None:
|
||||
logging.info("Initializing Download Frame")
|
||||
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
|
||||
if download_icon:
|
||||
self.download_icon: str = download_icon
|
||||
else:
|
||||
self.download_icon: str = "/System/Library/CoreServices/Installer.app/Contents/Resources/package.icns"
|
||||
|
||||
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
|
||||
icon = self.download_icon
|
||||
icon = wx.StaticBitmap(frame, bitmap=wx.Bitmap(icon, wx.BITMAP_TYPE_ICON), pos=(-1, 20))
|
||||
icon.SetSize((100, 100))
|
||||
icon.Centre(wx.HORIZONTAL)
|
||||
|
||||
title_label = wx.StaticText(frame, label=f"Downloading: {self.item_name}", pos=(-1,icon.GetPosition()[1] + icon.GetSize()[1] + 20))
|
||||
title_label.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar = wx.Gauge(frame, range=100, pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5), size=(300, 20), style=wx.GA_SMOOTH|wx.GA_PROGRESS)
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
label_amount = wx.StaticText(frame, label="Preparing download", pos=(-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1]))
|
||||
label_amount.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
label_amount.Centre(wx.HORIZONTAL)
|
||||
|
||||
return_button = wx.Button(frame, label="Cancel", pos=(-1, label_amount.GetPosition()[1] + label_amount.GetSize()[1] + 10))
|
||||
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():
|
||||
|
||||
percentage: int = round(self.download_obj.get_percent())
|
||||
if percentage == 0:
|
||||
percentage = 1
|
||||
|
||||
if percentage == -1:
|
||||
amount_str = f"{utilities.human_fmt(self.download_obj.downloaded_file_size)} downloaded ({utilities.human_fmt(self.download_obj.get_speed())}/s)"
|
||||
progress_bar.Pulse()
|
||||
else:
|
||||
amount_str = f"{utilities.seconds_to_readable_time(self.download_obj.get_time_remaining())}left - {utilities.human_fmt(self.download_obj.downloaded_file_size)} of {utilities.human_fmt(self.download_obj.total_file_size)} ({utilities.human_fmt(self.download_obj.get_speed())}/s)"
|
||||
progress_bar.SetValue(int(percentage))
|
||||
|
||||
label_amount.SetLabel(amount_str)
|
||||
label_amount.Centre(wx.HORIZONTAL)
|
||||
|
||||
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)
|
||||
|
||||
progress_bar.Destroy()
|
||||
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:
|
||||
logging.info("User cancelled download")
|
||||
self.user_cancelled = True
|
||||
self.download_obj.stop()
|
||||
|
||||
|
||||
99
opencore_legacy_patcher/wx_gui/gui_entry.py
Normal file
99
opencore_legacy_patcher/wx_gui/gui_entry.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
gui_entry.py: Entry point for the wxPython GUI
|
||||
"""
|
||||
|
||||
import wx
|
||||
import sys
|
||||
import atexit
|
||||
import logging
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..sys_patch import sys_patch_detect
|
||||
|
||||
from ..wx_gui import (
|
||||
gui_cache_os_update,
|
||||
gui_main_menu,
|
||||
gui_build,
|
||||
gui_install_oc,
|
||||
gui_sys_patch_start,
|
||||
gui_update,
|
||||
)
|
||||
|
||||
|
||||
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_start.SysPatchStartFrame
|
||||
UPDATE_APP = gui_update.UpdateFrame
|
||||
OS_CACHE = gui_cache_os_update.OSUpdateFrame
|
||||
|
||||
|
||||
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_start.SysPatchStartFrame
|
||||
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
|
||||
logging.info(f"Entry point set: {entry.__name__}")
|
||||
|
||||
# Normally set by main.py, but transitions from CLI mode may not have this set
|
||||
self.constants.gui_mode = True
|
||||
|
||||
self.frame: wx.Frame = entry(
|
||||
None,
|
||||
title=f"{self.constants.patcher_name} {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}",
|
||||
global_constants=self.constants,
|
||||
screen_location=None,
|
||||
**({"patches": patches} if "--gui_patch" in sys.argv or "--gui_unpatch" in sys.argv else {})
|
||||
)
|
||||
|
||||
atexit.register(self.OnCloseFrame)
|
||||
|
||||
if "--gui_patch" in sys.argv:
|
||||
self.frame.start_root_patching()
|
||||
elif "--gui_unpatch" in sys.argv:
|
||||
self.frame.revert_root_patching()
|
||||
|
||||
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()
|
||||
72
opencore_legacy_patcher/wx_gui/gui_help.py
Normal file
72
opencore_legacy_patcher/wx_gui/gui_help.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
gui_help.py: GUI Help Menu
|
||||
"""
|
||||
|
||||
import wx
|
||||
import logging
|
||||
import webbrowser
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..wx_gui import gui_support
|
||||
|
||||
|
||||
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:
|
||||
logging.info("Initializing Help Frame")
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
text_label = wx.StaticText(frame, label="Following resources are available:", pos=(-1,30))
|
||||
text_label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
366
opencore_legacy_patcher/wx_gui/gui_install_oc.py
Normal file
366
opencore_legacy_patcher/wx_gui/gui_install_oc.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
gui_install_oc.py: Frame for installing OpenCore to disk
|
||||
"""
|
||||
|
||||
import wx
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..datasets import os_data
|
||||
from ..utilities import install
|
||||
|
||||
from ..wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_support,
|
||||
gui_sys_patch_display
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
logging.info("Initializing Install OpenCore Frame")
|
||||
super(InstallOCFrame, self).__init__(parent, title=title, size=(300, 120), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
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.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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(10, wx.FONTWEIGHT_NORMAL))
|
||||
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
|
||||
logging.info("Available disks:")
|
||||
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(gui_support.font_factory(10, wx.FONTWEIGHT_NORMAL))
|
||||
disk_label.Centre(wx.HORIZONTAL)
|
||||
else:
|
||||
disk_label = wx.StaticText(dialog, label="", pos=(-1, disk_button.GetPosition()[1] + 15))
|
||||
disk_label.SetFont(gui_support.font_factory(10, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_BOLD))
|
||||
disk_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Add button: Search for disks again
|
||||
search_button = wx.Button(dialog, label="Search for disks again", size=(150,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=(150,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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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
|
||||
spacer = 0
|
||||
logging.info(f"Available partitions for {disk}:")
|
||||
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 + spacer))
|
||||
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()
|
||||
spacer += 25
|
||||
|
||||
# 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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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=(150,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 and self.constants.detected_os >= os_data.os_data.big_sur:
|
||||
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:
|
||||
self.Hide()
|
||||
gui_sys_patch_display.SysPatchDisplayFrame(
|
||||
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()
|
||||
|
||||
|
||||
|
||||
|
||||
495
opencore_legacy_patcher/wx_gui/gui_macos_installer_download.py
Normal file
495
opencore_legacy_patcher/wx_gui/gui_macos_installer_download.py
Normal file
@@ -0,0 +1,495 @@
|
||||
"""
|
||||
gui_macos_installer_download.py: macOS Installer Download Frame
|
||||
"""
|
||||
|
||||
import wx
|
||||
import locale
|
||||
import logging
|
||||
import threading
|
||||
import webbrowser
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..datasets import (
|
||||
os_data,
|
||||
smbios_data,
|
||||
cpu_data
|
||||
)
|
||||
from ..wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_support,
|
||||
gui_download,
|
||||
gui_macos_installer_flash
|
||||
)
|
||||
from ..utilities import (
|
||||
macos_installer_handler,
|
||||
utilities,
|
||||
network_handler,
|
||||
integrity_verification
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
logging.info("Initializing macOS Installer Download Frame")
|
||||
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.catalog_seed: macos_installer_handler.SeedType = macos_installer_handler.SeedType.DeveloperSeed
|
||||
|
||||
self.frame_modal = wx.Dialog(parent, title=title, size=(330, 200))
|
||||
|
||||
self._generate_elements(self.frame_modal)
|
||||
self.frame_modal.ShowWindowModal()
|
||||
|
||||
self.icons = [[self._icon_to_bitmap(i), self._icon_to_bitmap(i, (64, 64))] for i in self.constants.icons_path]
|
||||
|
||||
def _icon_to_bitmap(self, icon: str, size: tuple = (32, 32)) -> wx.Bitmap:
|
||||
"""
|
||||
Convert icon to bitmap
|
||||
"""
|
||||
return wx.Bitmap(wx.Bitmap(icon, wx.BITMAP_TYPE_ICON).ConvertToImage().Rescale(size[0], size[1], wx.IMAGE_QUALITY_HIGH))
|
||||
|
||||
def _macos_version_to_icon(self, version: int) -> int:
|
||||
"""
|
||||
Convert macOS version to icon
|
||||
"""
|
||||
try:
|
||||
self.constants.icons_path[version - 19]
|
||||
return version - 19
|
||||
except IndexError:
|
||||
return 0
|
||||
|
||||
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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))
|
||||
gui_support.GenerateMenubar(self, self.constants).generate()
|
||||
self.Centre()
|
||||
|
||||
# Title: Pulling installer catalog
|
||||
title_label = wx.StaticText(self, label="Finding Available Software", pos=(-1,5))
|
||||
title_label.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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():
|
||||
logging.info(f"Fetching installer catalog: {macos_installer_handler.SeedType(self.catalog_seed).name}")
|
||||
remote_obj = macos_installer_handler.RemoteInstallerCatalog(seed_override=self.catalog_seed)
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
bundles = [wx.BitmapBundle.FromBitmaps(icon) for icon in self.icons]
|
||||
|
||||
self.frame_modal.Destroy()
|
||||
self.frame_modal = wx.Dialog(self, title="Select macOS Installer", size=(460, 500))
|
||||
|
||||
# Title: Select macOS Installer
|
||||
title_label = wx.StaticText(self.frame_modal, label="Select macOS Installer", pos=(-1,-1))
|
||||
title_label.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
|
||||
# macOS Installers list
|
||||
id = wx.NewIdRef()
|
||||
|
||||
self.list = wx.ListCtrl(self.frame_modal, id, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_NO_HEADER | wx.BORDER_SUNKEN)
|
||||
self.list.SetSmallImages(bundles)
|
||||
|
||||
self.list.InsertColumn(0, "Version")
|
||||
self.list.InsertColumn(1, "Size")
|
||||
self.list.InsertColumn(2, "Release Date")
|
||||
|
||||
installers = self.available_installers_latest if show_full is False else self.available_installers
|
||||
if show_full is False:
|
||||
self.frame_modal.SetSize((460, 370))
|
||||
|
||||
if installers:
|
||||
locale.setlocale(locale.LC_TIME, '')
|
||||
logging.info(f"Available installers on SUCatalog ({'All entries' if show_full else 'Latest only'}):")
|
||||
for item in installers:
|
||||
extra = " Beta" if installers[item]['Variant'] in ["DeveloperSeed" , "PublicSeed"] else ""
|
||||
logging.info(f"- macOS {installers[item]['Version']} ({installers[item]['Build']}):\n - Size: {utilities.human_fmt(installers[item]['Size'])}\n - Source: {installers[item]['Source']}\n - Variant: {installers[item]['Variant']}\n - Link: {installers[item]['Link']}\n")
|
||||
index = self.list.InsertItem(self.list.GetItemCount(), f"macOS {installers[item]['Version']} {os_data.os_conversion.convert_kernel_to_marketing_name(int(installers[item]['Build'][:2]))}{extra} ({installers[item]['Build']})")
|
||||
self.list.SetItemImage(index, self._macos_version_to_icon(int(installers[item]['Build'][:2])))
|
||||
self.list.SetItem(index, 1, utilities.human_fmt(installers[item]['Size']))
|
||||
self.list.SetItem(index, 2, installers[item]['Date'].strftime("%x"))
|
||||
else:
|
||||
logging.error("No installers found on SUCatalog")
|
||||
wx.MessageDialog(self.frame_modal, "Failed to download Installer Catalog from Apple", "Error", wx.OK | wx.ICON_ERROR).ShowModal()
|
||||
|
||||
self.list.SetColumnWidth(0, 280)
|
||||
self.list.SetColumnWidth(1, 65)
|
||||
if show_full is True:
|
||||
self.list.SetColumnWidth(2, 80)
|
||||
else:
|
||||
self.list.SetColumnWidth(2, 94) # Hack to get the highlight to fill the ListCtrl
|
||||
|
||||
if show_full is False:
|
||||
self.list.Select(-1)
|
||||
|
||||
self.list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_select_list)
|
||||
self.list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_select_list)
|
||||
|
||||
self.select_button = wx.Button(self.frame_modal, label="Download", pos=(-1, -1), size=(150, -1))
|
||||
self.select_button.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
self.select_button.Bind(wx.EVT_BUTTON, lambda event, installers=installers: self.on_download_installer(installers))
|
||||
self.select_button.SetToolTip("Download the selected macOS Installer.")
|
||||
self.select_button.SetDefault()
|
||||
if show_full is True:
|
||||
self.select_button.Disable()
|
||||
|
||||
self.copy_button = wx.Button(self.frame_modal, label="Copy Link", pos=(-1, -1), size=(80, -1))
|
||||
self.copy_button.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
if show_full is True:
|
||||
self.copy_button.Disable()
|
||||
self.copy_button.SetToolTip("Copy the download link of the selected macOS Installer.")
|
||||
self.copy_button.Bind(wx.EVT_BUTTON, lambda event, installers=installers: self.on_copy_link(installers))
|
||||
|
||||
return_button = wx.Button(self.frame_modal, label="Return to Main Menu", pos=(-1, -1), size=(150, -1))
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
return_button.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
|
||||
self.showolderversions_checkbox = wx.CheckBox(self.frame_modal, label="Show Older/Beta Versions", pos=(-1, -1))
|
||||
if show_full is True:
|
||||
self.showolderversions_checkbox.SetValue(True)
|
||||
self.showolderversions_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self._display_available_installers(event, self.showolderversions_checkbox.GetValue()))
|
||||
|
||||
rectbox = wx.StaticBox(self.frame_modal, -1)
|
||||
rectsizer = wx.StaticBoxSizer(rectbox, wx.HORIZONTAL)
|
||||
rectsizer.Add(self.copy_button, 0, wx.EXPAND | wx.RIGHT, 5)
|
||||
rectsizer.Add(self.select_button, 0, wx.EXPAND | wx.LEFT, 5)
|
||||
|
||||
checkboxsizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
checkboxsizer.Add(self.showolderversions_checkbox, 0, wx.ALIGN_CENTRE | wx.RIGHT, 5)
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
sizer.AddSpacer(10)
|
||||
sizer.Add(title_label, 0, wx.ALIGN_CENTRE | wx.ALL, 0)
|
||||
sizer.Add(self.list, 1, wx.EXPAND | wx.ALL, 10)
|
||||
sizer.Add(rectsizer, 0, wx.ALIGN_CENTRE | wx.ALL, 0)
|
||||
sizer.Add(checkboxsizer, 0, wx.ALIGN_CENTRE | wx.ALL, 15)
|
||||
sizer.Add(return_button, 0, wx.ALIGN_CENTRE | wx.BOTTOM, 15)
|
||||
|
||||
self.frame_modal.SetSizer(sizer)
|
||||
self.frame_modal.ShowWindowModal()
|
||||
|
||||
def on_copy_link(self, installers: dict) -> None:
|
||||
|
||||
selected_item = self.list.GetFirstSelected()
|
||||
if selected_item != -1:
|
||||
clipboard = wx.Clipboard.Get()
|
||||
|
||||
if not clipboard.IsOpened():
|
||||
clipboard.Open()
|
||||
|
||||
clipboard.SetData(wx.TextDataObject(list(installers.values())[selected_item]['Link']))
|
||||
|
||||
clipboard.Close()
|
||||
|
||||
wx.MessageDialog(self.frame_modal, "Download link copied to clipboard", "", wx.OK | wx.ICON_INFORMATION).ShowModal()
|
||||
|
||||
|
||||
def on_select_list(self, event):
|
||||
if self.list.GetSelectedItemCount() > 0:
|
||||
self.select_button.Enable()
|
||||
self.copy_button.Enable()
|
||||
else:
|
||||
self.select_button.Disable()
|
||||
self.copy_button.Disable()
|
||||
|
||||
def on_download_installer(self, installers: dict) -> None:
|
||||
"""
|
||||
Download macOS installer
|
||||
"""
|
||||
|
||||
selected_item = self.list.GetFirstSelected()
|
||||
if selected_item != -1:
|
||||
|
||||
logging.info(f"Selected macOS {list(installers.values())[selected_item]['Version']} ({list(installers.values())[selected_item]['Build']})")
|
||||
|
||||
# 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 list(installers.values())[selected_item]["OS"] >= os_data.os_data.ventura:
|
||||
if smbios_data.smbios_dictionary[model]["CPU Generation"] <= cpu_data.CPUGen.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:
|
||||
logging.warning(f"Potential issues with {model} and {list(installers.values())[selected_item]['Version']} ({list(installers.values())[selected_item]['Build']}): {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. Otherwise, we recommend using macOS Monterey", "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 = list(installers.values())[selected_item]['Size'] * 2
|
||||
if host_space < needed_space:
|
||||
logging.error(f"Insufficient space to download and extract: {utilities.human_fmt(host_space)} available vs {utilities.human_fmt(needed_space)} required")
|
||||
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(list(installers.values())[selected_item]['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 {list(installers.values())[selected_item]['Version']} ({list(installers.values())[selected_item]['Build']})",
|
||||
download_icon=self.constants.icons_path[self._macos_version_to_icon(int(list(installers.values())[selected_item]['Build'][:2]))]
|
||||
)
|
||||
|
||||
if download_obj.download_complete is False:
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
self._validate_installer(list(installers.values())[selected_item]['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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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:
|
||||
logging.info("Validating macOS installer")
|
||||
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:
|
||||
logging.error(f"Chunklist validation failed: Hash mismatch on {chunk_obj.current_chunk}")
|
||||
wx.MessageBox(f"Chunklist validation failed: Hash mismatch on {chunk_obj.current_chunk}\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
|
||||
|
||||
logging.info("macOS installer validated")
|
||||
|
||||
# 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()
|
||||
597
opencore_legacy_patcher/wx_gui/gui_macos_installer_flash.py
Normal file
597
opencore_legacy_patcher/wx_gui/gui_macos_installer_flash.py
Normal file
@@ -0,0 +1,597 @@
|
||||
"""
|
||||
gui_macos_installer_flash.py: macOS Installer Flash Frame
|
||||
"""
|
||||
|
||||
import wx
|
||||
import time
|
||||
import logging
|
||||
import plistlib
|
||||
import tempfile
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..datasets import os_data
|
||||
|
||||
from ..wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_build,
|
||||
gui_support
|
||||
)
|
||||
from ..utilities import (
|
||||
macos_installer_handler,
|
||||
utilities,
|
||||
network_handler,
|
||||
kdk_handler,
|
||||
)
|
||||
|
||||
|
||||
class macOSInstallerFlashFrame(wx.Frame):
|
||||
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing macOS Installer Flash Frame")
|
||||
super(macOSInstallerFlashFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
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.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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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))
|
||||
|
||||
# Title: Select macOS Installer
|
||||
title_label = wx.StaticText(frame_modal, label="Select local macOS Installer", pos=(-1,5))
|
||||
title_label.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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:
|
||||
logging.info(f"Selected installer: {installer['Short Name']} ({installer['Version']} ({installer['Build']}))")
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(11, wx.FONTWEIGHT_NORMAL))
|
||||
warning_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# List of disks
|
||||
if self.available_disks:
|
||||
spacer = 5
|
||||
entries = len(self.available_disks)
|
||||
logging.info("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(gui_support.font_factory(13, wx.FONTWEIGHT_BOLD))
|
||||
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=(150, 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=(150, 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
|
||||
|
||||
logging.info(f"Selected disk: {disk['name']}")
|
||||
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(11, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(11, wx.FONTWEIGHT_NORMAL))
|
||||
warning_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Label: Bytes Written: 0 MB
|
||||
bytes_written_label = wx.StaticText(self, label="Bytes Written: 0.00 MB", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + 5))
|
||||
bytes_written_label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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:
|
||||
logging.error("Failed to prepare resources, cannot continue.")
|
||||
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)
|
||||
|
||||
# /dev/diskX -> diskX
|
||||
root_disk = disk['identifier'][5:]
|
||||
initial_bytes_written = float(utilities.monitor_disk_output(root_disk))
|
||||
self.result = False
|
||||
def _flash():
|
||||
logging.info(f"Flashing {installer['Path']} to {root_disk}")
|
||||
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:
|
||||
total_bytes_written = initial_bytes_written
|
||||
bytes_written = total_bytes_written - initial_bytes_written
|
||||
wx.CallAfter(bytes_written_label.SetLabel, f"Bytes Written: {bytes_written:.2f} MB")
|
||||
try:
|
||||
bytes_written = int(bytes_written)
|
||||
except:
|
||||
bytes_written = 0
|
||||
wx.CallAfter(progress_bar.SetValue, bytes_written)
|
||||
wx.Yield()
|
||||
|
||||
if self.result is False:
|
||||
logging.error("Failed to flash installer, cannot continue.")
|
||||
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(["/bin/rm", self.constants.installer_pkg_path])
|
||||
subprocess.run(["/usr/bin/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(["/bin/mkdir", "-p", f"{path}/Library/Packages/"])
|
||||
subprocess.run(["/bin/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(["/usr/bin/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(["/bin/cp", "-r", f"{mount_point}/KernelDebugKit.pkg", kdk_pkg_path])
|
||||
|
||||
logging.info("Unmounting KDK")
|
||||
result = subprocess.run(["/usr/bin/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:
|
||||
logging.info("Validating installer pkg")
|
||||
error_message = ""
|
||||
def _integrity_check():
|
||||
nonlocal error_message
|
||||
for folder in Path(utilities.grab_mount_point_from_disk(disk + "s2")).glob("*.app"):
|
||||
if folder.is_dir():
|
||||
dmg_path = folder / "Contents" / "SharedSupport" / "SharedSupport.dmg"
|
||||
break
|
||||
|
||||
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(["/usr/bin/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 error_message == "":
|
||||
logging.info("Installer pkg validated")
|
||||
return 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()
|
||||
428
opencore_legacy_patcher/wx_gui/gui_main_menu.py
Normal file
428
opencore_legacy_patcher/wx_gui/gui_main_menu.py
Normal file
@@ -0,0 +1,428 @@
|
||||
"""
|
||||
gui_main_menu.py: Generate GUI for main menu
|
||||
"""
|
||||
|
||||
import wx
|
||||
import wx.html2
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import requests
|
||||
import markdown2
|
||||
import threading
|
||||
import webbrowser
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..utilities import (
|
||||
global_settings,
|
||||
updates
|
||||
)
|
||||
from ..datasets import (
|
||||
os_data,
|
||||
css_data
|
||||
)
|
||||
from ..wx_gui import (
|
||||
gui_build,
|
||||
gui_macos_installer_download,
|
||||
gui_support,
|
||||
gui_help,
|
||||
gui_settings,
|
||||
gui_sys_patch_display,
|
||||
gui_update,
|
||||
)
|
||||
|
||||
|
||||
class MainFrame(wx.Frame):
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing Main Menu Frame")
|
||||
super(MainFrame, self).__init__(parent, title=title, size=(600, 400), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
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.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 {'' if self.constants.special_build else ''}{self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}", pos=(-1, 10))
|
||||
title_label.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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, title_label.GetPosition()[1] + 25
|
||||
))
|
||||
model_label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
model_label.Centre(wx.HORIZONTAL)
|
||||
self.model_label = model_label
|
||||
|
||||
# Buttons:
|
||||
menu_buttons = {
|
||||
"Build and Install OpenCore": {
|
||||
"function": self.on_build_and_install,
|
||||
"description": [
|
||||
"Prepares provided drive to be able",
|
||||
"to boot unsupported OSes.",
|
||||
"Use on installers or internal drives."
|
||||
],
|
||||
"icon": str(self.constants.icns_resource_path / "OC-Build.icns"),
|
||||
},
|
||||
"Create macOS Installer": {
|
||||
"function": self.on_create_macos_installer,
|
||||
"description": [
|
||||
"Download and flash a macOS",
|
||||
"Installer for your system.",
|
||||
],
|
||||
"icon": str(self.constants.icns_resource_path / "OC-Installer.icns"),
|
||||
},
|
||||
"⚙️ Settings": {
|
||||
"function": self.on_settings,
|
||||
"description": [
|
||||
],
|
||||
},
|
||||
"Post-Install Root Patch": {
|
||||
"function": self.on_post_install_root_patch,
|
||||
"description": [
|
||||
"Installs hardware drivers and",
|
||||
"patches for your system after",
|
||||
"installing a new version of macOS.",
|
||||
],
|
||||
"icon": str(self.constants.icns_resource_path / "OC-Patch.icns"),
|
||||
},
|
||||
|
||||
"Support": {
|
||||
"function": self.on_help,
|
||||
"description": [
|
||||
"Resources for OpenCore Legacy",
|
||||
"Patcher.",
|
||||
],
|
||||
"icon": str(self.constants.icns_resource_path / "OC-Support.icns"),
|
||||
},
|
||||
}
|
||||
button_x = 30
|
||||
button_y = model_label.GetPosition()[1] + 30
|
||||
rollover = len(menu_buttons) / 2
|
||||
if rollover % 1 != 0:
|
||||
rollover = int(rollover) + 1
|
||||
index = 0
|
||||
max_height = 0
|
||||
for button_name, button_function in menu_buttons.items():
|
||||
# place icon
|
||||
if "icon" in button_function:
|
||||
icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(button_function["icon"], wx.BITMAP_TYPE_ICON), pos=(button_x - 10, button_y), size=(64, 64))
|
||||
if button_name == "Post-Install Root Patch":
|
||||
icon.SetPosition((-1, button_y + 7))
|
||||
if button_name == "Create macOS Installer":
|
||||
icon.SetPosition((button_x - 5, button_y + 3))
|
||||
if button_name == "Support":
|
||||
# icon_mac.SetSize((80, 80))
|
||||
icon.SetPosition((button_x - 7, button_y + 3))
|
||||
if button_name == "Build and Install OpenCore":
|
||||
icon.SetSize((70, 70))
|
||||
if button_name == "⚙️ Settings":
|
||||
button_y += 5
|
||||
|
||||
button = wx.Button(self, label=button_name, pos=(button_x + 70, button_y), size=(180, 30))
|
||||
button.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
button.Bind(wx.EVT_BUTTON, lambda event, function=button_function["function"]: function(event))
|
||||
button_y += 30
|
||||
|
||||
# # Text: Description
|
||||
description_label = wx.StaticText(self, label='\n'.join(button_function["description"]), pos=(button_x + 75, button.GetPosition()[1] + button.GetSize()[1] + 3))
|
||||
description_label.SetFont(gui_support.font_factory(10, wx.FONTWEIGHT_NORMAL))
|
||||
# button_y += 15
|
||||
|
||||
for i, line in enumerate(button_function["description"]):
|
||||
if line == "":
|
||||
continue
|
||||
if i == 0:
|
||||
button_y += 11
|
||||
else:
|
||||
button_y += 13
|
||||
|
||||
button_y += 25
|
||||
|
||||
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()
|
||||
elif button_name == "⚙️ Settings":
|
||||
button.SetSize((100, -1))
|
||||
button.Centre(wx.HORIZONTAL)
|
||||
description_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
index += 1
|
||||
if index == rollover:
|
||||
max_height = button_y
|
||||
button_x = 320
|
||||
button_y = model_label.GetPosition()[1] + 30
|
||||
|
||||
|
||||
# Text: Copyright
|
||||
copy_label = wx.StaticText(self, label=self.constants.copyright_date, pos=(-1, max_height - 15))
|
||||
copy_label.SetFont(gui_support.font_factory(10, wx.FONTWEIGHT_NORMAL))
|
||||
copy_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set window size
|
||||
self.SetSize((-1, 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
|
||||
|
||||
self._fix_local_install()
|
||||
|
||||
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:
|
||||
logging.info("Skipping OpenCore and root volume patch update...")
|
||||
return
|
||||
|
||||
|
||||
logging.info("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 _fix_local_install(self) -> None:
|
||||
"""
|
||||
Work-around users manually copying the app to /Applications
|
||||
We'll delete the app, and create a proper symlink
|
||||
Note: This *shouldn't* be needed with installs after 0.6.7, but it's a good catch-all
|
||||
"""
|
||||
|
||||
if "--update_installed" not in sys.argv:
|
||||
return
|
||||
if self.constants.has_checked_updates is True:
|
||||
return
|
||||
|
||||
# Check if app exists in /Applications, and is not a symlink
|
||||
if Path("/Applications/OpenCore-Patcher.app").exists() and Path("/Applications/OpenCore-Patcher.app").is_symlink() is False:
|
||||
logging.info("Found user-installed app in /Applications, replacing with symlink")
|
||||
# Delete app
|
||||
result = subprocess.run(["/bin/rm", "-rf", "/Applications/OpenCore-Patcher.app"], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
logging.info("Failed to delete app from /Applications")
|
||||
return
|
||||
|
||||
# Create symlink
|
||||
result = subprocess.run(["/bin/ln", "-s", "/Library/Application Support/Dortania/OpenCore-Patcher.app", "/Applications/OpenCore-Patcher.app"], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
logging.info("Failed to create symlink to /Applications")
|
||||
return
|
||||
|
||||
|
||||
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
|
||||
|
||||
version = dict["Version"]
|
||||
logging.info(f"New version: {version}")
|
||||
|
||||
wx.CallAfter(self.on_update, dict["Link"], version, dict["Github Link"])
|
||||
|
||||
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):
|
||||
gui_sys_patch_display.SysPatchDisplayFrame(
|
||||
parent=self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition()
|
||||
)
|
||||
|
||||
|
||||
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, oclp_github_url: str):
|
||||
|
||||
ID_GITHUB = wx.NewId()
|
||||
ID_UPDATE = wx.NewId()
|
||||
|
||||
url = "https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest"
|
||||
response = requests.get(url).json()
|
||||
try:
|
||||
changelog = response["body"].split("## Asset Information")[0]
|
||||
except: #if user constantly checks for updates, github will rate limit them
|
||||
changelog = """## Unable to fetch changelog
|
||||
|
||||
Please check the Github page for more information about this release."""
|
||||
|
||||
html_markdown = markdown2.markdown(changelog, extras=["tables"])
|
||||
html_css = css_data.updater_css
|
||||
frame = wx.Dialog(None, -1, title="", size=(650, 500))
|
||||
frame.SetMinSize((650, 500))
|
||||
frame.SetWindowStyle(wx.STAY_ON_TOP)
|
||||
panel = wx.Panel(frame)
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
sizer.AddSpacer(10)
|
||||
self.title_text = wx.StaticText(panel, label="A new version of OpenCore Legacy Patcher is available!")
|
||||
self.description = wx.StaticText(panel, label=f"OpenCore Legacy Patcher {oclp_version} is now available - You have {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}. Would you like to update?")
|
||||
self.title_text.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
self.description.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
self.web_view = wx.html2.WebView.New(panel, style=wx.BORDER_SUNKEN)
|
||||
html_code = f'''
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
{html_css}
|
||||
</style>
|
||||
</head>
|
||||
<body class="markdown-body">
|
||||
{html_markdown.replace("<a href=", "<a target='_blank' href=")}
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
self.web_view.SetPage(html_code, "")
|
||||
self.web_view.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, self._onWebviewNav)
|
||||
self.web_view.EnableContextMenu(False)
|
||||
self.close_button = wx.Button(panel, label="Dismiss")
|
||||
self.close_button.Bind(wx.EVT_BUTTON, lambda event: frame.EndModal(wx.ID_CANCEL))
|
||||
self.view_button = wx.Button(panel, ID_GITHUB, label="View on GitHub")
|
||||
self.view_button.Bind(wx.EVT_BUTTON, lambda event: frame.EndModal(ID_GITHUB))
|
||||
self.install_button = wx.Button(panel, label="Download and Install")
|
||||
self.install_button.Bind(wx.EVT_BUTTON, lambda event: frame.EndModal(ID_UPDATE))
|
||||
self.install_button.SetDefault()
|
||||
|
||||
buttonsizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
buttonsizer.Add(self.close_button, 0, wx.ALIGN_CENTRE | wx.RIGHT, 5)
|
||||
buttonsizer.Add(self.view_button, 0, wx.ALIGN_CENTRE | wx.LEFT|wx.RIGHT, 5)
|
||||
buttonsizer.Add(self.install_button, 0, wx.ALIGN_CENTRE | wx.LEFT, 5)
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
sizer.Add(self.title_text, 0, wx.ALIGN_CENTRE | wx.TOP, 20)
|
||||
sizer.Add(self.description, 0, wx.ALIGN_CENTRE | wx.BOTTOM, 20)
|
||||
sizer.Add(self.web_view, 1, wx.EXPAND | wx.LEFT|wx.RIGHT, 10)
|
||||
sizer.Add(buttonsizer, 0, wx.ALIGN_RIGHT | wx.ALL, 20)
|
||||
panel.SetSizer(sizer)
|
||||
frame.Centre()
|
||||
|
||||
result = frame.ShowModal()
|
||||
|
||||
|
||||
if result == ID_GITHUB:
|
||||
webbrowser.open(oclp_github_url)
|
||||
elif result == ID_UPDATE:
|
||||
gui_update.UpdateFrame(
|
||||
parent=self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition(),
|
||||
url=oclp_url,
|
||||
version_label=oclp_version
|
||||
)
|
||||
|
||||
frame.Destroy()
|
||||
|
||||
def _onWebviewNav(self, event):
|
||||
url = event.GetURL()
|
||||
webbrowser.open(url)
|
||||
1344
opencore_legacy_patcher/wx_gui/gui_settings.py
Normal file
1344
opencore_legacy_patcher/wx_gui/gui_settings.py
Normal file
File diff suppressed because it is too large
Load Diff
383
opencore_legacy_patcher/wx_gui/gui_support.py
Normal file
383
opencore_legacy_patcher/wx_gui/gui_support.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
gui_support.py: Utilities for interacting with wxPython GUI
|
||||
"""
|
||||
|
||||
import os
|
||||
import wx
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import plistlib
|
||||
import threading
|
||||
import subprocess
|
||||
import applescript
|
||||
import packaging.version
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..wx_gui import gui_about
|
||||
from ..detections import device_probe
|
||||
|
||||
from ..datasets import (
|
||||
model_array,
|
||||
os_data,
|
||||
smbios_data
|
||||
)
|
||||
|
||||
|
||||
def get_font_face():
|
||||
if not get_font_face.font_face:
|
||||
get_font_face.font_face = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT).GetFaceName() or "Lucida Grande"
|
||||
|
||||
return get_font_face.font_face
|
||||
|
||||
|
||||
get_font_face.font_face = None
|
||||
|
||||
|
||||
# Centralize the common options for font creation
|
||||
def font_factory(size: int, weight):
|
||||
return wx.Font(size, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, weight, False, get_font_face())
|
||||
|
||||
|
||||
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()
|
||||
relaunchItem = fileMenu.Append(wx.ID_ANY, "&Relaunch as Root")
|
||||
fileMenu.AppendSeparator()
|
||||
revealLogItem = fileMenu.Append(wx.ID_ANY, "&Reveal Log File")
|
||||
|
||||
menubar.Append(fileMenu, "&File")
|
||||
self.frame.SetMenuBar(menubar)
|
||||
|
||||
self.frame.Bind(wx.EVT_MENU, lambda event: gui_about.AboutFrame(self.constants), aboutItem)
|
||||
self.frame.Bind(wx.EVT_MENU, lambda event: RelaunchApplicationAsRoot(self.frame, self.constants).relaunch(None), relaunchItem)
|
||||
self.frame.Bind(wx.EVT_MENU, lambda event: subprocess.run(["/usr/bin/open", "--reveal", self.constants.log_filepath]), revealLogItem)
|
||||
|
||||
if os.geteuid() == 0:
|
||||
relaunchItem.Enable(False)
|
||||
|
||||
|
||||
class GaugePulseCallback:
|
||||
"""
|
||||
Uses an alternative Pulse() method for wx.Gauge() on macOS Monterey+ with non-Metal GPUs
|
||||
Dirty hack, however better to display some form of animation than none at all
|
||||
|
||||
Note: This work-around is no longer needed on hosts using PatcherSupportPkg 1.1.2 or newer
|
||||
"""
|
||||
|
||||
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()
|
||||
if self.non_metal_alternative is True:
|
||||
if CheckProperties(global_constants).host_psp_version() >= packaging.version.Version("1.1.2"):
|
||||
self.non_metal_alternative = False
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def host_psp_version(self) -> packaging.version.Version:
|
||||
"""
|
||||
Grab PatcherSupportPkg version from OpenCore-Legacy-Patcher.plist
|
||||
"""
|
||||
oclp_plist_path = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist"
|
||||
if not Path(oclp_plist_path).exists():
|
||||
return packaging.version.Version("0.0.0")
|
||||
|
||||
oclp_plist = plistlib.load(open(oclp_plist_path, "rb"))
|
||||
if "PatcherSupportPkg" not in oclp_plist:
|
||||
return packaging.version.Version("0.0.0")
|
||||
|
||||
if oclp_plist["PatcherSupportPkg"].startswith("v"):
|
||||
oclp_plist["PatcherSupportPkg"] = oclp_plist["PatcherSupportPkg"][1:]
|
||||
|
||||
return packaging.version.parse(oclp_plist["PatcherSupportPkg"])
|
||||
|
||||
def host_has_3802_gpu(self) -> bool:
|
||||
"""
|
||||
Check if either host, or override model, has a 3802 GPU
|
||||
"""
|
||||
|
||||
gpu_archs = []
|
||||
if self.constants.custom_model:
|
||||
model = self.constants.custom_model
|
||||
else:
|
||||
model = self.constants.computer.real_model
|
||||
gpu_archs = [gpu.arch for gpu in self.constants.computer.gpus]
|
||||
|
||||
if not gpu_archs:
|
||||
gpu_archs = smbios_data.smbios_dictionary.get(model, {}).get("Stock GPUs", [])
|
||||
|
||||
for arch in gpu_archs:
|
||||
if arch in [
|
||||
device_probe.Intel.Archs.Ivy_Bridge,
|
||||
device_probe.Intel.Archs.Haswell,
|
||||
device_probe.NVIDIA.Archs.Kepler,
|
||||
]:
|
||||
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.YES_DEFAULT | 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 = [
|
||||
"/usr/bin/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(300, 300)
|
||||
self.frame.Centre()
|
||||
|
||||
# Header
|
||||
header = wx.StaticText(self.frame, label="Relaunching as root", pos=(-1, 5))
|
||||
header.SetFont(font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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)
|
||||
339
opencore_legacy_patcher/wx_gui/gui_sys_patch_display.py
Normal file
339
opencore_legacy_patcher/wx_gui/gui_sys_patch_display.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
gui_sys_patch_display.py: Display root patching menu
|
||||
"""
|
||||
|
||||
import os
|
||||
import wx
|
||||
import logging
|
||||
import plistlib
|
||||
import threading
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..sys_patch import sys_patch_detect
|
||||
|
||||
from ..wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_support,
|
||||
gui_sys_patch_start,
|
||||
)
|
||||
|
||||
|
||||
class SysPatchDisplayFrame(wx.Frame):
|
||||
"""
|
||||
Create a modal frame for displaying root patches
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing Root Patch Display Frame")
|
||||
|
||||
if parent:
|
||||
self.frame = parent
|
||||
else:
|
||||
super().__init__(parent, title=title, size=(360, 200), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX)
|
||||
self.frame = self
|
||||
self.frame.Centre()
|
||||
|
||||
self.title = title
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.frame_modal: wx.Dialog = None
|
||||
self.return_button: wx.Button = None
|
||||
self.available_patches: bool = False
|
||||
self.init_with_parent = True if parent else False
|
||||
|
||||
self.frame_modal = wx.Dialog(self.frame, title=title, size=(360, 200))
|
||||
|
||||
self._generate_elements_display_patches(self.frame_modal)
|
||||
|
||||
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
|
||||
if self.available_patches is False:
|
||||
gui_support.RestartHost(self.frame).restart(message="No root patch updates needed!\n\nWould you like to reboot to apply the new OpenCore build?")
|
||||
|
||||
|
||||
def _generate_elements_display_patches(self, frame: wx.Frame = None) -> 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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Label: Fetching patches...
|
||||
available_label = wx.StaticText(frame, label="Fetching patches for host", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 10))
|
||||
available_label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_BOLD))
|
||||
available_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Progress bar
|
||||
progress_bar = wx.Gauge(frame, range=100, pos=(-1, available_label.GetPosition()[1] + available_label.GetSize()[1] + 10), size=(250, 20))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
# Set window height
|
||||
frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
|
||||
|
||||
# Labels: {patch name}
|
||||
patches: dict = {}
|
||||
def _fetch_patches(self) -> None:
|
||||
nonlocal patches
|
||||
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
|
||||
thread = threading.Thread(target=_fetch_patches, args=(self,))
|
||||
thread.start()
|
||||
|
||||
frame.ShowWindowModal()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
|
||||
frame.Close()
|
||||
|
||||
progress_bar.Hide()
|
||||
progress_bar_animation.stop_pulse()
|
||||
|
||||
available_label.SetLabel("Available patches for your system:")
|
||||
available_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
anchor.Centre(wx.HORIZONTAL)
|
||||
anchor.Hide()
|
||||
|
||||
logging.info("Available patches:")
|
||||
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"- {patch}")
|
||||
patch_label = wx.StaticText(frame, label=f"- {patch}", pos=(anchor.GetPosition()[0], available_label.GetPosition()[1] + i))
|
||||
patch_label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
|
||||
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()[1] + 25))
|
||||
patch_label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_BOLD))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
longest_patch = ""
|
||||
for patch in patches:
|
||||
if not patch.startswith("Validation"):
|
||||
continue
|
||||
if patches[patch] is False:
|
||||
continue
|
||||
if patch == "Validation: Unpatching Possible":
|
||||
continue
|
||||
|
||||
if len(patch) > len(longest_patch):
|
||||
longest_patch = patch
|
||||
anchor = wx.StaticText(frame, label=longest_patch.split('Validation: ')[1], pos=(-1, patch_label.GetPosition()[1] + 20))
|
||||
anchor.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
anchor.Centre(wx.HORIZONTAL)
|
||||
anchor.Hide()
|
||||
|
||||
i = 0
|
||||
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=(anchor.GetPosition()[0], anchor.GetPosition()[1] + i))
|
||||
patch_label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
i = i + 20
|
||||
|
||||
if i == 20:
|
||||
patch_label.SetLabel(patch_label.GetLabel().replace("-", ""))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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.on_start_root_patching(patches))
|
||||
start_button.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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.on_revert_root_patching(patches))
|
||||
revert_button.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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_dismiss if self.init_with_parent else self.on_return_to_main_menu)
|
||||
return_button.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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()
|
||||
else:
|
||||
self.available_patches = False
|
||||
if can_unpatch is False:
|
||||
revert_button.Disable()
|
||||
|
||||
# Relaunch as root if not root
|
||||
if os.geteuid() != 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 + 15))
|
||||
frame.ShowWindowModal()
|
||||
|
||||
|
||||
def on_start_root_patching(self, patches: dict):
|
||||
frame = gui_sys_patch_start.SysPatchStartFrame(
|
||||
parent=None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
patches=patches,
|
||||
)
|
||||
frame.start_root_patching()
|
||||
self.on_return_dismiss() if self.init_with_parent else self.on_return_to_main_menu()
|
||||
|
||||
|
||||
def on_revert_root_patching(self, patches: dict):
|
||||
frame = gui_sys_patch_start.SysPatchStartFrame(
|
||||
parent=None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
patches=patches,
|
||||
)
|
||||
frame.revert_root_patching()
|
||||
self.on_return_dismiss() if self.init_with_parent else self.on_return_to_main_menu()
|
||||
|
||||
|
||||
def on_return_to_main_menu(self, event: wx.Event = None):
|
||||
# Get frame from event
|
||||
frame_modal: wx.Dialog = event.GetEventObject().GetParent()
|
||||
frame: wx.Frame = frame_modal.Parent
|
||||
frame_modal.Hide()
|
||||
frame.Hide()
|
||||
|
||||
main_menu_frame = gui_main_menu.MainFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
)
|
||||
main_menu_frame.Show()
|
||||
frame.Destroy()
|
||||
|
||||
|
||||
def on_return_dismiss(self, event: wx.Event = None):
|
||||
self.frame_modal.Hide()
|
||||
self.frame_modal.Destroy()
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
logging.info("Checking if new patches are needed")
|
||||
|
||||
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 isinstance(oclp_plist_data[key], (bool, int)):
|
||||
continue
|
||||
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
|
||||
381
opencore_legacy_patcher/wx_gui/gui_sys_patch_start.py
Normal file
381
opencore_legacy_patcher/wx_gui/gui_sys_patch_start.py
Normal file
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
gui_sys_patch_start.py: Root Patching Frame
|
||||
"""
|
||||
|
||||
import wx
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import plistlib
|
||||
import traceback
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .. import constants
|
||||
|
||||
from ..datasets import os_data
|
||||
from ..utilities import kdk_handler
|
||||
|
||||
from ..sys_patch import (
|
||||
sys_patch,
|
||||
sys_patch_detect
|
||||
)
|
||||
from ..wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_support,
|
||||
gui_download,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class SysPatchStartFrame(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 = {}):
|
||||
logging.info("Initializing Root Patching Frame")
|
||||
|
||||
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.patches: dict = patches
|
||||
|
||||
super(SysPatchStartFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, self.constants).generate()
|
||||
self.Centre()
|
||||
|
||||
if self.patches == {}:
|
||||
self.patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
|
||||
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
header.Centre(wx.HORIZONTAL)
|
||||
|
||||
subheader = wx.StaticText(frame, label="Fetching KDK database...", pos=(-1, header.GetPosition()[1] + header.GetSize()[1] + 5))
|
||||
subheader.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
subheader.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar = wx.Gauge(frame, range=100, pos=(-1, subheader.GetPosition()[1] + subheader.GetSize()[1] + 5), size=(250, 20))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
# Set size of frame
|
||||
frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 35))
|
||||
frame.Show()
|
||||
|
||||
# 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
|
||||
|
||||
logging.info("KDK download complete, validating with hdiutil")
|
||||
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")
|
||||
|
||||
for child in frame.GetChildren():
|
||||
child.Destroy()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _generate_modal(self, patches: dict = {}, variant: str = "Root Patching"):
|
||||
"""
|
||||
Create UI for root patching/unpatching
|
||||
"""
|
||||
supported_variants = ["Root Patching", "Revert Root Patches"]
|
||||
if variant not in supported_variants:
|
||||
logging.error(f"Unsupported variant: {variant}")
|
||||
return
|
||||
|
||||
self.frame_modal.Close() if self.frame_modal else None
|
||||
|
||||
dialog = wx.Dialog(self, title=self.title, size=(400, 200))
|
||||
|
||||
# Title
|
||||
title = wx.StaticText(dialog, label=variant, pos=(-1, 10))
|
||||
title.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
anchor.Centre(wx.HORIZONTAL)
|
||||
anchor.Hide()
|
||||
|
||||
# Labels
|
||||
i = 0
|
||||
logging.info("Available patches:")
|
||||
for patch in patches:
|
||||
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
|
||||
logging.info(f"- {patch}")
|
||||
patch_label = wx.StaticText(dialog, label=f"- {patch}", pos=(anchor.GetPosition()[0], label.GetPosition()[1] + 20 + i))
|
||||
patch_label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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))
|
||||
self.frame_modal = dialog
|
||||
dialog.ShowWindowModal()
|
||||
|
||||
|
||||
def start_root_patching(self):
|
||||
logging.info("Starting root patching")
|
||||
|
||||
while gui_support.PayloadMount(self.constants, self).is_unpack_finished() is False:
|
||||
wx.Yield()
|
||||
|
||||
if self.patches["Settings: Kernel Debug Kit missing"] is True:
|
||||
if self._kdk_download(self) is False:
|
||||
sys.exit(1)
|
||||
|
||||
self._generate_modal(self.patches, "Root Patching")
|
||||
self.return_button.Disable()
|
||||
|
||||
thread = threading.Thread(target=self._start_root_patching, args=(self.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):
|
||||
logging.info("Reverting root patches")
|
||||
|
||||
self._generate_modal(self.patches, "Revert Root Patches")
|
||||
self.return_button.Disable()
|
||||
|
||||
thread = threading.Thread(target=self._revert_root_patching, args=(self.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):
|
||||
# Get frame from event
|
||||
frame_modal: wx.Dialog = event.GetEventObject().GetParent()
|
||||
frame: wx.Frame = frame_modal.Parent
|
||||
frame_modal.Hide()
|
||||
frame.Hide()
|
||||
|
||||
main_menu_frame = gui_main_menu.MainFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
)
|
||||
main_menu_frame.Show()
|
||||
frame.Destroy()
|
||||
|
||||
|
||||
def on_return_dismiss(self, event: wx.Event = None):
|
||||
self.frame_modal.Hide()
|
||||
self.frame_modal.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.frame_modal).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.frame_modal).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(
|
||||
[
|
||||
"/usr/bin/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(["/usr/bin/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
|
||||
"""
|
||||
|
||||
logging.info("Checking if new patches are needed")
|
||||
|
||||
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 isinstance(oclp_plist_data[key], (bool, int)):
|
||||
continue
|
||||
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
|
||||
291
opencore_legacy_patcher/wx_gui/gui_update.py
Normal file
291
opencore_legacy_patcher/wx_gui/gui_update.py
Normal file
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
gui_update.py: 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 .. import constants
|
||||
|
||||
from ..wx_gui import (
|
||||
gui_download,
|
||||
gui_support
|
||||
)
|
||||
from ..utilities import (
|
||||
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:
|
||||
logging.info("Initializing Update Frame")
|
||||
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))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
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:
|
||||
version_label = dict["Version"]
|
||||
url = dict["Link"]
|
||||
else:
|
||||
wx.MessageBox("Failed to get update info", "Critical Error")
|
||||
sys.exit(1)
|
||||
|
||||
self.version_label = version_label
|
||||
self.url = url
|
||||
|
||||
logging.info(f"Update URL: {url}")
|
||||
logging.info(f"Update Version: {version_label}")
|
||||
|
||||
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(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
|
||||
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_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
self.progress_bar = progress_bar
|
||||
self.progress_bar_animation = progress_bar_animation
|
||||
|
||||
self.frame.Centre()
|
||||
self.frame.Show()
|
||||
wx.Yield()
|
||||
|
||||
download_obj = None
|
||||
def _fetch_update() -> None:
|
||||
nonlocal download_obj
|
||||
download_obj = network_handler.DownloadObject(url, self.constants.payload_path / "OpenCore-Patcher-GUI.app.zip")
|
||||
|
||||
thread = threading.Thread(target=_fetch_update)
|
||||
thread.start()
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
gui_download.DownloadFrame(
|
||||
self.frame,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
download_obj=download_obj,
|
||||
item_name=f"OpenCore Patcher {version_label}",
|
||||
download_icon=str(self.constants.app_icon_path)
|
||||
)
|
||||
|
||||
if download_obj.download_complete is False:
|
||||
progress_bar_animation.stop_pulse()
|
||||
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()
|
||||
progress_bar_animation.stop_pulse()
|
||||
|
||||
# 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(gui_support.font_factory(13, wx.FONTWEIGHT_BOLD))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
|
||||
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(["/bin/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(
|
||||
["/usr/bin/ditto", "-xk", str(self.constants.payload_path / "OpenCore-Patcher-GUI.app.zip"), str(self.constants.payload_path)], capture_output=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logging.error(f"Failed to extract update. Error: {result.stderr.decode('utf-8')}")
|
||||
wx.CallAfter(self.progress_bar_animation.stop_pulse)
|
||||
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:
|
||||
logging.error("Failed to extract update. Error: Update file does not exist")
|
||||
wx.CallAfter(self.progress_bar_animation.stop_pulse)
|
||||
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 'OpenCore-Patcher.app' exists
|
||||
if [ -d "/Library/Application Support/Dortania/OpenCore-Patcher.app" ]; then
|
||||
rm -rf "/Library/Application Support/Dortania/OpenCore-Patcher.app"
|
||||
fi
|
||||
|
||||
if [ -d "/Applications/OpenCore-Patcher.app" ]; then
|
||||
rm -rf "/Applications/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
|
||||
ln -s "/Library/Application Support/Dortania/OpenCore-Patcher.app" "/Applications/OpenCore-Patcher.app"
|
||||
|
||||
# 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_animation.stop_pulse)
|
||||
wx.CallAfter(self.progress_bar.SetValue, 0)
|
||||
if "User cancelled" in result.stderr.decode("utf-8"):
|
||||
logging.info("User cancelled update")
|
||||
wx.CallAfter(wx.MessageBox, "User cancelled update", "Update Cancelled", wx.OK | wx.ICON_INFORMATION)
|
||||
else:
|
||||
logging.critical(f"Failed to install update. Error: {result.stderr.decode('utf-8')}")
|
||||
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"])
|
||||
Reference in New Issue
Block a user