Add GUI Support for OpCore Simplify (#512)

* Refactor OpCore-Simplify to GUI version

* New ConfigEditor

* Add requirement checks and installation in launchers

* Add GitHub Actions workflow to generate manifest.json

* Set compression level for asset

* Skip .git and __pycache__ folders

* Refactor update process to include integrity checker

* Add SMBIOS model selection

* Update README.md

* Update to main branch
This commit is contained in:
Hoang Hong Quan
2025-12-30 14:19:47 +07:00
committed by GitHub
parent 871d826ea4
commit 0e608a56ce
38 changed files with 4948 additions and 1636 deletions

57
.github/workflows/generate-manifest.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Generate Manifest
on:
push:
paths-ignore:
- '.gitattributes'
- '.gitignore'
- 'LICENSE'
- 'README.md'
workflow_dispatch:
release:
types: [published]
jobs:
generate-manifest:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Generate manifest.json
run: |
python3 << 'EOF'
import os
import sys
from Scripts import integrity_checker
from Scripts import utils
checker = integrity_checker.IntegrityChecker()
root_folder = os.getcwd()
manifest_path = os.path.join(root_folder, "manifest.json")
print(f"Generating manifest from: {root_folder}")
manifest_data = checker.generate_folder_manifest(root_folder, manifest_path)
if manifest_data:
print(f"Manifest generated successfully with {len(manifest_data)} files")
else:
print("Failed to generate manifest")
sys.exit(1)
EOF
- name: Upload manifest.json to Artifacts
uses: actions/upload-artifact@v4
with:
name: manifest.json
path: ./manifest.json
if-no-files-found: error
compression-level: 0

View File

@@ -299,9 +299,56 @@ if /i "!just_installing!" == "TRUE" (
) )
exit /b exit /b
:checkrequirements
REM Check and install Python requirements
set "requirements_file=!thisDir!requirements.txt"
if not exist "!requirements_file!" (
echo Warning: requirements.txt not found. Skipping dependency check.
exit /b 0
)
echo Checking Python dependencies...
"!pypath!" -m pip --version > nul 2>&1
set "pip_check_error=!ERRORLEVEL!"
if not "!pip_check_error!" == "0" (
echo Warning: pip is not available. Attempting to install pip...
"!pypath!" -m ensurepip --upgrade > nul 2>&1
set "ensurepip_error=!ERRORLEVEL!"
if not "!ensurepip_error!" == "0" (
echo Error: Could not install pip. Please install pip manually.
exit /b 1
)
)
REM Try to import key packages to check if they're installed
"!pypath!" -c "import PyQt6; import qfluentwidgets" > nul 2>&1
set "import_check_error=!ERRORLEVEL!"
if not "!import_check_error!" == "0" (
echo Installing required packages from requirements.txt...
"!pypath!" -m pip install --upgrade -r "!requirements_file!"
set "pip_install_error=!ERRORLEVEL!"
if not "!pip_install_error!" == "0" (
echo.
echo Error: Failed to install requirements. Please install them manually:
echo !pypath! -m pip install -r !requirements_file!
echo.
echo Press [enter] to exit...
pause > nul
exit /b 1
)
echo Requirements installed successfully.
) else (
echo All requirements are already installed.
)
exit /b 0
:runscript :runscript
REM Python found REM Python found
cls cls
REM Check and install requirements before running the script
call :checkrequirements
set "req_check_error=!ERRORLEVEL!"
if not "!req_check_error!" == "0" (
exit /b 1
)
set "args=%*" set "args=%*"
set "args=!args:"=!" set "args=!args:"=!"
if "!args!"=="" ( if "!args!"=="" (

View File

@@ -283,6 +283,42 @@ prompt_and_download() {
done done
} }
check_and_install_requirements() {
local python="$1"
local requirements_file="$dir/requirements.txt"
# Check if requirements.txt exists
if [ ! -f "$requirements_file" ]; then
echo "Warning: requirements.txt not found. Skipping dependency check."
return 0
fi
# Check if pip is available
if ! "$python" -m pip --version > /dev/null 2>&1; then
echo "Warning: pip is not available. Attempting to install pip..."
if ! "$python" -m ensurepip --upgrade > /dev/null 2>&1; then
echo "Error: Could not install pip. Please install pip manually."
return 1
fi
fi
# Check if requirements are installed by trying to import key packages
echo "Checking Python dependencies..."
if ! "$python" -c "import PyQt6; import qfluentwidgets" > /dev/null 2>&1; then
echo "Installing required packages from requirements.txt..."
if ! "$python" -m pip install --upgrade -r "$requirements_file"; then
echo "Error: Failed to install requirements. Please install them manually:"
echo " $python -m pip install -r $requirements_file"
return 1
fi
echo "Requirements installed successfully."
else
echo "All requirements are already installed."
fi
return 0
}
main() { main() {
local python= version= local python= version=
# Verify our target exists # Verify our target exists
@@ -310,6 +346,11 @@ main() {
prompt_and_download prompt_and_download
return 1 return 1
fi fi
# Check and install requirements before running the script
if ! check_and_install_requirements "$python"; then
echo "Failed to install requirements. Exiting."
exit 1
fi
# Found it - start our script and pass all args # Found it - start our script and pass all args
"$python" "$dir/$target" "${args[@]}" "$python" "$dir/$target" "${args[@]}"
} }

View File

@@ -1,482 +1,318 @@
from Scripts.datasets import os_data
from Scripts.datasets import chipset_data
from Scripts import acpi_guru
from Scripts import compatibility_checker
from Scripts import config_prodigy
from Scripts import gathering_files
from Scripts import hardware_customizer
from Scripts import kext_maestro
from Scripts import report_validator
from Scripts import run
from Scripts import smbios
from Scripts import utils
import updater
import os import os
import sys import sys
import re import platform
import shutil
import traceback import traceback
import time
class OCPE: from PyQt6.QtCore import Qt, pyqtSignal
def __init__(self): from PyQt6.QtGui import QFont
self.u = utils.Utils("OpCore Simplify") from PyQt6.QtWidgets import QApplication
self.u.clean_temporary_dir() from qfluentwidgets import FluentWindow, NavigationItemPosition, FluentIcon, InfoBar, InfoBarPosition
self.ac = acpi_guru.ACPIGuru()
self.c = compatibility_checker.CompatibilityChecker()
self.co = config_prodigy.ConfigProdigy()
self.o = gathering_files.gatheringFiles()
self.h = hardware_customizer.HardwareCustomizer()
self.k = kext_maestro.KextMaestro()
self.s = smbios.SMBIOS()
self.v = report_validator.ReportValidator()
self.r = run.Run()
self.result_dir = self.u.get_temporary_dir()
def select_hardware_report(self): from Scripts.datasets import os_data
self.ac.dsdt = self.ac.acpi.acpi_tables = None from Scripts.state import HardwareReportState, macOSVersionState, SMBIOSState, BuildState
from Scripts.pages import HomePage, SelectHardwareReportPage, CompatibilityPage, ConfigurationPage, BuildPage, SettingsPage
from Scripts.backend import Backend
from Scripts import ui_utils
from Scripts.custom_dialogs import set_default_gui_handler
import updater
while True: WINDOW_MIN_SIZE = (1000, 700)
self.u.head("Select hardware report") WINDOW_DEFAULT_SIZE = (1200, 800)
print("")
if os.name == "nt":
print("\033[1;93mNote:\033[0m") class OCS(FluentWindow):
print("- Ensure you are using the latest version of Hardware Sniffer before generating the hardware report.") open_result_folder_signal = pyqtSignal(str)
print("- Hardware Sniffer will not collect information related to Resizable BAR option of GPU (disabled by default) and monitor connections in Windows PE.")
print("") PLATFORM_FONTS = {
print("E. Export hardware report (Recommended)") "Windows": "Segoe UI",
print("") "Darwin": "SF Pro Display",
print("Q. Quit") "Linux": "Ubuntu"
print("") }
def __init__(self, backend):
super().__init__()
self.backend = backend
self.settings = self.backend.settings
self.ui_utils = ui_utils.UIUtils()
user_input = self.u.request_input("Drag and drop your hardware report here (.JSON){}: ".format(" or type \"E\" to export" if os.name == "nt" else "")) self._init_state()
if user_input.lower() == "q": self._setup_window()
self.u.exit_program() self._connect_signals()
if user_input.lower() == "e": self._setup_backend_handlers()
hardware_sniffer = self.o.gather_hardware_sniffer() self.init_navigation()
if not hardware_sniffer: def _init_state(self):
continue self.hardware_state = HardwareReportState()
self.macos_state = macOSVersionState()
self.smbios_state = SMBIOSState()
self.build_state = BuildState()
self.build_btn = None
self.progress_bar = None
self.progress_label = None
self.build_log = None
self.open_result_btn = None
report_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "SysReport") def _setup_window(self):
self.setWindowTitle("OpCore Simplify")
self.setMinimumSize(*WINDOW_MIN_SIZE)
self._restore_window_geometry()
self.u.head("Exporting Hardware Report") font = QFont()
print("") system = platform.system()
print("Exporting hardware report to {}...".format(report_dir)) font_family = self.PLATFORM_FONTS.get(system, "Ubuntu")
font.setFamily(font_family)
output = self.r.run({ font.setStyleHint(QFont.StyleHint.SansSerif)
"args":[hardware_sniffer, "-e", "-o", report_dir] self.setFont(font)
})
def _restore_window_geometry(self):
if output[-1] != 0: saved_geometry = self.settings.get("window_geometry")
error_code = output[-1]
if error_code == 3: if saved_geometry and isinstance(saved_geometry, dict):
error_message = "Error collecting hardware." x = saved_geometry.get("x")
elif error_code == 4: y = saved_geometry.get("y")
error_message = "Error generating hardware report." width = saved_geometry.get("width", WINDOW_DEFAULT_SIZE[0])
elif error_code == 5: height = saved_geometry.get("height", WINDOW_DEFAULT_SIZE[1])
error_message = "Error dumping ACPI tables."
else:
error_message = "Unknown error."
print("")
print("Could not export the hardware report. {}".format(error_message))
print("Please try again or using Hardware Sniffer manually.")
print("")
self.u.request_input()
continue
else:
report_path = os.path.join(report_dir, "Report.json")
acpitables_dir = os.path.join(report_dir, "ACPI")
report_data = self.u.read_file(report_path)
self.ac.read_acpi_tables(acpitables_dir)
return report_path, report_data
path = self.u.normalize_path(user_input)
is_valid, errors, warnings, data = self.v.validate_report(path) if x is not None and y is not None:
screen = QApplication.primaryScreen()
if screen:
screen_geometry = screen.availableGeometry()
if (screen_geometry.left() <= x <= screen_geometry.right() and
screen_geometry.top() <= y <= screen_geometry.bottom()):
self.setGeometry(x, y, width, height)
return
self._center_window()
def _center_window(self):
screen = QApplication.primaryScreen()
if screen:
screen_geometry = screen.availableGeometry()
window_width = WINDOW_DEFAULT_SIZE[0]
window_height = WINDOW_DEFAULT_SIZE[1]
self.v.show_validation_report(path, is_valid, errors, warnings) x = screen_geometry.left() + (screen_geometry.width() - window_width) // 2
if not is_valid or errors: y = screen_geometry.top() + (screen_geometry.height() - window_height) // 2
print("")
print("\033[32mSuggestion:\033[0m Please re-export the hardware report and try again.")
print("")
self.u.request_input("Press Enter to go back...")
else:
return path, data
def show_oclp_warning(self): self.setGeometry(x, y, window_width, window_height)
while True: else:
self.u.head("OpenCore Legacy Patcher Warning") self.resize(*WINDOW_DEFAULT_SIZE)
print("")
print("1. OpenCore Legacy Patcher is the only solution to enable dropped GPU and Broadcom WiFi") def _save_window_geometry(self):
print(" support in newer macOS versions, as well as to bring back AppleHDA for macOS Tahoe 26.") geometry = self.geometry()
print("") window_geometry = {
print("2. OpenCore Legacy Patcher disables macOS security features including SIP and AMFI, which may") "x": geometry.x(),
print(" lead to issues such as requiring full installers for updates, application crashes, and") "y": geometry.y(),
print(" system instability.") "width": geometry.width(),
print("") "height": geometry.height()
print("3. OpenCore Legacy Patcher is not officially supported for Hackintosh community.") }
print("") self.settings.set("window_geometry", window_geometry)
print("\033[1;91mImportant:\033[0m")
print("Please consider these risks carefully before proceeding.") def closeEvent(self, event):
print("") self._save_window_geometry()
print("\033[1;96mSupport for macOS Tahoe 26:\033[0m") super().closeEvent(event)
print("To patch macOS Tahoe 26, you must download OpenCore-Patcher 3.0.0 or newer from")
print("my repository: \033[4mlzhoang2801/OpenCore-Legacy-Patcher\033[0m on GitHub.") def _connect_signals(self):
print("Older or official Dortania releases are NOT supported for Tahoe 26.") self.backend.log_message_signal.connect(
print("") lambda message, level, to_build_log: (
option = self.u.request_input("Do you want to continue with OpenCore Legacy Patcher? (yes/No): ").strip().lower() [
if option == "yes": self.build_log.append(line)
return True for line in (message.splitlines() or [""])
elif option == "no": ]
if to_build_log and getattr(self, "build_log", None) else None
)
)
self.backend.update_status_signal.connect(self.update_status)
self.open_result_folder_signal.connect(self._handle_open_result_folder)
def _setup_backend_handlers(self):
self.backend.u.gui_handler = self
set_default_gui_handler(self)
def init_navigation(self):
self.homePage = HomePage(self, ui_utils_instance=self.ui_utils)
self.SelectHardwareReportPage = SelectHardwareReportPage(self, ui_utils_instance=self.ui_utils)
self.compatibilityPage = CompatibilityPage(self, ui_utils_instance=self.ui_utils)
self.configurationPage = ConfigurationPage(self, ui_utils_instance=self.ui_utils)
self.buildPage = BuildPage(self, ui_utils_instance=self.ui_utils)
self.settingsPage = SettingsPage(self)
self.addSubInterface(
self.homePage,
FluentIcon.HOME,
"Home",
NavigationItemPosition.TOP
)
self.addSubInterface(
self.SelectHardwareReportPage,
FluentIcon.FOLDER_ADD,
"1. Select Hardware Report",
NavigationItemPosition.TOP
)
self.addSubInterface(
self.compatibilityPage,
FluentIcon.CHECKBOX,
"2. Check Compatibility",
NavigationItemPosition.TOP
)
self.addSubInterface(
self.configurationPage,
FluentIcon.EDIT,
"3. Configure OpenCore EFI",
NavigationItemPosition.TOP
)
self.addSubInterface(
self.buildPage,
FluentIcon.DEVELOPER_TOOLS,
"4. Build & Review",
NavigationItemPosition.TOP
)
self.navigationInterface.addSeparator()
self.addSubInterface(
self.settingsPage,
FluentIcon.SETTING,
"Settings",
NavigationItemPosition.BOTTOM
)
def _handle_open_result_folder(self, folder_path):
self.backend.u.open_folder(folder_path)
def update_status(self, message, status_type="INFO"):
if status_type == "success":
InfoBar.success(
title="Success",
content=message,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000,
parent=self
)
elif status_type == "ERROR":
InfoBar.error(
title="ERROR",
content=message,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=5000,
parent=self
)
elif status_type == "WARNING":
InfoBar.warning(
title="WARNING",
content=message,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=4000,
parent=self
)
else:
InfoBar.info(
title="INFO",
content=message,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000,
parent=self
)
def validate_prerequisites(self, require_hardware_report=True, require_dsdt=True, require_darwin_version=True, check_compatibility_error=True, require_customized_hardware=True, show_status=True):
if require_hardware_report:
if not self.hardware_state.hardware_report:
if show_status:
self.update_status("Please select hardware report first", "WARNING")
return False
if require_dsdt:
if not self.backend.ac._ensure_dsdt():
if show_status:
self.update_status("Please load ACPI tables first", "WARNING")
return False
if check_compatibility_error:
if self.hardware_state.compatibility_error:
if show_status:
self.update_status("Incompatible hardware detected, please select different hardware report and try again", "WARNING")
return False
if require_darwin_version:
if not self.macos_state.darwin_version:
if show_status:
self.update_status("Please select target macOS version first", "WARNING")
return False return False
def select_macos_version(self, hardware_report, native_macos_version, ocl_patched_macos_version): if require_customized_hardware:
suggested_macos_version = native_macos_version[1] if not self.hardware_state.customized_hardware:
version_pattern = re.compile(r'^(\d+)(?:\.(\d+)(?:\.(\d+))?)?$') if show_status:
self.update_status("Please reload hardware report and select target macOS version to continue", "WARNING")
for device_type in ("GPU", "Network", "Bluetooth", "SD Controller"): return False
if device_type in hardware_report:
for device_name, device_props in hardware_report[device_type].items():
if device_props.get("Compatibility", (None, None)) != (None, None):
if device_type == "GPU" and device_props.get("Device Type") == "Integrated GPU":
device_id = device_props.get("Device ID", ""*8)[5:]
if device_props.get("Manufacturer") == "AMD" or device_id.startswith(("59", "87C0")):
suggested_macos_version = "22.99.99"
elif device_id.startswith(("09", "19")):
suggested_macos_version = "21.99.99"
if self.u.parse_darwin_version(suggested_macos_version) > self.u.parse_darwin_version(device_props.get("Compatibility")[0]):
suggested_macos_version = device_props.get("Compatibility")[0]
while True:
if "Beta" in os_data.get_macos_name_by_darwin(suggested_macos_version):
suggested_macos_version = "{}{}".format(int(suggested_macos_version[:2]) - 1, suggested_macos_version[2:])
else:
break
while True:
self.u.head("Select macOS Version")
if native_macos_version[1][:2] != suggested_macos_version[:2]:
print("")
print("\033[1;36mSuggested macOS version:\033[0m")
print("- For better compatibility and stability, we suggest you to use only {} or older.".format(os_data.get_macos_name_by_darwin(suggested_macos_version)))
print("")
print("Available macOS versions:")
print("")
oclp_min = int(ocl_patched_macos_version[-1][:2]) if ocl_patched_macos_version else 99
oclp_max = int(ocl_patched_macos_version[0][:2]) if ocl_patched_macos_version else 0
min_version = min(int(native_macos_version[0][:2]), oclp_min)
max_version = max(int(native_macos_version[-1][:2]), oclp_max)
for darwin_version in range(min_version, max_version + 1):
name = os_data.get_macos_name_by_darwin(str(darwin_version))
label = " (\033[1;93mRequires OpenCore Legacy Patcher\033[0m)" if oclp_min <= darwin_version <= oclp_max else ""
print(" {}. {}{}".format(darwin_version, name, label))
print("")
print("\033[1;93mNote:\033[0m")
print("- To select a major version, enter the number (e.g., 19).")
print("- To specify a full version, use the Darwin version format (e.g., 22.4.6).")
print("")
print("Q. Quit")
print("")
option = self.u.request_input("Please enter the macOS version you want to use (default: {}): ".format(os_data.get_macos_name_by_darwin(suggested_macos_version))) or suggested_macos_version
if option.lower() == "q":
self.u.exit_program()
match = version_pattern.match(option)
if match:
target_version = "{}.{}.{}".format(match.group(1), match.group(2) if match.group(2) else 99, match.group(3) if match.group(3) else 99)
if ocl_patched_macos_version and self.u.parse_darwin_version(ocl_patched_macos_version[-1]) <= self.u.parse_darwin_version(target_version) <= self.u.parse_darwin_version(ocl_patched_macos_version[0]):
return target_version
elif self.u.parse_darwin_version(native_macos_version[0]) <= self.u.parse_darwin_version(target_version) <= self.u.parse_darwin_version(native_macos_version[-1]):
return target_version
def build_opencore_efi(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp):
steps = [
"Copying EFI base to results folder",
"Applying ACPI patches",
"Copying kexts and snapshotting to config.plist",
"Generating config.plist",
"Cleaning up unused drivers, resources, and tools"
]
title = "Building OpenCore EFI" return True
self.u.progress_bar(title, steps, 0) def apply_macos_version(self, version):
self.u.create_folder(self.result_dir, remove_content=True) self.macos_state.darwin_version = version
self.macos_state.selected_version_name = os_data.get_macos_name_by_darwin(version)
if not os.path.exists(self.k.ock_files_dir): self.hardware_state.customized_hardware, self.hardware_state.disabled_devices, self.macos_state.needs_oclp = self.backend.h.hardware_customization(self.hardware_state.hardware_report, version)
raise Exception("Directory '{}' does not exist.".format(self.k.ock_files_dir))
self.smbios_state.model_name = self.backend.s.select_smbios_model(self.hardware_state.customized_hardware, version)
source_efi_dir = os.path.join(self.k.ock_files_dir, "OpenCorePkg") self.backend.ac.select_acpi_patches(self.hardware_state.customized_hardware, self.hardware_state.disabled_devices)
shutil.copytree(source_efi_dir, self.result_dir, dirs_exist_ok=True)
config_file = os.path.join(self.result_dir, "EFI", "OC", "config.plist")
config_data = self.u.read_file(config_file)
if not config_data: self.macos_state.needs_oclp, audio_layout_id, audio_controller_properties = self.backend.k.select_required_kexts(self.hardware_state.customized_hardware, version, self.macos_state.needs_oclp, self.backend.ac.patches)
raise Exception("Error: The file {} does not exist.".format(config_file))
self.u.progress_bar(title, steps, 1) if audio_layout_id is not None:
config_data["ACPI"]["Add"] = [] self.hardware_state.audio_layout_id = audio_layout_id
config_data["ACPI"]["Delete"] = [] self.hardware_state.audio_controller_properties = audio_controller_properties
config_data["ACPI"]["Patch"] = []
if self.ac.ensure_dsdt():
self.ac.hardware_report = hardware_report
self.ac.disabled_devices = disabled_devices
self.ac.acpi_directory = os.path.join(self.result_dir, "EFI", "OC", "ACPI")
self.ac.smbios_model = smbios_model
self.ac.lpc_bus_device = self.ac.get_lpc_name()
for patch in self.ac.patches: self.backend.s.smbios_specific_options(self.hardware_state.customized_hardware, self.smbios_state.model_name, version, self.backend.ac.patches, self.backend.k)
if patch.checked:
if patch.name == "BATP":
patch.checked = getattr(self.ac, patch.function_name)()
self.k.kexts[kext_maestro.kext_data.kext_index_by_name.get("ECEnabler")].checked = patch.checked
continue
acpi_load = getattr(self.ac, patch.function_name)() self.configurationPage.update_display()
if not isinstance(acpi_load, dict): def setup_exception_hook(self):
continue def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
config_data["ACPI"]["Add"].extend(acpi_load.get("Add", [])) error_details = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
config_data["ACPI"]["Delete"].extend(acpi_load.get("Delete", [])) error_message = "Uncaught exception detected:\n{}".format(error_details)
config_data["ACPI"]["Patch"].extend(acpi_load.get("Patch", []))
self.backend.u.log_message(error_message, level="ERROR")
config_data["ACPI"]["Patch"].extend(self.ac.dsdt_patches)
config_data["ACPI"]["Patch"] = self.ac.apply_acpi_patches(config_data["ACPI"]["Patch"])
self.u.progress_bar(title, steps, 2)
kexts_directory = os.path.join(self.result_dir, "EFI", "OC", "Kexts")
self.k.install_kexts_to_efi(macos_version, kexts_directory)
config_data["Kernel"]["Add"] = self.k.load_kexts(hardware_report, macos_version, kexts_directory)
self.u.progress_bar(title, steps, 3)
self.co.genarate(hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp, self.k.kexts, config_data)
self.u.write_file(config_file, config_data)
self.u.progress_bar(title, steps, 4)
files_to_remove = []
drivers_directory = os.path.join(self.result_dir, "EFI", "OC", "Drivers")
driver_list = self.u.find_matching_paths(drivers_directory, extension_filter=".efi")
driver_loaded = [kext.get("Path") for kext in config_data.get("UEFI").get("Drivers")]
for driver_path, type in driver_list:
if not driver_path in driver_loaded:
files_to_remove.append(os.path.join(drivers_directory, driver_path))
resources_audio_dir = os.path.join(self.result_dir, "EFI", "OC", "Resources", "Audio")
if os.path.exists(resources_audio_dir):
files_to_remove.append(resources_audio_dir)
picker_variant = config_data.get("Misc", {}).get("Boot", {}).get("PickerVariant")
if picker_variant in (None, "Auto"):
picker_variant = "Acidanthera/GoldenGate"
if os.name == "nt":
picker_variant = picker_variant.replace("/", "\\")
resources_image_dir = os.path.join(self.result_dir, "EFI", "OC", "Resources", "Image")
available_picker_variants = self.u.find_matching_paths(resources_image_dir, type_filter="dir")
for variant_name, variant_type in available_picker_variants:
variant_path = os.path.join(resources_image_dir, variant_name)
if ".icns" in ", ".join(os.listdir(variant_path)):
if picker_variant not in variant_name:
files_to_remove.append(variant_path)
tools_directory = os.path.join(self.result_dir, "EFI", "OC", "Tools")
tool_list = self.u.find_matching_paths(tools_directory, extension_filter=".efi")
tool_loaded = [tool.get("Path") for tool in config_data.get("Misc").get("Tools")]
for tool_path, type in tool_list:
if not tool_path in tool_loaded:
files_to_remove.append(os.path.join(tools_directory, tool_path))
if "manifest.json" in os.listdir(self.result_dir):
files_to_remove.append(os.path.join(self.result_dir, "manifest.json"))
for file_path in files_to_remove:
try: try:
if os.path.isdir(file_path): sys.__stderr__.write("\n[CRITICAL ERROR] {}\n".format(error_message))
shutil.rmtree(file_path) except:
else: pass
os.remove(file_path)
except Exception as e:
print("Failed to remove file: {}".format(e))
self.u.progress_bar(title, steps, len(steps), done=True)
print("OpenCore EFI build complete.")
time.sleep(2)
def check_bios_requirements(self, org_hardware_report, hardware_report):
requirements = []
org_firmware_type = org_hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown")
firmware_type = hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown")
if org_firmware_type == "Legacy" and firmware_type == "UEFI":
requirements.append("Enable UEFI mode (disable Legacy/CSM (Compatibility Support Module))")
secure_boot = hardware_report.get("BIOS", {}).get("Secure Boot", "Unknown") sys.excepthook = handle_exception
if secure_boot != "Disabled":
requirements.append("Disable Secure Boot")
if hardware_report.get("Motherboard", {}).get("Platform") == "Desktop" and hardware_report.get("Motherboard", {}).get("Chipset") in chipset_data.IntelChipsets[112:]:
resizable_bar_enabled = any(gpu_props.get("Resizable BAR", "Disabled") == "Enabled" for gpu_props in hardware_report.get("GPU", {}).values())
if not resizable_bar_enabled:
requirements.append("Enable Above 4G Decoding")
requirements.append("Disable Resizable BAR/Smart Access Memory")
return requirements
def before_using_efi(self, org_hardware_report, hardware_report):
while True:
self.u.head("Before Using EFI")
print("")
print("\033[93mPlease complete the following steps:\033[0m")
print("")
bios_requirements = self.check_bios_requirements(org_hardware_report, hardware_report)
if bios_requirements:
print("* BIOS/UEFI Settings Required:")
for requirement in bios_requirements:
print(" - {}".format(requirement))
print("")
print("* USB Mapping:")
print(" - Use USBToolBox tool to map USB ports.")
print(" - Add created UTBMap.kext into the {} folder.".format("EFI\\OC\\Kexts" if os.name == "nt" else "EFI/OC/Kexts"))
print(" - Remove UTBDefault.kext in the {} folder.".format("EFI\\OC\\Kexts" if os.name == "nt" else "EFI/OC/Kexts"))
print(" - Edit config.plist:")
print(" - Use ProperTree to open your config.plist.")
print(" - Run OC Snapshot by pressing Command/Ctrl + R.")
print(" - If you have more than 15 ports on a single controller, enable the XhciPortLimit patch.")
print(" - Save the file when finished.")
print("")
print("Type \"AGREE\" to open the built EFI for you\n")
response = self.u.request_input("")
if response.lower() == "agree":
self.u.open_folder(self.result_dir)
break
else:
print("\033[91mInvalid input. Please try again.\033[0m")
def main(self): if __name__ == "__main__":
hardware_report_path = None backend = Backend()
native_macos_version = None
disabled_devices = None app = QApplication(sys.argv)
macos_version = None set_default_gui_handler(app)
ocl_patched_macos_version = None
needs_oclp = False window = OCS(backend)
smbios_model = None window.setup_exception_hook()
window.show()
while True:
self.u.head() if backend.settings.get_auto_update_check():
print("") updater.Updater(
print(" Hardware Report: {}".format(hardware_report_path or 'Not selected')) utils_instance=backend.u,
if hardware_report_path: github_instance=backend.github,
print("") resource_fetcher_instance=backend.resource_fetcher,
print(" macOS Version: {}".format(os_data.get_macos_name_by_darwin(macos_version) if macos_version else 'Not selected') + (' (' + macos_version + ')' if macos_version else '') + ('. \033[1;93mRequires OpenCore Legacy Patcher\033[0m' if needs_oclp else '')) run_instance=backend.r,
print(" SMBIOS: {}".format(smbios_model or 'Not selected')) integrity_checker_instance=backend.integrity_checker
if disabled_devices: ).run_update()
print(" Disabled Devices:")
for device, _ in disabled_devices.items(): sys.exit(app.exec())
print(" - {}".format(device))
print("")
print("1. Select Hardware Report")
print("2. Select macOS Version")
print("3. Customize ACPI Patch")
print("4. Customize Kexts")
print("5. Customize SMBIOS Model")
print("6. Build OpenCore EFI")
print("")
print("Q. Quit")
print("")
option = self.u.request_input("Select an option: ")
if option.lower() == "q":
self.u.exit_program()
if option == "1":
hardware_report_path, hardware_report = self.select_hardware_report()
hardware_report, native_macos_version, ocl_patched_macos_version = self.c.check_compatibility(hardware_report)
macos_version = self.select_macos_version(hardware_report, native_macos_version, ocl_patched_macos_version)
customized_hardware, disabled_devices, needs_oclp = self.h.hardware_customization(hardware_report, macos_version)
smbios_model = self.s.select_smbios_model(customized_hardware, macos_version)
if not self.ac.ensure_dsdt():
self.ac.select_acpi_tables()
self.ac.select_acpi_patches(customized_hardware, disabled_devices)
needs_oclp = self.k.select_required_kexts(customized_hardware, macos_version, needs_oclp, self.ac.patches)
self.s.smbios_specific_options(customized_hardware, smbios_model, macos_version, self.ac.patches, self.k)
if not hardware_report_path:
self.u.head()
print("\n\n")
print("\033[1;93mPlease select a hardware report first.\033[0m")
print("\n\n")
self.u.request_input("Press Enter to go back...")
continue
if option == "2":
macos_version = self.select_macos_version(hardware_report, native_macos_version, ocl_patched_macos_version)
customized_hardware, disabled_devices, needs_oclp = self.h.hardware_customization(hardware_report, macos_version)
smbios_model = self.s.select_smbios_model(customized_hardware, macos_version)
needs_oclp = self.k.select_required_kexts(customized_hardware, macos_version, needs_oclp, self.ac.patches)
self.s.smbios_specific_options(customized_hardware, smbios_model, macos_version, self.ac.patches, self.k)
elif option == "3":
self.ac.customize_patch_selection()
elif option == "4":
self.k.kext_configuration_menu(macos_version)
elif option == "5":
smbios_model = self.s.customize_smbios_model(customized_hardware, smbios_model, macos_version)
self.s.smbios_specific_options(customized_hardware, smbios_model, macos_version, self.ac.patches, self.k)
elif option == "6":
if needs_oclp and not self.show_oclp_warning():
macos_version = self.select_macos_version(hardware_report, native_macos_version, ocl_patched_macos_version)
customized_hardware, disabled_devices, needs_oclp = self.h.hardware_customization(hardware_report, macos_version)
smbios_model = self.s.select_smbios_model(customized_hardware, macos_version)
needs_oclp = self.k.select_required_kexts(customized_hardware, macos_version, needs_oclp, self.ac.patches)
self.s.smbios_specific_options(customized_hardware, smbios_model, macos_version, self.ac.patches, self.k)
continue
try:
self.o.gather_bootloader_kexts(self.k.kexts, macos_version)
except Exception as e:
print("\033[91mError: {}\033[0m".format(e))
print("")
self.u.request_input("Press Enter to continue...")
continue
self.build_opencore_efi(customized_hardware, disabled_devices, smbios_model, macos_version, needs_oclp)
self.before_using_efi(hardware_report, customized_hardware)
self.u.head("Result")
print("")
print("Your OpenCore EFI for {} has been built at:".format(customized_hardware.get("Motherboard").get("Name")))
print("\t{}".format(self.result_dir))
print("")
self.u.request_input("Press Enter to main menu...")
if __name__ == '__main__':
update_flag = updater.Updater().run_update()
if update_flag:
os.execv(sys.executable, ['python3'] + sys.argv)
o = OCPE()
while True:
try:
o.main()
except Exception as e:
o.u.head("An Error Occurred")
print("")
print(traceback.format_exc())
o.u.request_input()

View File

@@ -19,23 +19,6 @@
</p> </p>
</div> </div>
> [!NOTE]
> **OpenCore Legacy Patcher 3.0.0 Now Supports macOS Tahoe 26!**
>
> The long awaited version 3.0.0 of OpenCore Legacy Patcher is here, bringing **initial support for macOS Tahoe 26** to the community!
>
> 🚨 **Please Note:**
> - Only OpenCore-Patcher 3.0.0 **from the [lzhoang2801/OpenCore-Legacy-Patcher](https://github.com/lzhoang2801/OpenCore-Legacy-Patcher/releases/tag/3.0.0)** repository provides support for macOS Tahoe 26 with early patches.
> - Official Dortania releases or older patches **will NOT work** with macOS Tahoe 26.
> [!WARNING]
> While OpCore Simplify significantly reduces setup time, the Hackintosh journey still requires:
> - Understanding basic concepts from the [Dortania Guide](https://dortania.github.io/OpenCore-Install-Guide/)
> - Testing and troubleshooting during the installation process
> - Patience and persistence in resolving any issues that arise
>
> Our tool does not guarantee a successful installation in the first attempt, but it should help you get started.
## ✨ **Features** ## ✨ **Features**
1. **Comprehensive Hardware and macOS Support** 1. **Comprehensive Hardware and macOS Support**
@@ -101,39 +84,30 @@
- On **macOS**, run `OpCore-Simplify.command`. - On **macOS**, run `OpCore-Simplify.command`.
- On **Linux**, run `OpCore-Simplify.py` with existing Python interpreter. - On **Linux**, run `OpCore-Simplify.py` with existing Python interpreter.
![OpCore Simplify Menu](https://i.imgur.com/vTr1V9D.png) ![OpCore Simplify Main](https://private-user-images.githubusercontent.com/169338399/529304376-037b1b04-8f76-4a31-87f2-b2b779ff4cdb.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwMzkxNjUsIm5iZiI6MTc2NzAzODg2NSwicGF0aCI6Ii8xNjkzMzgzOTkvNTI5MzA0Mzc2LTAzN2IxYjA0LThmNzYtNGEzMS04N2YyLWIyYjc3OWZmNGNkYi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjI5JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIyOVQyMDA3NDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT05M2JiZTA0YzE2OWFlNDljZjlmYjI2NDBjZGQ0NGU5Njg1ODMwMDgwN2EyYjcxMmQ5ZDcyODBlN2JjMDFlZDdkJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.yaVVUwu0hf8Q0pZp-cn_8KixBEr0g62lXYGslkYLITc)
3. **Selecting hardware report**: 3. **Selecting hardware report**:
- On Windows, there will be an option for `E. Export hardware report`. It's recommended to use this for the best results with your hardware configuration and BIOS at the time of building.
- Alternatively, use [**Hardware Sniffer**](https://github.com/lzhoang2801/Hardware-Sniffer) to create a `Report.json` and ACPI dump for configuration manully.
![Selecting hardware report](https://i.imgur.com/MbRmIGJ.png) ![Selecting hardware report](https://private-user-images.githubusercontent.com/169338399/529304594-b1e608a7-6428-4f49-8426-f4ad289a7484.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwMzkxNjUsIm5iZiI6MTc2NzAzODg2NSwicGF0aCI6Ii8xNjkzMzgzOTkvNTI5MzA0NTk0LWIxZTYwOGE3LTY0MjgtNGY0OS04NDI2LWY0YWQyODlhNzQ4NC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjI5JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIyOVQyMDA3NDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04ZDA0ZjljMGY4MTc5YmUzNmFmZDNhNDRjYTU5ZDJiZDQ3NzI0ZjkwMDI4ODNjMDhhMTNhZGY1M2Y3MmY5MjZlJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.-9K5kz6tXqrNHK-E4jWyAio481ave9ypIt6-Em_yBJM)
![Loading ACPI Tables](https://i.imgur.com/SbL6N6v.png) 4. **Verifying hardware compatibility**:
![Compatibility Checker](https://i.imgur.com/kuDGMmp.png) ![Compatibility Checker](https://private-user-images.githubusercontent.com/169338399/529304672-72d4ba8c-1d8e-4a59-80e2-23212b3213da.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwMzkxNjUsIm5iZiI6MTc2NzAzODg2NSwicGF0aCI6Ii8xNjkzMzgzOTkvNTI5MzA0NjcyLTcyZDRiYThjLTFkOGUtNGE1OS04MGUyLTIzMjEyYjMyMTNkYS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjI5JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIyOVQyMDA3NDVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0yMTA4OWFjYWM4Zjk0NWRiNDVlZWY5YzYzYTUzNmQwMDk3MzZhMTQ0MGVhN2NkODAxMjcwOTBmY2I2MGZlMzg2JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.dgaX8DZcsK3CbuKOdu62JNOypngycO98ezN5qUxvGNI)
4. **Selecting macOS Version and Customizing OpenCore EFI**: 5. **Selecting macOS Version and Customizing OpenCore EFI**:
- By default, the latest compatible macOS version will be selected for your hardware. - By default, the latest compatible macOS version will be selected for your hardware.
- OpCore Simplify will automatically apply essential ACPI patches and kexts. - OpCore Simplify will automatically apply essential ACPI patches and kexts.
- You can manually review and customize these settings as needed. - You can manually review and customize these settings as needed.
![OpCore Simplify Menu](https://i.imgur.com/TSk9ejy.png) ![Configuration Page](https://private-user-images.githubusercontent.com/169338399/530910046-81462033-696d-46e2-91f2-358ceff37199.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzg5NjQsIm5iZiI6MTc2NzA3ODY2NCwicGF0aCI6Ii8xNjkzMzgzOTkvNTMwOTEwMDQ2LTgxNDYyMDMzLTY5NmQtNDZlMi05MWYyLTM1OGNlZmYzNzE5OS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzExMDRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1lOGNlNzQ4MmE3ZTkzMGI0MDU0MzliZTAyMzI0YzhkZTJjNDkwYjc5NmZmZTA4YjE2NjUwYmUyMWUyMThlYzc1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.XuqqC-VOk4SS9zSCgyGaGfrmNbjDm-MCGiK4l597ink)
5. **Building OpenCore EFI**: 6. **Building OpenCore EFI**:
- Once you've customized all options, select **Build OpenCore EFI** to generate your EFI. - Once you've customized all options, select **Build OpenCore EFI** to generate your EFI.
- The tool will automatically download the necessary bootloader and kexts, which may take a few minutes. - The tool will automatically download the necessary bootloader and kexts, which may take a few minutes.
![WiFi Profile Extractor](https://i.imgur.com/71TkJkD.png) ![OCLP Warning](https://private-user-images.githubusercontent.com/169338399/530910077-88987465-2aab-47b9-adf8-e56f6248c88f.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzg5NjQsIm5iZiI6MTc2NzA3ODY2NCwicGF0aCI6Ii8xNjkzMzgzOTkvNTMwOTEwMDc3LTg4OTg3NDY1LTJhYWItNDdiOS1hZGY4LWU1NmY2MjQ4Yzg4Zi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzExMDRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT01OWUzNGM2MzZmMmMwYjA5OWU0YzYxMTQ0Yjg5M2RkM2QzNDcyZGVlMTVkZWQ1ZTE5OTU5MjYwZGQ0ODVlZGVmJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.twu-QrH30NkDnqSVsvsmySf15ePAWhCStGjZDO3ia40)
![Choosing Codec Layout ID](https://i.imgur.com/Mcm20EQ.png) ![Build Result](https://private-user-images.githubusercontent.com/169338399/530910249-f91813db-b201-4d6a-b604-691014d29074.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzg5NjQsIm5iZiI6MTc2NzA3ODY2NCwicGF0aCI6Ii8xNjkzMzgzOTkvNTMwOTEwMjQ5LWY5MTgxM2RiLWIyMDEtNGQ2YS1iNjA0LTY5MTAxNGQyOTA3NC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzExMDRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mZDVjYjM2MzY2ZGJhMDcxODRlZGUzY2RhNGFjMjYzNGMyYWFiYWVmZGM4YzRmMDlkZTgzNzEwZjRjYWY2MDM1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.uq7WzyDJKImuoUFjgfQG41s4VfgMq7h64BaSjRpU6cg)
![Building OpenCore EFI](https://i.imgur.com/deyj5de.png)
6. **USB Mapping**:
- After building your EFI, follow the steps for mapping USB ports.
![Results](https://i.imgur.com/MIPigPF.png)
7. **Create USB and Install macOS**: 7. **Create USB and Install macOS**:
- Use [**UnPlugged**](https://github.com/corpnewt/UnPlugged) on Windows to create a USB macOS installer, or follow [this guide](https://dortania.github.io/OpenCore-Install-Guide/installer-guide/mac-install.html) for macOS. - Use [**UnPlugged**](https://github.com/corpnewt/UnPlugged) on Windows to create a USB macOS installer, or follow [this guide](https://dortania.github.io/OpenCore-Install-Guide/installer-guide/mac-install.html) for macOS.
@@ -158,6 +132,7 @@ Distributed under the BSD 3-Clause License. See `LICENSE` for more information.
- [OpenCorePkg](https://github.com/acidanthera/OpenCorePkg) and [kexts](https://github.com/lzhoang2801/OpCore-Simplify/blob/main/Scripts/datasets/kext_data.py) The backbone of this project. - [OpenCorePkg](https://github.com/acidanthera/OpenCorePkg) and [kexts](https://github.com/lzhoang2801/OpCore-Simplify/blob/main/Scripts/datasets/kext_data.py) The backbone of this project.
- [SSDTTime](https://github.com/corpnewt/SSDTTime) SSDT patching utilities. - [SSDTTime](https://github.com/corpnewt/SSDTTime) SSDT patching utilities.
- [@rubentalstra](https://github.com/rubentalstra): Idea and code prototype [Implement GUI #471](https://github.com/lzhoang2801/OpCore-Simplify/pull/471)
## 📞 **Contact** ## 📞 **Contact**
@@ -168,4 +143,4 @@ Distributed under the BSD 3-Clause License. See `LICENSE` for more information.
## 🌟 **Star History** ## 🌟 **Star History**
[![Star History Chart](https://api.star-history.com/svg?repos=lzhoang2801/OpCore-Simplify&type=Date)](https://star-history.com/#lzhoang2801/OpCore-Simplify&Date) [![Star History Chart](https://api.star-history.com/svg?repos=lzhoang2801/OpCore-Simplify&type=Date)](https://star-history.com/#lzhoang2801/OpCore-Simplify&Date)

View File

@@ -8,6 +8,7 @@ from Scripts import smbios
from Scripts import dsdt from Scripts import dsdt
from Scripts import run from Scripts import run
from Scripts import utils from Scripts import utils
from Scripts.custom_dialogs import show_checklist_dialog
import os import os
import binascii import binascii
import re import re
@@ -17,11 +18,11 @@ import sys
import plistlib import plistlib
class ACPIGuru: class ACPIGuru:
def __init__(self): def __init__(self, dsdt_instance=None, smbios_instance=None, run_instance=None, utils_instance=None):
self.acpi = dsdt.DSDT() self.acpi = dsdt_instance if dsdt_instance else dsdt.DSDT()
self.smbios = smbios.SMBIOS() self.smbios = smbios_instance if smbios_instance else smbios.SMBIOS()
self.run = run.Run().run self.run = run_instance.run if run_instance else run.Run().run
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
self.patches = acpi_patch_data.patches self.patches = acpi_patch_data.patches
self.hardware_report = None self.hardware_report = None
self.disabled_devices = None self.disabled_devices = None
@@ -118,9 +119,7 @@ class ACPIGuru:
def read_acpi_tables(self, path): def read_acpi_tables(self, path):
if not path: if not path:
return return
self.utils.head("Loading ACPI Table(s)") self.utils.log_message("[ACPI GURU] Loading ACPI Table(s) from {}".format(path), level="INFO")
print("by CorpNewt")
print("")
tables = [] tables = []
trouble_dsdt = None trouble_dsdt = None
fixed = False fixed = False
@@ -129,10 +128,10 @@ class ACPIGuru:
# Clear any existing tables so we load anew # Clear any existing tables so we load anew
self.acpi.acpi_tables = {} self.acpi.acpi_tables = {}
if os.path.isdir(path): if os.path.isdir(path):
print("Gathering valid tables from {}...\n".format(os.path.basename(path))) self.utils.log_message("[ACPI GURU] Gathering valid tables from {}".format(os.path.basename(path)), level="INFO")
for t in self.sorted_nicely(os.listdir(path)): for t in self.sorted_nicely(os.listdir(path)):
if not "Patched" in t and self.acpi.table_is_valid(path,t): if not "Patched" in t and self.acpi.table_is_valid(path,t):
print(" - {}".format(t)) self.utils.log_message("[ACPI GURU] Found valid table: {}".format(t), level="INFO")
tables.append(t) tables.append(t)
if not tables: if not tables:
# Check if there's an ACPI directory within the passed # Check if there's an ACPI directory within the passed
@@ -140,50 +139,44 @@ class ACPIGuru:
if os.path.isdir(os.path.join(path,"ACPI")): if os.path.isdir(os.path.join(path,"ACPI")):
# Rerun this function with that updated path # Rerun this function with that updated path
return self.read_acpi_tables(os.path.join(path,"ACPI")) return self.read_acpi_tables(os.path.join(path,"ACPI"))
print(" - No valid .aml files were found!") self.utils.log_message("[ACPI GURU] No valid .aml files were found!", level="ERROR")
print("")
#self.u.grab("Press [enter] to return...") #self.u.grab("Press [enter] to return...")
self.utils.request_input()
# Restore any prior tables # Restore any prior tables
self.acpi.acpi_tables = prior_tables self.acpi.acpi_tables = prior_tables
return return
print("") self.utils.log_message("[ACPI GURU] Found at least one valid table", level="INFO")
# We got at least one file - let's look for the DSDT specifically # We got at least one file - let's look for the DSDT specifically
# and try to load that as-is. If it doesn't load, we'll have to # and try to load that as-is. If it doesn't load, we'll have to
# manage everything with temp folders # manage everything with temp folders
dsdt_list = [x for x in tables if self.acpi._table_signature(path,x) == "DSDT"] dsdt_list = [x for x in tables if self.acpi._table_signature(path,x) == "DSDT"]
if len(dsdt_list) > 1: if len(dsdt_list) > 1:
print("Multiple files with DSDT signature passed:") self.utils.log_message("[ACPI GURU] Multiple files with DSDT signature passed:", level="ERROR")
for d in self.sorted_nicely(dsdt_list): for d in self.sorted_nicely(dsdt_list):
print(" - {}".format(d)) self.utils.log_message("[ACPI GURU] Found DSDT file: {}".format(d), level="INFO")
print("\nOnly one is allowed at a time. Please remove one of the above and try again.") self.utils.log_message("[ACPI GURU] Only one is allowed at a time. Please remove one of the above and try again.", level="ERROR")
print("")
#self.u.grab("Press [enter] to return...") #self.u.grab("Press [enter] to return...")
self.utils.request_input()
# Restore any prior tables # Restore any prior tables
self.acpi.acpi_tables = prior_tables self.acpi.acpi_tables = prior_tables
return return
# Get the DSDT, if any # Get the DSDT, if any
dsdt = dsdt_list[0] if len(dsdt_list) else None dsdt = dsdt_list[0] if len(dsdt_list) else None
if dsdt: # Try to load it and see if it causes problems if dsdt: # Try to load it and see if it causes problems
print("Disassembling {} to verify if pre-patches are needed...".format(dsdt)) self.utils.log_message("[ACPI GURU] Disassembling {} to verify if pre-patches are needed...".format(dsdt), level="INFO")
if not self.acpi.load(os.path.join(path,dsdt))[0]: if not self.acpi.load(os.path.join(path,dsdt))[0]:
trouble_dsdt = dsdt trouble_dsdt = dsdt
else: else:
print("\nDisassembled successfully!\n") self.utils.log_message("[ACPI GURU] Disassembled successfully!", level="INFO")
elif not "Patched" in path and os.path.isfile(path): elif not "Patched" in path and os.path.isfile(path):
print("Loading {}...".format(os.path.basename(path))) self.utils.log_message("[ACPI GURU] Loading {}...".format(os.path.basename(path)), level="INFO")
if self.acpi.load(path)[0]: if self.acpi.load(path)[0]:
print("\nDone.") self.utils.log_message("[ACPI GURU] Done.", level="INFO")
# If it loads fine - just return the path # If it loads fine - just return the path
# to the parent directory # to the parent directory
return os.path.dirname(path) return os.path.dirname(path)
if not self.acpi._table_signature(path) == "DSDT": if not self.acpi._table_signature(path) == "DSDT":
# Not a DSDT, we aren't applying pre-patches # Not a DSDT, we aren't applying pre-patches
print("\n{} could not be disassembled!".format(os.path.basename(path))) self.utils.log_message("[ACPI GURU] {} could not be disassembled!".format(os.path.basename(path)), level="ERROR")
print("")
#self.u.grab("Press [enter] to return...") #self.u.grab("Press [enter] to return...")
self.utils.request_input()
# Restore any prior tables # Restore any prior tables
self.acpi.acpi_tables = prior_tables self.acpi.acpi_tables = prior_tables
return return
@@ -194,10 +187,8 @@ class ACPIGuru:
tables.append(os.path.basename(path)) tables.append(os.path.basename(path))
path = os.path.dirname(path) path = os.path.dirname(path)
else: else:
print("Passed file/folder does not exist!") self.utils.log_message("[ACPI GURU] Passed file/folder does not exist!", level="ERROR")
print("")
#self.u.grab("Press [enter] to return...") #self.u.grab("Press [enter] to return...")
self.utils.request_input()
# Restore any prior tables # Restore any prior tables
self.acpi.acpi_tables = prior_tables self.acpi.acpi_tables = prior_tables
return return
@@ -214,22 +205,22 @@ class ACPIGuru:
# Get a reference to the new trouble file # Get a reference to the new trouble file
trouble_path = os.path.join(temp,trouble_dsdt) trouble_path = os.path.join(temp,trouble_dsdt)
# Now we try patching it # Now we try patching it
print("Checking available pre-patches...") self.utils.log_message("[ACPI GURU] Checking available pre-patches...", level="INFO")
print("Loading {} into memory...".format(trouble_dsdt)) self.utils.log_message("[ACPI GURU] Loading {} into memory...".format(trouble_dsdt), level="INFO")
with open(trouble_path,"rb") as f: with open(trouble_path,"rb") as f:
d = f.read() d = f.read()
res = self.acpi.check_output(path) res = self.acpi.check_output(path)
target_name = self.get_unique_name(trouble_dsdt,res,name_append="-Patched") target_name = self.get_unique_name(trouble_dsdt,res,name_append="-Patched")
self.dsdt_patches = [] self.dsdt_patches = []
print("Iterating patches...\n") self.utils.log_message("[ACPI GURU] Iterating patches...", level="INFO")
for p in self.pre_patches: for p in self.pre_patches:
if not all(x in p for x in ("PrePatch","Comment","Find","Replace")): continue if not all(x in p for x in ("PrePatch","Comment","Find","Replace")): continue
print(" - {}".format(p["PrePatch"])) self.utils.log_message("[ACPI GURU] Found pre-patch: {}".format(p["PrePatch"]), level="INFO")
find = binascii.unhexlify(p["Find"]) find = binascii.unhexlify(p["Find"])
if d.count(find) == 1: if d.count(find) == 1:
self.dsdt_patches.append(p) # Retain the patch self.dsdt_patches.append(p) # Retain the patch
repl = binascii.unhexlify(p["Replace"]) repl = binascii.unhexlify(p["Replace"])
print(" --> Located - applying...") self.utils.log_message("[ACPI GURU] Located pre-patch - applying...", level="INFO")
d = d.replace(find,repl) # Replace it in memory d = d.replace(find,repl) # Replace it in memory
with open(trouble_path,"wb") as f: with open(trouble_path,"wb") as f:
f.write(d) # Write the updated file f.write(d) # Write the updated file
@@ -237,7 +228,7 @@ class ACPIGuru:
if self.acpi.load(trouble_path)[0]: if self.acpi.load(trouble_path)[0]:
fixed = True fixed = True
# We got it to load - let's write the patches # We got it to load - let's write the patches
print("\nDisassembled successfully!\n") self.utils.log_message("[ACPI GURU] Disassembled successfully!", level="INFO")
#self.make_plist(None, None, patches) #self.make_plist(None, None, patches)
# Save to the local file # Save to the local file
#with open(os.path.join(res,target_name),"wb") as f: #with open(os.path.join(res,target_name),"wb") as f:
@@ -246,10 +237,8 @@ class ACPIGuru:
#self.patch_warn() #self.patch_warn()
break break
if not fixed: if not fixed:
print("\n{} could not be disassembled!".format(trouble_dsdt)) self.utils.log_message("[ACPI GURU] {} could not be disassembled!".format(trouble_dsdt), level="ERROR")
print("")
#self.u.grab("Press [enter] to return...") #self.u.grab("Press [enter] to return...")
self.utils.request_input()
if temp: if temp:
shutil.rmtree(temp,ignore_errors=True) shutil.rmtree(temp,ignore_errors=True)
# Restore any prior tables # Restore any prior tables
@@ -257,26 +246,26 @@ class ACPIGuru:
return return
# Let's load the rest of the tables # Let's load the rest of the tables
if len(tables) > 1: if len(tables) > 1:
print("Loading valid tables in {}...".format(path)) self.utils.log_message("[ACPI GURU] Loading valid tables in {}...".format(path), level="INFO")
loaded_tables,failed = self.acpi.load(temp or path) loaded_tables,failed = self.acpi.load(temp or path)
if not loaded_tables or failed: if not loaded_tables or failed:
print("\nFailed to load tables in {}{}\n".format( self.utils.log_message("[ACPI GURU] Failed to load tables in {}{}\n".format(
os.path.dirname(path) if os.path.isfile(path) else path, os.path.dirname(path) if os.path.isfile(path) else path,
":" if failed else "" ":" if failed else ""
)) ))
for t in self.sorted_nicely(failed): for t in self.sorted_nicely(failed):
print(" - {}".format(t)) self.utils.log_message("[ACPI GURU] Failed to load table: {}".format(t), level="ERROR")
# Restore any prior tables # Restore any prior tables
if not loaded_tables: if not loaded_tables:
self.acpi.acpi_tables = prior_tables self.acpi.acpi_tables = prior_tables
else: else:
if len(tables) > 1: #if len(tables) > 1:
print("") # Newline for readability # print("") # Newline for readability
print("Done.") self.utils.log_message("[ACPI GURU] Done.", level="INFO")
# If we had to patch the DSDT, or if not all tables loaded, # If we had to patch the DSDT, or if not all tables loaded,
# make sure we get interaction from the user to continue # make sure we get interaction from the user to continue
if trouble_dsdt or not loaded_tables or failed: if trouble_dsdt or not loaded_tables or failed:
print("") pass
#self.u.grab("Press [enter] to return...") #self.u.grab("Press [enter] to return...")
#self.utils.request_input() #self.utils.request_input()
if temp: if temp:
@@ -293,7 +282,7 @@ class ACPIGuru:
# Got it already # Got it already
return True return True
# Need to prompt # Need to prompt
self.select_acpi_tables() #self.select_acpi_tables()
self.dsdt = self.acpi.get_dsdt_or_only() self.dsdt = self.acpi.get_dsdt_or_only()
if self._ensure_dsdt(allow_any=allow_any): if self._ensure_dsdt(allow_any=allow_any):
return True return True
@@ -3214,20 +3203,6 @@ DefinitionBlock ("", "SSDT", 2, "ZPSS", "WMIS", 0x00000000)
"Delete": deletes "Delete": deletes
} }
def select_acpi_tables(self):
while True:
self.utils.head("Select ACPI Tables")
print("")
print("Q. Quit")
print(" ")
menu = self.utils.request_input("Please drag and drop ACPI Tables folder here: ")
if menu.lower() == "q":
self.utils.exit_program()
path = self.utils.normalize_path(menu)
if not path:
continue
return self.read_acpi_tables(path)
def get_patch_index(self, name): def get_patch_index(self, name):
for index, patch in enumerate(self.patches): for index, patch in enumerate(self.patches):
if patch.name == name: if patch.name == name:
@@ -3235,6 +3210,7 @@ DefinitionBlock ("", "SSDT", 2, "ZPSS", "WMIS", 0x00000000)
return None return None
def select_acpi_patches(self, hardware_report, disabled_devices): def select_acpi_patches(self, hardware_report, disabled_devices):
self.utils.log_message("[ACPI GURU] Selecting ACPI patches...", level="INFO")
selected_patches = [] selected_patches = []
if "Laptop" in hardware_report.get("Motherboard").get("Platform") and \ if "Laptop" in hardware_report.get("Motherboard").get("Platform") and \
@@ -3315,42 +3291,22 @@ DefinitionBlock ("", "SSDT", 2, "ZPSS", "WMIS", 0x00000000)
if device_info.get("Bus Type") == "ACPI" and device_info.get("Device") in pci_data.YogaHIDs: if device_info.get("Bus Type") == "ACPI" and device_info.get("Device") in pci_data.YogaHIDs:
selected_patches.append("WMIS") selected_patches.append("WMIS")
self.utils.log_message("[ACPI GURU] Selected patches: {}".format(", ".join(selected_patches)), level="INFO")
for patch in self.patches: for patch in self.patches:
patch.checked = patch.name in selected_patches patch.checked = patch.name in selected_patches
def customize_patch_selection(self): def customize_patch_selection(self):
while True: items = []
contents = [] checked_indices = []
contents.append("")
contents.append("List of available patches:") for i, patch in enumerate(self.patches):
contents.append("") label = f"{patch.name} - {patch.description}"
for index, kext in enumerate(self.patches, start=1): items.append(label)
checkbox = "[*]" if kext.checked else "[ ]" if patch.checked:
checked_indices.append(i)
line = "{} {:2}. {:15} - {:60}".format(checkbox, index, kext.name, kext.description) result = show_checklist_dialog("Configure ACPI Patches", "Select ACPI patches you want to apply:", items, checked_indices)
if kext.checked:
line = "\033[1;32m{}\033[0m".format(line) if result is not None:
contents.append(line) for i, patch in enumerate(self.patches):
contents.append("") patch.checked = i in result
contents.append("\033[1;93mNote:\033[0m You can select multiple kexts by entering their indices separated by commas (e.g., '1, 2, 3').")
contents.append("")
contents.append("B. Back")
contents.append("Q. Quit")
contents.append("")
content = "\n".join(contents)
self.utils.adjust_window_size(content)
self.utils.head("Customize ACPI Patch Selections", resize=False)
print(content)
option = self.utils.request_input("Select your option: ")
if option.lower() == "q":
self.utils.exit_program()
if option.lower() == "b":
return
indices = [int(i.strip()) -1 for i in option.split(",") if i.strip().isdigit()]
for index in indices:
if index >= 0 and index < len(self.patches):
patch = self.patches[index]
patch.checked = not patch.checked

140
Scripts/backend.py Normal file
View File

@@ -0,0 +1,140 @@
import os
import sys
import logging
from datetime import datetime
from PyQt6.QtCore import QObject, pyqtSignal
from Scripts import acpi_guru
from Scripts import compatibility_checker
from Scripts import config_prodigy
from Scripts import gathering_files
from Scripts import hardware_customizer
from Scripts import kext_maestro
from Scripts import report_validator
from Scripts import run
from Scripts import smbios
from Scripts import settings
from Scripts import utils
from Scripts import integrity_checker
from Scripts import resource_fetcher
from Scripts import github
from Scripts import wifi_profile_extractor
from Scripts import dsdt
class LogSignalHandler(logging.Handler):
def __init__(self, signal):
super().__init__()
self.signal = signal
def emit(self, record):
msg = self.format(record)
to_build_log = getattr(record, "to_build_log", False)
self.signal.emit(msg, record.levelname, to_build_log)
class Backend(QObject):
log_message_signal = pyqtSignal(str, str, bool)
update_status_signal = pyqtSignal(str, str)
def __init__(self):
super().__init__()
self.u = utils.Utils()
self.settings = settings.Settings(utils_instance=self.u)
self.log_file_path = None
self._setup_logging()
self.u.clean_temporary_dir()
self.integrity_checker = integrity_checker.IntegrityChecker(utils_instance=self.u)
self.resource_fetcher = resource_fetcher.ResourceFetcher(
utils_instance=self.u,
integrity_checker_instance=self.integrity_checker
)
self.github = github.Github(
utils_instance=self.u,
resource_fetcher_instance=self.resource_fetcher
)
self.r = run.Run()
self.wifi_extractor = wifi_profile_extractor.WifiProfileExtractor(
run_instance=self.r,
utils_instance=self.u
)
self.k = kext_maestro.KextMaestro(utils_instance=self.u)
self.c = compatibility_checker.CompatibilityChecker(
utils_instance=self.u,
settings_instance=self.settings
)
self.h = hardware_customizer.HardwareCustomizer(utils_instance=self.u)
self.v = report_validator.ReportValidator(utils_instance=self.u)
self.dsdt = dsdt.DSDT(
utils_instance=self.u,
github_instance=self.github,
resource_fetcher_instance=self.resource_fetcher,
run_instance=self.r
)
self.o = gathering_files.gatheringFiles(
utils_instance=self.u,
github_instance=self.github,
kext_maestro_instance=self.k,
integrity_checker_instance=self.integrity_checker,
resource_fetcher_instance=self.resource_fetcher
)
self.s = smbios.SMBIOS(
gathering_files_instance=self.o,
run_instance=self.r,
utils_instance=self.u,
settings_instance=self.settings
)
self.ac = acpi_guru.ACPIGuru(
dsdt_instance=self.dsdt,
smbios_instance=self.s,
run_instance=self.r,
utils_instance=self.u
)
self.co = config_prodigy.ConfigProdigy(
gathering_files_instance=self.o,
smbios_instance=self.s,
utils_instance=self.u
)
custom_output_dir = self.settings.get_build_output_directory()
if custom_output_dir:
self.result_dir = self.u.create_folder(custom_output_dir, remove_content=True)
else:
self.result_dir = self.u.get_temporary_dir()
def _setup_logging(self):
logger = logging.getLogger("OpCoreSimplify")
logger.setLevel(logging.DEBUG)
logger.handlers = []
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(logging.DEBUG)
stream_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
logger.addHandler(stream_handler)
signal_handler = LogSignalHandler(self.log_message_signal)
signal_handler.setLevel(logging.DEBUG)
signal_handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(signal_handler)
if self.settings.get_enable_debug_logging():
try:
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Logs")
os.makedirs(log_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
self.log_file_path = os.path.join(log_dir, "ocs-{}.txt".format(timestamp))
file_handler = logging.FileHandler(self.log_file_path, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", "%Y-%m-%d %H:%M:%S"))
logger.addHandler(file_handler)
except Exception as e:
print("Failed to setup file logging: {}".format(e))

View File

@@ -3,39 +3,14 @@ from Scripts.datasets import os_data
from Scripts.datasets import pci_data from Scripts.datasets import pci_data
from Scripts.datasets import codec_layouts from Scripts.datasets import codec_layouts
from Scripts import utils from Scripts import utils
import time from Scripts import settings
class CompatibilityChecker: class CompatibilityChecker:
def __init__(self): def __init__(self, utils_instance=None, settings_instance=None):
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
self.settings = settings_instance if settings_instance else settings.Settings()
self.error_codes = []
def show_macos_compatibility(self, device_compatibility):
if not device_compatibility:
return "\033[90mUnchecked\033[0m"
if not device_compatibility[0]:
return "\033[0;31mUnsupported\033[0m"
max_compatibility = self.utils.parse_darwin_version(device_compatibility[0])[0]
min_compatibility = self.utils.parse_darwin_version(device_compatibility[-1])[0]
max_version = self.utils.parse_darwin_version(os_data.get_latest_darwin_version())[0]
min_version = self.utils.parse_darwin_version(os_data.get_lowest_darwin_version())[0]
if max_compatibility == min_version:
return "\033[1;36mMaximum support up to {}\033[0m".format(
os_data.get_macos_name_by_darwin(device_compatibility[-1])
)
if min_version < min_compatibility or max_compatibility < max_version:
return "\033[1;32m{} to {}\033[0m".format(
os_data.get_macos_name_by_darwin(device_compatibility[-1]),
os_data.get_macos_name_by_darwin(device_compatibility[0])
)
return "\033[1;36mUp to {}\033[0m".format(
os_data.get_macos_name_by_darwin(device_compatibility[0])
)
def is_low_end_intel_cpu(self, processor_name): def is_low_end_intel_cpu(self, processor_name):
return any(cpu_branding in processor_name for cpu_branding in ("Celeron", "Pentium")) return any(cpu_branding in processor_name for cpu_branding in ("Celeron", "Pentium"))
@@ -53,29 +28,14 @@ class CompatibilityChecker:
self.hardware_report["CPU"]["Compatibility"] = (max_version, min_version) self.hardware_report["CPU"]["Compatibility"] = (max_version, min_version)
print("{}- {}: {}".format(" "*3, self.hardware_report.get("CPU").get("Processor Name"), self.show_macos_compatibility(self.hardware_report["CPU"].get("Compatibility"))))
if max_version == min_version and max_version == None: if max_version == min_version and max_version == None:
print("") self.error_codes.append("ERROR_MISSING_SSE4")
print("Missing required SSE4.x instruction set.") return
print("Your CPU is not supported by macOS versions newer than Sierra (10.12).")
print("")
self.utils.request_input()
self.utils.exit_program()
self.max_native_macos_version = max_version self.max_native_macos_version = max_version
self.min_native_macos_version = min_version self.min_native_macos_version = min_version
def check_gpu_compatibility(self): def check_gpu_compatibility(self):
if not self.hardware_report.get("GPU"):
print("")
print("No GPU found!")
print("Please make sure to export the hardware report with the GPU information")
print("and try again.")
print("")
self.utils.request_input()
self.utils.exit_program()
for gpu_name, gpu_props in self.hardware_report["GPU"].items(): for gpu_name, gpu_props in self.hardware_report["GPU"].items():
gpu_manufacturer = gpu_props.get("Manufacturer") gpu_manufacturer = gpu_props.get("Manufacturer")
gpu_codename = gpu_props.get("Codename") gpu_codename = gpu_props.get("Codename")
@@ -155,21 +115,6 @@ class CompatibilityChecker:
if self.utils.parse_darwin_version(max_version) < self.utils.parse_darwin_version(ocl_patched_max_version): if self.utils.parse_darwin_version(max_version) < self.utils.parse_darwin_version(ocl_patched_max_version):
gpu_props["OCLP Compatibility"] = (ocl_patched_max_version, ocl_patched_min_version if self.utils.parse_darwin_version(ocl_patched_min_version) > self.utils.parse_darwin_version("{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0)) else "{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0)) gpu_props["OCLP Compatibility"] = (ocl_patched_max_version, ocl_patched_min_version if self.utils.parse_darwin_version(ocl_patched_min_version) > self.utils.parse_darwin_version("{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0)) else "{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0))
print("{}- {}: {}".format(" "*3, gpu_name, self.show_macos_compatibility(gpu_props.get("Compatibility"))))
if "OCLP Compatibility" in gpu_props:
print("{}- OCLP Compatibility: {}".format(" "*6, self.show_macos_compatibility(gpu_props.get("OCLP Compatibility"))))
connected_monitors = []
for monitor_name, monitor_info in self.hardware_report.get("Monitor", {}).items():
if monitor_info.get("Connected GPU") == gpu_name:
connected_monitors.append("{} ({})".format(monitor_name, monitor_info.get("Connector Type")))
if "Intel" in gpu_manufacturer and device_id.startswith(("01", "04", "0A", "0C", "0D")):
if monitor_info.get("Connector Type") == "VGA":
connected_monitors[-1] = "\033[0;31m{}{}\033[0m".format(connected_monitors[-1][:-1], ", unsupported)")
if connected_monitors:
print("{}- Connected Monitor{}: {}".format(" "*6, "s" if len(connected_monitors) > 1 else "", ", ".join(connected_monitors)))
max_supported_gpu_version = min_supported_gpu_version = None max_supported_gpu_version = min_supported_gpu_version = None
for gpu_name, gpu_props in self.hardware_report.get("GPU").items(): for gpu_name, gpu_props in self.hardware_report.get("GPU").items():
@@ -189,18 +134,14 @@ class CompatibilityChecker:
self.ocl_patched_macos_version = (gpu_props.get("OCLP Compatibility")[0], self.ocl_patched_macos_version[-1] if self.ocl_patched_macos_version and self.utils.parse_darwin_version(self.ocl_patched_macos_version[-1]) < self.utils.parse_darwin_version(gpu_props.get("OCLP Compatibility")[-1]) else gpu_props.get("OCLP Compatibility")[-1]) self.ocl_patched_macos_version = (gpu_props.get("OCLP Compatibility")[0], self.ocl_patched_macos_version[-1] if self.ocl_patched_macos_version and self.utils.parse_darwin_version(self.ocl_patched_macos_version[-1]) < self.utils.parse_darwin_version(gpu_props.get("OCLP Compatibility")[-1]) else gpu_props.get("OCLP Compatibility")[-1])
if max_supported_gpu_version == min_supported_gpu_version and max_supported_gpu_version == None: if max_supported_gpu_version == min_supported_gpu_version and max_supported_gpu_version == None:
print("") self.error_codes.append("ERROR_NO_COMPATIBLE_GPU")
print("You cannot install macOS without a supported GPU.") return
print("Please do NOT spam my inbox or issue tracker about this issue anymore!")
print("")
self.utils.request_input()
self.utils.exit_program()
self.max_native_macos_version = max_supported_gpu_version if self.utils.parse_darwin_version(max_supported_gpu_version) < self.utils.parse_darwin_version(self.max_native_macos_version) else self.max_native_macos_version self.max_native_macos_version = max_supported_gpu_version if self.utils.parse_darwin_version(max_supported_gpu_version) < self.utils.parse_darwin_version(self.max_native_macos_version) else self.max_native_macos_version
self.min_native_macos_version = min_supported_gpu_version if self.utils.parse_darwin_version(min_supported_gpu_version) > self.utils.parse_darwin_version(self.min_native_macos_version) else self.min_native_macos_version self.min_native_macos_version = min_supported_gpu_version if self.utils.parse_darwin_version(min_supported_gpu_version) > self.utils.parse_darwin_version(self.min_native_macos_version) else self.min_native_macos_version
def check_sound_compatibility(self): def check_sound_compatibility(self):
for audio_device, audio_props in self.hardware_report.get("Sound", {}).items(): for _, audio_props in self.hardware_report.get("Sound", {}).items():
codec_id = audio_props.get("Device ID") codec_id = audio_props.get("Device ID")
max_version = min_version = None max_version = min_version = None
@@ -213,19 +154,9 @@ class CompatibilityChecker:
audio_props["Compatibility"] = (max_version, min_version) audio_props["Compatibility"] = (max_version, min_version)
print("{}- {}: {}".format(" "*3, audio_device, self.show_macos_compatibility(audio_props.get("Compatibility"))))
audio_endpoints = audio_props.get("Audio Endpoints")
if audio_endpoints:
print("{}- Audio Endpoint{}: {}".format(" "*6, "s" if len(audio_endpoints) > 1 else "", ", ".join(audio_endpoints)))
def check_biometric_compatibility(self): def check_biometric_compatibility(self):
print(" \033[1;93mNote:\033[0m Biometric authentication in macOS requires Apple T2 Chip,") for _, biometric_props in self.hardware_report.get("Biometric", {}).items():
print(" which is not available for Hackintosh systems.")
print("")
for biometric_device, biometric_props in self.hardware_report.get("Biometric", {}).items():
biometric_props["Compatibility"] = (None, None) biometric_props["Compatibility"] = (None, None)
print("{}- {}: {}".format(" "*3, biometric_device, self.show_macos_compatibility(biometric_props.get("Compatibility"))))
def check_network_compatibility(self): def check_network_compatibility(self):
for device_name, device_props in self.hardware_report.get("Network", {}).items(): for device_name, device_props in self.hardware_report.get("Network", {}).items():
@@ -265,31 +196,7 @@ class CompatibilityChecker:
if bus_type.startswith("PCI") and not device_props.get("Compatibility"): if bus_type.startswith("PCI") and not device_props.get("Compatibility"):
device_props["Compatibility"] = (None, None) device_props["Compatibility"] = (None, None)
print("{}- {}: {}".format(" "*3, device_name, self.show_macos_compatibility(device_props.get("Compatibility"))))
if device_id in pci_data.WirelessCardIDs:
if device_id in pci_data.BroadcomWiFiIDs:
print("{}- Continuity Support: \033[1;32mFull\033[0m (AirDrop, Handoff, Universal Clipboard, Instant Hotspot,...)".format(" "*6))
elif device_id in pci_data.IntelWiFiIDs:
print("{}- Continuity Support: \033[1;33mPartial\033[0m (Handoff and Universal Clipboard with AirportItlwm)".format(" "*6))
print("{}\033[1;93mNote:\033[0m AirDrop, Universal Clipboard, Instant Hotspot,... not available".format(" "*6))
elif device_id in pci_data.AtherosWiFiIDs:
print("{}- Continuity Support: \033[1;31mLimited\033[0m (No Continuity features available)".format(" "*6))
print("{}\033[1;93mNote:\033[0m Atheros cards are not recommended for macOS".format(" "*6))
if "OCLP Compatibility" in device_props:
print("{}- OCLP Compatibility: {}".format(" "*6, self.show_macos_compatibility(device_props.get("OCLP Compatibility"))))
def check_storage_compatibility(self): def check_storage_compatibility(self):
if not self.hardware_report.get("Storage Controllers"):
print("")
print("No storage controller found!")
print("Please make sure to export the hardware report with the storage controller information")
print("and try again.")
print("")
self.utils.request_input()
self.utils.exit_program()
for controller_name, controller_props in self.hardware_report["Storage Controllers"].items(): for controller_name, controller_props in self.hardware_report["Storage Controllers"].items():
if controller_props.get("Bus Type") != "PCI": if controller_props.get("Bus Type") != "PCI":
continue continue
@@ -301,28 +208,17 @@ class CompatibilityChecker:
min_version = os_data.get_lowest_darwin_version() min_version = os_data.get_lowest_darwin_version()
if device_id in pci_data.IntelVMDIDs: if device_id in pci_data.IntelVMDIDs:
print("") self.error_codes.append("ERROR_INTEL_VMD")
print("Intel VMD controllers are not supported in macOS.") return
print("Please disable Intel VMD in the BIOS settings and try again with new hardware report.")
print("")
self.utils.request_input()
self.utils.exit_program()
if next((device for device in pci_data.UnsupportedNVMeSSDIDs if device_id == device[0] and subsystem_id in device[1]), None): if next((device for device in pci_data.UnsupportedNVMeSSDIDs if device_id == device[0] and subsystem_id in device[1]), None):
max_version = min_version = None max_version = min_version = None
controller_props["Compatibility"] = (max_version, min_version) controller_props["Compatibility"] = (max_version, min_version)
print("{}- {}: {}".format(" "*3, controller_name, self.show_macos_compatibility(controller_props.get("Compatibility"))))
if all(controller_props.get("Compatibility") == (None, None) for controller_name, controller_props in self.hardware_report["Storage Controllers"].items()): if all(controller_props.get("Compatibility") == (None, None) for controller_name, controller_props in self.hardware_report["Storage Controllers"].items()):
print("") self.error_codes.append("ERROR_NO_COMPATIBLE_STORAGE")
print("No compatible storage controller for macOS was found!") return
print("Consider purchasing a compatible SSD NVMe for your system.")
print("Western Digital NVMe SSDs are generally recommended for good macOS compatibility.")
print("")
self.utils.request_input()
self.utils.exit_program()
def check_bluetooth_compatibility(self): def check_bluetooth_compatibility(self):
for bluetooth_name, bluetooth_props in self.hardware_report.get("Bluetooth", {}).items(): for bluetooth_name, bluetooth_props in self.hardware_report.get("Bluetooth", {}).items():
@@ -339,8 +235,6 @@ class CompatibilityChecker:
max_version = min_version = None max_version = min_version = None
bluetooth_props["Compatibility"] = (max_version, min_version) bluetooth_props["Compatibility"] = (max_version, min_version)
print("{}- {}: {}".format(" "*3, bluetooth_name, self.show_macos_compatibility(bluetooth_props.get("Compatibility"))))
def check_sd_controller_compatibility(self): def check_sd_controller_compatibility(self):
for controller_name, controller_props in self.hardware_report.get("SD Controller", {}).items(): for controller_name, controller_props in self.hardware_report.get("SD Controller", {}).items():
@@ -357,16 +251,12 @@ class CompatibilityChecker:
controller_props["Compatibility"] = (max_version, min_version) controller_props["Compatibility"] = (max_version, min_version)
print("{}- {}: {}".format(" "*3, controller_name, self.show_macos_compatibility(controller_props.get("Compatibility"))))
def check_compatibility(self, hardware_report): def check_compatibility(self, hardware_report):
self.hardware_report = hardware_report self.hardware_report = hardware_report
self.ocl_patched_macos_version = None self.ocl_patched_macos_version = None
self.error_codes = []
self.utils.head("Compatibility Checker") self.utils.log_message("[COMPATIBILITY CHECKER] Starting compatibility check...", level="INFO")
print("")
print("Checking compatibility with macOS for the following devices:")
print("")
steps = [ steps = [
('CPU', self.check_cpu_compatibility), ('CPU', self.check_cpu_compatibility),
@@ -379,15 +269,13 @@ class CompatibilityChecker:
('SD Controller', self.check_sd_controller_compatibility) ('SD Controller', self.check_sd_controller_compatibility)
] ]
index = 0
for device_type, function in steps: for device_type, function in steps:
if self.hardware_report.get(device_type): if self.hardware_report.get(device_type):
index += 1
print("{}. {}:".format(index, device_type))
time.sleep(0.25)
function() function()
print("") if self.error_codes:
self.utils.request_input() self.utils.log_message("[COMPATIBILITY CHECKER] Compatibility check that found errors: {}".format(", ".join(self.error_codes)), level="INFO")
return hardware_report, (None, None), None, self.error_codes
return hardware_report, (self.min_native_macos_version, self.max_native_macos_version), self.ocl_patched_macos_version self.utils.log_message("[COMPATIBILITY CHECKER] Compatibility check completed successfully", level="INFO")
return hardware_report, (self.min_native_macos_version, self.max_native_macos_version), self.ocl_patched_macos_version, self.error_codes

View File

@@ -1,3 +1,5 @@
import copy
import os
from Scripts.datasets import chipset_data from Scripts.datasets import chipset_data
from Scripts.datasets import cpu_data from Scripts.datasets import cpu_data
from Scripts.datasets import mac_model_data from Scripts.datasets import mac_model_data
@@ -8,13 +10,14 @@ from Scripts.datasets import codec_layouts
from Scripts import gathering_files from Scripts import gathering_files
from Scripts import smbios from Scripts import smbios
from Scripts import utils from Scripts import utils
from Scripts.custom_dialogs import show_options_dialog
import random import random
class ConfigProdigy: class ConfigProdigy:
def __init__(self): def __init__(self, gathering_files_instance=None, smbios_instance=None, utils_instance=None):
self.g = gathering_files.gatheringFiles() self.g = gathering_files_instance if gathering_files_instance else gathering_files.gatheringFiles()
self.smbios = smbios.SMBIOS() self.smbios = smbios_instance if smbios_instance else smbios.SMBIOS()
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
self.cpuids = { self.cpuids = {
"Ivy Bridge": "A9060300", "Ivy Bridge": "A9060300",
"Haswell": "C3060300", "Haswell": "C3060300",
@@ -237,76 +240,7 @@ class ConfigProdigy:
return dict(sorted(igpu_properties.items(), key=lambda item: item[0])) return dict(sorted(igpu_properties.items(), key=lambda item: item[0]))
def select_audio_codec_layout(self, hardware_report, config=None, controller_required=False): def deviceproperties(self, hardware_report, disabled_devices, macos_version, kexts, audio_layout_id=None, audio_controller_properties=None):
try:
for device_properties in config["DeviceProperties"]["Add"].values():
if device_properties.get("layout-id"):
return None, None
except:
pass
codec_id = None
audio_controller_properties = None
for codec_properties in hardware_report.get("Sound", {}).values():
if codec_properties.get("Device ID") in codec_layouts.data:
codec_id = codec_properties.get("Device ID")
if codec_properties.get("Controller Device ID"):
for device_name, device_properties in hardware_report.get("System Devices").items():
if device_properties.get("Device ID") == codec_properties.get("Controller Device ID"):
audio_controller_properties = device_properties
break
break
if not codec_id:
return None, None
if controller_required and not audio_controller_properties:
return None, None
available_layouts = codec_layouts.data.get(codec_id)
recommended_authors = ("Mirone", "InsanelyDeepak", "Toleda", "DalianSky")
recommended_layouts = [layout for layout in available_layouts if self.utils.contains_any(recommended_authors, layout.comment)]
default_layout = random.choice(recommended_layouts or available_layouts)
while True:
contents = []
contents.append("")
contents.append("List of Codec Layouts:")
contents.append("")
contents.append("ID Comment")
contents.append("------------------------------------------------------------------")
for layout in available_layouts:
line = "{:<4} {}".format(layout.id, layout.comment[:60])
if layout == default_layout:
contents.append("\033[1;32m{}\033[0m".format(line))
else:
contents.append(line)
contents.append("")
contents.append("\033[1;93mNote:\033[0m")
contents.append("- The default layout may not be optimal.")
contents.append("- Test different layouts to find what works best for your system.")
contents.append("")
content = "\n".join(contents)
self.utils.adjust_window_size(content)
self.utils.head("Choosing Codec Layout ID", resize=False)
print(content)
selected_layout_id = self.utils.request_input(f"Enter the ID of the codec layout you want to use (default: {default_layout.id}): ") or default_layout.id
try:
selected_layout_id = int(selected_layout_id)
for layout in available_layouts:
if layout.id == selected_layout_id:
return selected_layout_id, audio_controller_properties
except:
continue
def deviceproperties(self, hardware_report, disabled_devices, macos_version, kexts):
deviceproperties_add = {} deviceproperties_add = {}
def add_device_property(pci_path, properties): def add_device_property(pci_path, properties):
@@ -349,11 +283,8 @@ class ConfigProdigy:
"model": gpu_name "model": gpu_name
}) })
if kexts[kext_data.kext_index_by_name.get("AppleALC")].checked: if audio_layout_id is not None and audio_controller_properties is not None:
selected_layout_id, audio_controller_properties = self.select_audio_codec_layout(hardware_report, controller_required=True) add_device_property(audio_controller_properties.get("PCI Path"), {"layout-id": audio_layout_id})
if selected_layout_id and audio_controller_properties:
add_device_property(audio_controller_properties.get("PCI Path"), {"layout-id": selected_layout_id})
for network_name, network_props in hardware_report.get("Network", {}).items(): for network_name, network_props in hardware_report.get("Network", {}).items():
device_id = network_props.get("Device ID") device_id = network_props.get("Device ID")
@@ -502,7 +433,7 @@ class ConfigProdigy:
return kernel_patch return kernel_patch
def boot_args(self, hardware_report, macos_version, needs_oclp, kexts, config): def boot_args(self, hardware_report, macos_version, needs_oclp, kexts, config, audio_layout_id=None, audio_controller_properties=None):
boot_args = [ boot_args = [
"-v", "-v",
"debug=0x100", "debug=0x100",
@@ -566,10 +497,8 @@ class ConfigProdigy:
elif discrete_gpu.get("Manufacturer") == "NVIDIA" and not "Kepler" in discrete_gpu.get("Codename"): elif discrete_gpu.get("Manufacturer") == "NVIDIA" and not "Kepler" in discrete_gpu.get("Codename"):
boot_args.extend(("nvda_drv_vrl=1", "ngfxcompat=1", "ngfxgl=1")) boot_args.extend(("nvda_drv_vrl=1", "ngfxcompat=1", "ngfxgl=1"))
elif kext.name == "AppleALC": elif kext.name == "AppleALC":
selected_layout_id, _ = self.select_audio_codec_layout(hardware_report, config) if audio_layout_id is not None and audio_controller_properties is None:
boot_args.append("alcid={}".format(audio_layout_id))
if selected_layout_id:
boot_args.append("alcid={}".format(selected_layout_id))
elif kext.name == "VoodooI2C": elif kext.name == "VoodooI2C":
boot_args.append("-vi2c-force-polling") boot_args.append("-vi2c-force-polling")
elif kext.name == "CpuTopologyRebuild": elif kext.name == "CpuTopologyRebuild":
@@ -611,7 +540,7 @@ class ConfigProdigy:
return uefi_drivers return uefi_drivers
def genarate(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp, kexts, config): def genarate(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp, kexts, config, audio_layout_id=None, audio_controller_properties=None):
del config["#WARNING - 1"] del config["#WARNING - 1"]
del config["#WARNING - 2"] del config["#WARNING - 2"]
del config["#WARNING - 3"] del config["#WARNING - 3"]
@@ -636,7 +565,7 @@ class ConfigProdigy:
config["Booter"]["Quirks"]["SetupVirtualMap"] = hardware_report.get("BIOS").get("Firmware Type") == "UEFI" and not hardware_report.get("Motherboard").get("Chipset") in chipset_data.AMDChipsets[11:17] + chipset_data.IntelChipsets[90:100] config["Booter"]["Quirks"]["SetupVirtualMap"] = hardware_report.get("BIOS").get("Firmware Type") == "UEFI" and not hardware_report.get("Motherboard").get("Chipset") in chipset_data.AMDChipsets[11:17] + chipset_data.IntelChipsets[90:100]
config["Booter"]["Quirks"]["SyncRuntimePermissions"] = "AMD" in hardware_report.get("CPU").get("Manufacturer") or hardware_report.get("Motherboard").get("Chipset") in chipset_data.IntelChipsets[90:100] + chipset_data.IntelChipsets[104:] config["Booter"]["Quirks"]["SyncRuntimePermissions"] = "AMD" in hardware_report.get("CPU").get("Manufacturer") or hardware_report.get("Motherboard").get("Chipset") in chipset_data.IntelChipsets[90:100] + chipset_data.IntelChipsets[104:]
config["DeviceProperties"]["Add"] = self.deviceproperties(hardware_report, disabled_devices, macos_version, kexts) config["DeviceProperties"]["Add"] = self.deviceproperties(hardware_report, disabled_devices, macos_version, kexts, audio_layout_id, audio_controller_properties)
config["Kernel"]["Block"] = self.block_kext_bundle(kexts) config["Kernel"]["Block"] = self.block_kext_bundle(kexts)
spoof_cpuid = self.spoof_cpuid( spoof_cpuid = self.spoof_cpuid(
@@ -685,7 +614,7 @@ class ConfigProdigy:
config["Misc"]["Tools"] = [] config["Misc"]["Tools"] = []
del config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["#INFO (prev-lang:kbd)"] del config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["#INFO (prev-lang:kbd)"]
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["boot-args"] = self.boot_args(hardware_report, macos_version, needs_oclp, kexts, config) config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["boot-args"] = self.boot_args(hardware_report, macos_version, needs_oclp, kexts, config, audio_layout_id, audio_controller_properties)
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["csr-active-config"] = self.utils.hex_to_bytes(self.csr_active_config(macos_version)) config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["csr-active-config"] = self.utils.hex_to_bytes(self.csr_active_config(macos_version))
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["prev-lang:kbd"] = self.utils.hex_to_bytes("") config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["prev-lang:kbd"] = self.utils.hex_to_bytes("")

461
Scripts/custom_dialogs.py Normal file
View File

@@ -0,0 +1,461 @@
import re
import functools
from PyQt6.QtCore import Qt, QObject, QThread, QMetaObject, QCoreApplication, pyqtSlot, pyqtSignal
from PyQt6.QtWidgets import QWidget, QHBoxLayout, QRadioButton, QButtonGroup, QVBoxLayout, QCheckBox, QScrollArea, QLabel
from qfluentwidgets import MessageBoxBase, SubtitleLabel, BodyLabel, LineEdit, PushButton, ProgressBar
from Scripts.datasets import os_data
_default_gui_handler = None
def set_default_gui_handler(handler):
global _default_gui_handler
_default_gui_handler = handler
class ThreadRunner(QObject):
def __init__(self, func, *args, **kwargs):
super().__init__()
self.func = func
self.args = args
self.kwargs = kwargs
self.result = None
self.exception = None
@pyqtSlot()
def run(self):
try:
self.result = self.func(*self.args, **self.kwargs)
except Exception as e:
self.exception = e
def ensure_main_thread(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if QThread.currentThread() == QCoreApplication.instance().thread():
return func(*args, **kwargs)
runner = ThreadRunner(func, *args, **kwargs)
runner.moveToThread(QCoreApplication.instance().thread())
QMetaObject.invokeMethod(runner, "run", Qt.ConnectionType.BlockingQueuedConnection)
if runner.exception:
raise runner.exception
return runner.result
return wrapper
class CustomMessageDialog(MessageBoxBase):
def __init__(self, title, content):
super().__init__(_default_gui_handler)
self.titleLabel = SubtitleLabel(title, self.widget)
self.contentLabel = BodyLabel(content, self.widget)
self.contentLabel.setWordWrap(True)
is_html = bool(re.search(r"<[^>]+>", content))
if is_html:
self.contentLabel.setTextFormat(Qt.TextFormat.RichText)
self.contentLabel.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
self.contentLabel.setOpenExternalLinks(True)
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.contentLabel)
self.widget.setMinimumWidth(600)
self.custom_widget = None
self.input_field = None
self.button_group = None
def add_input(self, placeholder: str = "", default_value: str = ""):
self.input_field = LineEdit(self.widget)
if placeholder:
self.input_field.setPlaceholderText(placeholder)
if default_value:
self.input_field.setText(str(default_value))
self.viewLayout.addWidget(self.input_field)
self.input_field.setFocus()
return self.input_field
def add_custom_widget(self, widget: QWidget):
self.custom_widget = widget
self.viewLayout.addWidget(widget)
def add_radio_options(self, options, default_index=0):
self.button_group = QButtonGroup(self)
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(10, 5, 10, 5)
for i, option_text in enumerate(options):
is_html = bool(re.search(r"<[^>]+>", option_text))
if is_html:
row_widget = QWidget()
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(0, 0, 0, 0)
row_layout.setSpacing(8)
radio = QRadioButton()
label = BodyLabel(option_text)
label.setTextFormat(Qt.TextFormat.RichText)
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
label.setOpenExternalLinks(True)
label.setWordWrap(True)
row_layout.addWidget(radio)
row_layout.addWidget(label, 1)
layout.addWidget(row_widget)
else:
radio = QRadioButton(option_text)
layout.addWidget(radio)
self.button_group.addButton(radio, i)
if i == default_index:
radio.setChecked(True)
self.viewLayout.addWidget(container)
return self.button_group
def add_checklist(self, items, checked_indices=None):
if checked_indices is None:
checked_indices = []
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFixedHeight(400)
container = QWidget()
layout = QVBoxLayout(container)
checkboxes = []
current_category = None
for i, item in enumerate(items):
label_text = item
category = None
supported = True
if isinstance(item, dict):
label_text = item.get("label", "")
category = item.get("category")
supported = item.get("supported", True)
if category and category != current_category:
current_category = category
if i > 0:
layout.addSpacing(10)
header = QLabel("Category: {}".format(category))
header.setStyleSheet("font-weight: bold; color: #0078D4; padding-top: 5px; padding-bottom: 5px; border-bottom: 1px solid #E1DFDD;")
layout.addWidget(header)
cb = QCheckBox(label_text)
if i in checked_indices:
cb.setChecked(True)
if not supported:
cb.setStyleSheet("color: #A19F9D;")
layout.addWidget(cb)
checkboxes.append(cb)
layout.addStretch()
scroll.setWidget(container)
self.viewLayout.addWidget(scroll)
return checkboxes
def configure_buttons(self, yes_text: str = "OK", no_text: str = "Cancel", show_cancel: bool = True):
self.yesButton.setText(yes_text)
self.cancelButton.setText(no_text)
self.cancelButton.setVisible(show_cancel)
@ensure_main_thread
def show_info(title: str, content: str) -> None:
dialog = CustomMessageDialog(title, content)
dialog.configure_buttons(yes_text="OK", show_cancel=False)
dialog.exec()
@ensure_main_thread
def show_confirmation(title: str, content: str, yes_text="Yes", no_text="No") -> bool:
dialog = CustomMessageDialog(title, content)
dialog.configure_buttons(yes_text=yes_text, no_text=no_text, show_cancel=True)
return dialog.exec()
@ensure_main_thread
def show_options_dialog(title, content, options, default_index=0):
dialog = CustomMessageDialog(title, content)
dialog.add_radio_options(options, default_index)
dialog.configure_buttons(yes_text="OK", show_cancel=True)
if dialog.exec():
return dialog.button_group.checkedId()
return None
@ensure_main_thread
def show_checklist_dialog(title, content, items, checked_indices=None):
dialog = CustomMessageDialog(title, content)
checkboxes = dialog.add_checklist(items, checked_indices)
dialog.configure_buttons(yes_text="OK", show_cancel=True)
if dialog.exec():
return [i for i, cb in enumerate(checkboxes) if cb.isChecked()]
return None
@ensure_main_thread
def ask_network_count(total_networks):
content = (
"Found {} WiFi networks on this device.<br><br>"
"How many networks would you like to process?<br>"
"<ul>"
"<li>Enter a number (1-{})</li>"
"<li>Or select \"Process All\"</li>"
"</ul>"
).format(total_networks, total_networks)
dialog = CustomMessageDialog("WiFi Network Retrieval", content)
dialog.input_field = dialog.add_input(placeholder="1-{} (Default: 5)".format(total_networks), default_value="5")
button_layout = QHBoxLayout()
all_btn = PushButton("Process All Networks", dialog.widget)
button_layout.addWidget(all_btn)
button_layout.addStretch()
dialog.viewLayout.addLayout(button_layout)
result = {"value": 5}
def on_all_clicked():
result["value"] = "a"
dialog.accept()
all_btn.clicked.connect(on_all_clicked)
def on_accept():
if result["value"] == "a":
return
text = dialog.input_field.text().strip()
if not text:
result["value"] = 5
elif text.lower() == "a":
result["value"] = "a"
else:
try:
val = int(text)
result["value"] = min(max(1, val), total_networks)
except ValueError:
result["value"] = 5
original_accept = dialog.accept
def custom_accept():
on_accept()
original_accept()
dialog.accept = custom_accept
if dialog.exec():
return result["value"]
return 5
def show_smbios_selection_dialog(title, content, items, current_selection, default_selection):
dialog = CustomMessageDialog(title, content)
top_container = QWidget()
top_layout = QHBoxLayout(top_container)
top_layout.setContentsMargins(0, 0, 0, 0)
show_all_cb = QCheckBox("Show all models")
restore_btn = PushButton("Restore default ({})".format(default_selection))
top_layout.addWidget(show_all_cb)
top_layout.addStretch()
top_layout.addWidget(restore_btn)
dialog.viewLayout.addWidget(top_container)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFixedHeight(400)
container = QWidget()
layout = QVBoxLayout(container)
layout.setSpacing(5)
button_group = QButtonGroup(dialog)
item_widgets = []
current_category = None
for i, item in enumerate(items):
category = item.get("category")
category_label = None
if category != current_category:
current_category = category
category_label = QLabel("Category: {}".format(category))
category_label.setStyleSheet("font-weight: bold; color: #0078D4; margin-top: 10px; border-bottom: 1px solid #E1DFDD;")
layout.addWidget(category_label)
row_widget = QWidget()
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(20, 0, 0, 0)
radio = QRadioButton(item.get("label"))
if not item.get("is_supported"):
radio.setStyleSheet("color: #A19F9D;")
row_layout.addWidget(radio)
layout.addWidget(row_widget)
button_group.addButton(radio, i)
if item.get("name") == current_selection:
radio.setChecked(True)
widget_data = {
"row": row_widget,
"category_label": category_label,
"item": item,
"radio": radio
}
item_widgets.append(widget_data)
layout.addStretch()
scroll.setWidget(container)
dialog.viewLayout.addWidget(scroll)
def update_visibility():
show_all = show_all_cb.isChecked()
visible_categories = set()
for w in item_widgets:
item = w["item"]
is_current_or_default = item.get("name") in (current_selection, default_selection)
is_compatible = item.get("is_compatible")
should_show = is_current_or_default or show_all or is_compatible
w["row"].setVisible(should_show)
if should_show:
visible_categories.add(item.get("category"))
for w in item_widgets:
if w["category_label"]:
w["category_label"].setVisible(w["item"].get("category") in visible_categories)
show_all_cb.stateChanged.connect(update_visibility)
def restore_default():
for i, item in enumerate(items):
if item.get("name") == default_selection:
button_group.button(i).setChecked(True)
break
restore_btn.clicked.connect(restore_default)
update_visibility()
dialog.configure_buttons(yes_text="OK", show_cancel=True)
if dialog.exec():
selected_id = button_group.checkedId()
if selected_id >= 0:
return items[selected_id].get("name")
return None
def show_macos_version_dialog(native_macos_version, ocl_patched_macos_version, suggested_macos_version):
content = ""
if native_macos_version[1][:2] != suggested_macos_version[:2]:
suggested_macos_name = os_data.get_macos_name_by_darwin(suggested_macos_version)
content += "<b style=\"color: #1565C0\">Suggested macOS version:</b> For better compatibility and stability, we suggest you to use only <b>{}</b> or older.<br><br>".format(suggested_macos_name)
content += "Please select the macOS version you want to use:"
options = []
version_values = []
default_index = None
native_min = int(native_macos_version[0][:2])
native_max = int(native_macos_version[-1][:2])
oclp_min = int(ocl_patched_macos_version[-1][:2]) if ocl_patched_macos_version else 99
oclp_max = int(ocl_patched_macos_version[0][:2]) if ocl_patched_macos_version else 0
min_version = min(native_min, oclp_min)
max_version = max(native_max, oclp_max)
for darwin_version in range(min_version, max_version + 1):
if not (native_min <= darwin_version <= native_max or oclp_min <= darwin_version <= oclp_max):
continue
name = os_data.get_macos_name_by_darwin(str(darwin_version))
label = ""
if oclp_min <= darwin_version <= oclp_max:
label = " <i style=\"color: #FF8C00\">(Requires OpenCore Legacy Patcher)</i>"
options.append("<span>{}{}</span>".format(name, label))
version_values.append(darwin_version)
if darwin_version == int(suggested_macos_version[:2]):
default_index = len(options) - 1
result = show_options_dialog("Select macOS Version", content, options, default_index)
if result is not None:
return "{}.99.99".format(version_values[result])
return None
class UpdateDialog(MessageBoxBase):
progress_updated = pyqtSignal(int, str)
def __init__(self, title="Update", initial_status="Checking for updates..."):
super().__init__(_default_gui_handler)
self.titleLabel = SubtitleLabel(title, self.widget)
self.statusLabel = BodyLabel(initial_status, self.widget)
self.statusLabel.setWordWrap(True)
self.progressBar = ProgressBar(self.widget)
self.progressBar.setRange(0, 100)
self.progressBar.setValue(0)
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.statusLabel)
self.viewLayout.addWidget(self.progressBar)
self.widget.setMinimumWidth(600)
self.cancelButton.setVisible(False)
self.yesButton.setVisible(False)
self.progress_updated.connect(self._update_progress_safe)
@pyqtSlot(int, str)
def _update_progress_safe(self, value, status_text):
self.progressBar.setValue(value)
if status_text:
self.statusLabel.setText(status_text)
QCoreApplication.processEvents()
def update_progress(self, value, status_text=""):
self.progress_updated.emit(value, status_text)
def set_status(self, status_text):
self.update_progress(self.progressBar.value(), status_text)
def show_buttons(self, show_ok=False, show_cancel=False):
self.yesButton.setVisible(show_ok)
self.cancelButton.setVisible(show_cancel)
def configure_buttons(self, ok_text="OK", cancel_text="Cancel"):
self.yesButton.setText(ok_text)
self.cancelButton.setText(cancel_text)
def show_update_dialog(title="Update", initial_status="Checking for updates..."):
dialog = UpdateDialog(title, initial_status)
return dialog

View File

@@ -0,0 +1,47 @@
from typing import Dict, Callable
from Scripts.value_formatters import format_value, get_value_type
def get_tooltip(key_path, value, original_value = None, context = None):
context = context or {}
if key_path in TOOLTIP_GENERATORS:
generator = TOOLTIP_GENERATORS[key_path]
return generator(key_path, value, original_value, context)
path_parts = key_path.split(".")
for i in range(len(path_parts), 0, -1):
parent_path = ".".join(path_parts[:i]) + ".*"
if parent_path in TOOLTIP_GENERATORS:
generator = TOOLTIP_GENERATORS[parent_path]
return generator(key_path, value, original_value, context)
return _default_tooltip(key_path, value, original_value, context)
def _default_tooltip(key_path, value, original_value, context):
tooltip = f"<b>{key_path}</b><br><br>"
if original_value is not None and original_value != value:
tooltip += f"<b>Original:</b> {format_value(original_value)}<br>"
original_type = get_value_type(original_value)
if original_type:
tooltip += f"<b>Type:</b> {original_type}<br>"
tooltip += f"<b>Modified:</b> {format_value(value)}<br>"
modified_type = get_value_type(value)
if modified_type:
tooltip += f"<b>Type:</b> {modified_type}<br>"
tooltip += "<br>"
else:
tooltip += f"<b>Value:</b> {format_value(value)}<br>"
value_type = get_value_type(value)
if value_type:
tooltip += f"<b>Type:</b> {value_type}<br>"
tooltip += "<br>"
return tooltip
TOOLTIP_GENERATORS: Dict[str, Callable] = {}
def _register_tooltip(path, generator):
TOOLTIP_GENERATORS[path] = generator

View File

@@ -1,3 +1,9 @@
from Scripts.settings import Settings
settings = Settings()
INCLUDE_BETA = settings.get_include_beta_versions()
class macOSVersionInfo: class macOSVersionInfo:
def __init__(self, name, macos_version, release_status = "final"): def __init__(self, name, macos_version, release_status = "final"):
self.name = name self.name = name
@@ -17,7 +23,7 @@ macos_versions = [
macOSVersionInfo("Tahoe", "26") macOSVersionInfo("Tahoe", "26")
] ]
def get_latest_darwin_version(include_beta=True): def get_latest_darwin_version(include_beta=INCLUDE_BETA):
for macos_version in macos_versions[::-1]: for macos_version in macos_versions[::-1]:
if include_beta: if include_beta:
return "{}.{}.{}".format(macos_version.darwin_version, 99, 99) return "{}.{}.{}".format(macos_version.darwin_version, 99, 99)
@@ -32,4 +38,4 @@ def get_macos_name_by_darwin(darwin_version):
for data in macos_versions: for data in macos_versions:
if data.darwin_version == int(darwin_version[:2]): if data.darwin_version == int(darwin_version[:2]):
return "macOS {} {}{}".format(data.name, data.macos_version, "" if data.release_status == "final" else " (Beta)") return "macOS {} {}{}".format(data.name, data.macos_version, "" if data.release_status == "final" else " (Beta)")
return None return None

View File

@@ -7,13 +7,11 @@ from Scripts import run
from Scripts import utils from Scripts import utils
class DSDT: class DSDT:
def __init__(self, **kwargs): def __init__(self, utils_instance=None, github_instance=None, resource_fetcher_instance=None, run_instance=None):
#self.dl = downloader.Downloader() self.u = utils_instance if utils_instance else utils.Utils()
self.github = github.Github() self.github = github_instance if github_instance else github.Github()
self.fetcher = resource_fetcher.ResourceFetcher() self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
self.r = run.Run() self.r = run_instance if run_instance else run.Run()
#self.u = utils.Utils("SSDT Time")
self.u = utils.Utils()
self.iasl_url_macOS = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-stable" self.iasl_url_macOS = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-stable"
self.iasl_url_macOS_legacy = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-legacy" self.iasl_url_macOS_legacy = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-legacy"
self.iasl_url_linux = "https://raw.githubusercontent.com/corpnewt/linux_iasl/main/iasl.zip" self.iasl_url_linux = "https://raw.githubusercontent.com/corpnewt/linux_iasl/main/iasl.zip"
@@ -315,10 +313,7 @@ class DSDT:
return self.check_iasl(legacy=legacy,try_downloading=False) return self.check_iasl(legacy=legacy,try_downloading=False)
def _download_and_extract(self, temp, url): def _download_and_extract(self, temp, url):
self.u.head("Gathering Files") self.u.log_message("[DSDT] Downloading iasl...", level="INFO")
print("")
print("Please wait for download iasl...")
print("")
ztemp = tempfile.mkdtemp(dir=temp) ztemp = tempfile.mkdtemp(dir=temp)
zfile = os.path.basename(url) zfile = os.path.basename(url)
#print("Downloading {}".format(os.path.basename(url))) #print("Downloading {}".format(os.path.basename(url)))

View File

@@ -1,3 +1,4 @@
from Scripts.custom_dialogs import show_info
from Scripts import github from Scripts import github
from Scripts import kext_maestro from Scripts import kext_maestro
from Scripts import integrity_checker from Scripts import integrity_checker
@@ -11,12 +12,12 @@ import platform
os_name = platform.system() os_name = platform.system()
class gatheringFiles: class gatheringFiles:
def __init__(self): def __init__(self, utils_instance=None, github_instance=None, kext_maestro_instance=None, integrity_checker_instance=None, resource_fetcher_instance=None):
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
self.github = github.Github() self.github = github_instance if github_instance else github.Github()
self.kext = kext_maestro.KextMaestro() self.kext = kext_maestro_instance if kext_maestro_instance else kext_maestro.KextMaestro()
self.fetcher = resource_fetcher.ResourceFetcher() self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
self.integrity_checker = integrity_checker.IntegrityChecker() self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker()
self.dortania_builds_url = "https://raw.githubusercontent.com/dortania/build-repo/builds/latest.json" self.dortania_builds_url = "https://raw.githubusercontent.com/dortania/build-repo/builds/latest.json"
self.ocbinarydata_url = "https://github.com/acidanthera/OcBinaryData/archive/refs/heads/master.zip" self.ocbinarydata_url = "https://github.com/acidanthera/OcBinaryData/archive/refs/heads/master.zip"
self.amd_vanilla_patches_url = "https://raw.githubusercontent.com/AMD-OSX/AMD_Vanilla/beta/patches.plist" self.amd_vanilla_patches_url = "https://raw.githubusercontent.com/AMD-OSX/AMD_Vanilla/beta/patches.plist"
@@ -85,6 +86,7 @@ class gatheringFiles:
def move_bootloader_kexts_to_product_directory(self, product_name): def move_bootloader_kexts_to_product_directory(self, product_name):
if not os.path.exists(self.temporary_dir): if not os.path.exists(self.temporary_dir):
self.utils.log_message("[GATHERING FILES] The directory {} does not exist.".format(self.temporary_dir), level="ERROR", to_build_log=True)
raise FileNotFoundError("The directory {} does not exist.".format(self.temporary_dir)) raise FileNotFoundError("The directory {} does not exist.".format(self.temporary_dir))
temp_product_dir = os.path.join(self.temporary_dir, product_name) temp_product_dir = os.path.join(self.temporary_dir, product_name)
@@ -139,9 +141,7 @@ class gatheringFiles:
return True return True
def gather_bootloader_kexts(self, kexts, macos_version): def gather_bootloader_kexts(self, kexts, macos_version):
self.utils.head("Gathering Files") self.utils.log_message("[GATHERING FILES] Please wait for download OpenCorePkg, kexts and macserial...", level="INFO", to_build_log=True)
print("")
print("Please wait for download OpenCorePkg, kexts and macserial...")
download_history = self.utils.read_file(self.download_history_file) download_history = self.utils.read_file(self.download_history_file)
if not isinstance(download_history, list): if not isinstance(download_history, list):
@@ -187,8 +187,7 @@ class gatheringFiles:
product_download_index = self.get_product_index(download_database, product.github_repo.get("repo")) product_download_index = self.get_product_index(download_database, product.github_repo.get("repo"))
if product_download_index is None: if product_download_index is None:
print("\n") self.utils.log_message("[GATHERING FILES] Could not find download URL for {}.".format(product_name), level="WARNING", to_build_log=True)
print("Could not find download URL for {}.".format(product_name))
continue continue
product_info = download_database[product_download_index] product_info = download_database[product_download_index]
@@ -210,20 +209,14 @@ class gatheringFiles:
folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path) folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path)
if is_latest_id and folder_is_valid: if is_latest_id and folder_is_valid:
print(f"\nLatest version of {product_name} already downloaded.") self.utils.log_message("[GATHERING FILES] Latest version of {} already downloaded.".format(product_name), level="INFO", to_build_log=True)
continue continue
print("") self.utils.log_message("[GATHERING FILES] Updating {}...".format(product_name), level="INFO", to_build_log=True)
print("Updating" if product_history_index is not None else "Please wait for download", end=" ")
print("{}...".format(product_name))
if product_download_url: if product_download_url:
print("from {}".format(product_download_url)) self.utils.log_message("[GATHERING FILES] Downloading from {}".format(product_download_url), level="INFO", to_build_log=True)
print("")
else: else:
print("") self.utils.log_message("[GATHERING FILES] Could not find download URL for {}.".format(product_name), level="ERROR", to_build_log=True)
print("Could not find download URL for {}.".format(product_name))
print("")
self.utils.request_input()
shutil.rmtree(self.temporary_dir, ignore_errors=True) shutil.rmtree(self.temporary_dir, ignore_errors=True)
return False return False
@@ -231,9 +224,10 @@ class gatheringFiles:
if not self.fetcher.download_and_save_file(product_download_url, zip_path, sha256_hash): if not self.fetcher.download_and_save_file(product_download_url, zip_path, sha256_hash):
folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path) folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path)
if product_history_index is not None and folder_is_valid: if product_history_index is not None and folder_is_valid:
print("Using previously downloaded version of {}.".format(product_name)) self.utils.log_message("[GATHERING FILES] Using previously downloaded version of {}.".format(product_name), level="INFO", to_build_log=True)
continue continue
else: else:
self.utils.log_message("[GATHERING FILES] Could not download {} at this time. Please try again later.".format(product_name), level="ERROR", to_build_log=True)
raise Exception("Could not download {} at this time. Please try again later.".format(product_name)) raise Exception("Could not download {} at this time. Please try again later.".format(product_name))
self.utils.extract_zip_file(zip_path) self.utils.extract_zip_file(zip_path)
@@ -250,17 +244,12 @@ class gatheringFiles:
if "OpenCore" in product_name: if "OpenCore" in product_name:
oc_binary_data_zip_path = os.path.join(self.temporary_dir, "OcBinaryData.zip") oc_binary_data_zip_path = os.path.join(self.temporary_dir, "OcBinaryData.zip")
print("") self.utils.log_message("[GATHERING FILES] Please wait for download OcBinaryData...", level="INFO", to_build_log=True)
print("Please wait for download OcBinaryData...") self.utils.log_message("[GATHERING FILES] Downloading from {}".format(self.ocbinarydata_url), level="INFO", to_build_log=True)
print("from {}".format(self.ocbinarydata_url))
print("")
self.fetcher.download_and_save_file(self.ocbinarydata_url, oc_binary_data_zip_path) self.fetcher.download_and_save_file(self.ocbinarydata_url, oc_binary_data_zip_path)
if not os.path.exists(oc_binary_data_zip_path): if not os.path.exists(oc_binary_data_zip_path):
print("") self.utils.log_message("[GATHERING FILES] Could not download OcBinaryData at this time. Please try again later.", level="ERROR", to_build_log=True)
print("Could not download OcBinaryData at this time.")
print("Please try again later.\n")
self.utils.request_input()
shutil.rmtree(self.temporary_dir, ignore_errors=True) shutil.rmtree(self.temporary_dir, ignore_errors=True)
return False return False
@@ -278,14 +267,9 @@ class gatheringFiles:
response = self.fetcher.fetch_and_parse_content(patches_url, "plist") response = self.fetcher.fetch_and_parse_content(patches_url, "plist")
return response["Kernel"]["Patch"] return response["Kernel"]["Patch"]
except: except:
print("") self.utils.log_message("[GATHERING FILES] Unable to download {} at this time".format(patches_name), level="WARNING", to_build_log=True)
print("Unable to download {} at this time".format(patches_name)) show_info("Download Failed", "Unable to download {} at this time. Please try again later or apply them manually.".format(patches_name))
print("from " + patches_url)
print("")
print("Please try again later or apply them manually.")
print("")
self.utils.request_input()
return [] return []
def _update_download_history(self, download_history, product_name, product_id, product_url, sha256_hash): def _update_download_history(self, download_history, product_name, product_id, product_url, sha256_hash):
@@ -310,7 +294,7 @@ class gatheringFiles:
if os_name != "Windows": if os_name != "Windows":
return return
self.utils.head("Gathering Hardware Sniffer") self.utils.log_message("[GATHERING FILES] Gathering Hardware Sniffer...", level="INFO")
PRODUCT_NAME = "Hardware-Sniffer-CLI.exe" PRODUCT_NAME = "Hardware-Sniffer-CLI.exe"
REPO_OWNER = "lzhoang2801" REPO_OWNER = "lzhoang2801"
@@ -333,11 +317,7 @@ class gatheringFiles:
break break
if not all([product_id, product_download_url, sha256_hash]): if not all([product_id, product_download_url, sha256_hash]):
print("") show_info("Release Information Not Found", "Could not find release information for {}. Please try again later.".format(PRODUCT_NAME))
print("Could not find release information for {}.".format(PRODUCT_NAME))
print("Please try again later.")
print("")
self.utils.request_input()
raise Exception("Could not find release information for {}.".format(PRODUCT_NAME)) raise Exception("Could not find release information for {}.".format(PRODUCT_NAME))
download_history = self.utils.read_file(self.download_history_file) download_history = self.utils.read_file(self.download_history_file)
@@ -356,22 +336,14 @@ class gatheringFiles:
file_is_valid = (sha256_hash == local_hash) file_is_valid = (sha256_hash == local_hash)
if is_latest_id and file_is_valid: if is_latest_id and file_is_valid:
print("") self.utils.log_message("[GATHERING FILES] Latest version of {} already downloaded.".format(PRODUCT_NAME), level="INFO")
print("Latest version of {} already downloaded.".format(PRODUCT_NAME))
return destination_path return destination_path
print("") self.utils.log_message("[GATHERING FILES] {} {}...".format("Updating" if product_history_index is not None else "Please wait for download", PRODUCT_NAME), level="INFO")
print("Updating" if product_history_index is not None else "Please wait for download", end=" ")
print("{}...".format(PRODUCT_NAME))
print("")
print("from {}".format(product_download_url))
print("")
if not self.fetcher.download_and_save_file(product_download_url, destination_path, sha256_hash): if not self.fetcher.download_and_save_file(product_download_url, destination_path, sha256_hash):
manual_download_url = f"https://github.com/{REPO_OWNER}/{REPO_NAME}/releases/latest" manual_download_url = "https://github.com/{}/{}/releases/latest".format(REPO_OWNER, REPO_NAME)
print("Go to {} to download {} manually.".format(manual_download_url, PRODUCT_NAME)) show_info("Download Failed", "Go to {} to download {} manually.".format(manual_download_url, PRODUCT_NAME))
print("")
self.utils.request_input()
raise Exception("Failed to download {}.".format(PRODUCT_NAME)) raise Exception("Failed to download {}.".format(PRODUCT_NAME))
self._update_download_history(download_history, PRODUCT_NAME, product_id, product_download_url, sha256_hash) self._update_download_history(download_history, PRODUCT_NAME, product_id, product_download_url, sha256_hash)

View File

@@ -4,9 +4,9 @@ import random
import json import json
class Github: class Github:
def __init__(self): def __init__(self, utils_instance=None, resource_fetcher_instance=None):
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
self.fetcher = resource_fetcher.ResourceFetcher() self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
def extract_payload(self, response): def extract_payload(self, response):
for line in response.splitlines(): for line in response.splitlines():

View File

@@ -1,12 +1,38 @@
from Scripts.datasets import os_data from Scripts.datasets import os_data
from Scripts.datasets import pci_data from Scripts.datasets import pci_data
from Scripts import compatibility_checker from Scripts.custom_dialogs import show_confirmation, show_info, show_options_dialog
from Scripts import utils from Scripts import utils
class HardwareCustomizer: class HardwareCustomizer:
def __init__(self): def __init__(self, utils_instance=None):
self.compatibility_checker = compatibility_checker.CompatibilityChecker() self.utils = utils_instance if utils_instance else utils.Utils()
self.utils = utils.Utils()
def show_macos_compatibility(self, device_compatibility):
if not device_compatibility:
return "<span style='color:gray'>Unchecked</span>"
if not device_compatibility[0]:
return "<span style='color:red'>Unsupported</span>"
max_compatibility = self.utils.parse_darwin_version(device_compatibility[0])[0]
min_compatibility = self.utils.parse_darwin_version(device_compatibility[-1])[0]
max_version = self.utils.parse_darwin_version(os_data.get_latest_darwin_version())[0]
min_version = self.utils.parse_darwin_version(os_data.get_lowest_darwin_version())[0]
if max_compatibility == min_version:
return "<span style='color:blue'>Maximum support up to {}</span>".format(
os_data.get_macos_name_by_darwin(device_compatibility[-1])
)
if min_version < min_compatibility or max_compatibility < max_version:
return "<span style='color:green'>{} to {}</span>".format(
os_data.get_macos_name_by_darwin(device_compatibility[-1]),
os_data.get_macos_name_by_darwin(device_compatibility[0])
)
return "<span style='color:blue'>Up to {}</span>".format(
os_data.get_macos_name_by_darwin(device_compatibility[0])
)
def hardware_customization(self, hardware_report, macos_version): def hardware_customization(self, hardware_report, macos_version):
self.hardware_report = hardware_report self.hardware_report = hardware_report
@@ -16,7 +42,7 @@ class HardwareCustomizer:
self.selected_devices = {} self.selected_devices = {}
needs_oclp = False needs_oclp = False
self.utils.head("Hardware Customization") self.utils.log_message("[HARDWARE CUSTOMIZATION] Starting hardware customization", level="INFO")
for device_type, devices in self.hardware_report.items(): for device_type, devices in self.hardware_report.items():
if not device_type in ("BIOS", "GPU", "Sound", "Biometric", "Network", "Storage Controllers", "Bluetooth", "SD Controller"): if not device_type in ("BIOS", "GPU", "Sound", "Biometric", "Network", "Storage Controllers", "Bluetooth", "SD Controller"):
@@ -27,24 +53,20 @@ class HardwareCustomizer:
if device_type == "BIOS": if device_type == "BIOS":
self.customized_hardware[device_type] = devices.copy() self.customized_hardware[device_type] = devices.copy()
if devices.get("Firmware Type") != "UEFI":
print("\n*** BIOS Firmware Type is not UEFI")
print("")
print("Do you want to build the EFI for UEFI?")
print("If yes, please make sure to update your BIOS and enable UEFI Boot Mode in your BIOS settings.")
print("You can still proceed with Legacy if you prefer.")
print("")
while True: if devices.get("Firmware Type") != "UEFI":
answer = self.utils.request_input("Build EFI for UEFI? (Yes/no): ").strip().lower() content = (
if answer == "yes": "Would you like to build the EFI for UEFI?<br>"
self.customized_hardware[device_type]["Firmware Type"] = "UEFI" "If yes, please make sure to update your BIOS and enable UEFI Boot Mode in your BIOS settings.<br>"
break "You can still proceed with Legacy if you prefer."
elif answer == "no": )
self.customized_hardware[device_type]["Firmware Type"] = "Legacy" if show_confirmation("BIOS Firmware Type is not UEFI", content):
break self.utils.log_message("[HARDWARE CUSTOMIZATION] BIOS Firmware Type is not UEFI, building EFI for UEFI", level="INFO")
else: self.customized_hardware[device_type]["Firmware Type"] = "UEFI"
print("\033[91mInvalid selection, please try again.\033[0m\n\n") else:
self.utils.log_message("[HARDWARE CUSTOMIZATION] BIOS Firmware Type is not UEFI, building EFI for Legacy", level="INFO")
self.customized_hardware[device_type]["Firmware Type"] = "Legacy"
continue continue
for device_name in devices: for device_name in devices:
@@ -72,21 +94,27 @@ class HardwareCustomizer:
self._handle_device_selection(device_type if device_type != "Network" else "WiFi") self._handle_device_selection(device_type if device_type != "Network" else "WiFi")
if self.selected_devices: if self.selected_devices:
self.utils.head("Device Selection Summary") content = "The following devices have been selected for your configuration:<br>"
print("") content += "<table width='100%' cellpadding='4'>"
print("Selected devices:") content += "<tr>"
print("") content += "<td><b>Category</b></td>"
print("Type Device Device ID") content += "<td><b>Device Name</b></td>"
print("------------------------------------------------------------------") content += "<td><b>Device ID</b></td>"
content += "</tr>"
for device_type, device_dict in self.selected_devices.items(): for device_type, device_dict in self.selected_devices.items():
for device_name, device_props in device_dict.items(): for device_name, device_props in device_dict.items():
device_id = device_props.get("Device ID", "Unknown") device_id = device_props.get("Device ID", "Unknown")
print("{:<13} {:<42} {}".format(device_type, device_name[:38], device_id)) content += "<tr>"
print("") content += "<td>{}</td>".format(device_type)
print("All other devices of the same type have been disabled.") content += "<td>{}</td>".format(device_name)
print("") content += "<td>{}</td>".format(device_id)
self.utils.request_input() content += "</tr>"
content += "</table>"
content += "<p><i>Note: Unselected devices in these categories have been disabled.</i></p>"
show_info("Hardware Configuration Summary", content)
return self.customized_hardware, self.disabled_devices, needs_oclp return self.customized_hardware, self.disabled_devices, needs_oclp
def _get_device_combinations(self, device_indices): def _get_device_combinations(self, device_indices):
@@ -114,10 +142,12 @@ class HardwareCustomizer:
devices = self._get_compatible_devices(device_type) devices = self._get_compatible_devices(device_type)
device_groups = None device_groups = None
title = "Multiple {} Devices Detected".format(device_type)
content = []
if len(devices) > 1: if len(devices) > 1:
print("\n*** Multiple {} Devices Detected".format(device_type))
if device_type == "WiFi" or device_type == "Bluetooth": if device_type == "WiFi" or device_type == "Bluetooth":
print(f"macOS works best with only one {device_type} device enabled.") content.append("macOS works best with only one {} device enabled.<br>".format(device_type))
elif device_type == "GPU": elif device_type == "GPU":
_apu_index = None _apu_index = None
_navi_22_indices = set() _navi_22_indices = set()
@@ -148,7 +178,7 @@ class HardwareCustomizer:
_other_indices.add(index) _other_indices.add(index)
if _apu_index or _navi_22_indices: if _apu_index or _navi_22_indices:
print("Multiple active GPUs can cause kext conflicts in macOS.") content.append("Multiple active GPUs can cause kext conflicts in macOS.")
device_groups = [] device_groups = []
if _apu_index: if _apu_index:
@@ -158,7 +188,7 @@ class HardwareCustomizer:
if _navi_indices or _intel_gpu_indices or _other_indices: if _navi_indices or _intel_gpu_indices or _other_indices:
device_groups.append(_navi_indices | _intel_gpu_indices | _other_indices) device_groups.append(_navi_indices | _intel_gpu_indices | _other_indices)
selected_devices = self._select_device(device_type, devices, device_groups) selected_devices = self._select_device(device_type, devices, device_groups, title, content)
if selected_devices: if selected_devices:
for selected_device in selected_devices: for selected_device in selected_devices:
if not device_type in self.selected_devices: if not device_type in self.selected_devices:
@@ -185,14 +215,15 @@ class HardwareCustomizer:
return compatible_devices return compatible_devices
def _select_device(self, device_type, devices, device_groups=None): def _select_device(self, device_type, devices, device_groups=None, title=None, content=None):
print("") self.utils.log_message("[HARDWARE CUSTOMIZATION] Starting device selection for {}".format(device_type), level="INFO")
if device_groups: if device_groups:
print("Please select a {} combination configuration:".format(device_type)) content.append("Please select a {} combination configuration:".format(device_type))
else: else:
print("Please select which {} device you want to use:".format(device_type)) content.append("Please select which {} device you want to use:".format(device_type))
print("")
options = []
if device_groups: if device_groups:
valid_combinations = [] valid_combinations = []
@@ -230,67 +261,48 @@ class HardwareCustomizer:
valid_combinations.sort(key=lambda x: (len(x[0]), x[2][0])) valid_combinations.sort(key=lambda x: (len(x[0]), x[2][0]))
for idx, (group_devices, _, group_compatibility) in enumerate(valid_combinations, start=1): for group_devices, group_indices, group_compatibility in valid_combinations:
print("{}. {}".format(idx, " + ".join(group_devices))) option = "<b>{}</b>".format(" + ".join(group_devices))
if group_compatibility: if group_compatibility:
print(" Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility(group_compatibility))) option += "<br>Compatibility: {}".format(self.show_macos_compatibility(group_compatibility))
if len(group_devices) == 1: if len(group_devices) == 1:
device_props = devices[group_devices[0]] device_props = devices[group_devices[0]]
if device_props.get("OCLP Compatibility"): if device_props.get("OCLP Compatibility"):
oclp_compatibility = device_props.get("OCLP Compatibility") option += "<br>OCLP Compatibility: {}".format(self.show_macos_compatibility((device_props.get("OCLP Compatibility")[0], os_data.get_lowest_darwin_version())))
if self.utils.parse_darwin_version(oclp_compatibility[0]) > self.utils.parse_darwin_version(group_compatibility[0]): options.append(option)
print(" OCLP Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version()))))
print("")
while True:
choice = self.utils.request_input(f"Select a {device_type} combination (1-{len(valid_combinations)}): ")
try:
choice_num = int(choice)
if 1 <= choice_num <= len(valid_combinations):
selected_devices, _, _ = valid_combinations[choice_num - 1]
for device in devices:
if device not in selected_devices:
self._disable_device(device_type, device, devices[device])
return selected_devices
else:
print("Invalid option. Please try again.")
except ValueError:
print("Please enter a valid number.")
else: else:
for index, device_name in enumerate(devices, start=1): for device_name, device_props in devices.items():
device_props = devices[device_name]
compatibility = device_props.get("Compatibility") compatibility = device_props.get("Compatibility")
print("{}. {}".format(index, device_name)) option = "<b>{}</b>".format(device_name)
print(" Device ID: {}".format(device_props.get("Device ID", "Unknown"))) option += "<br>Device ID: {}".format(device_props.get("Device ID", "Unknown"))
print(" Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility(compatibility))) option += "<br>Compatibility: {}".format(self.show_macos_compatibility(compatibility))
if device_props.get("OCLP Compatibility"): if device_props.get("OCLP Compatibility"):
oclp_compatibility = device_props.get("OCLP Compatibility") oclp_compatibility = device_props.get("OCLP Compatibility")
if self.utils.parse_darwin_version(oclp_compatibility[0]) > self.utils.parse_darwin_version(compatibility[0]): if self.utils.parse_darwin_version(oclp_compatibility[0]) > self.utils.parse_darwin_version(compatibility[0]):
print(" OCLP Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version())))) option += "<br>OCLP Compatibility: {}".format(self.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version())))
print() options.append(option)
self.utils.log_message("[HARDWARE CUSTOMIZATION] Options: {}".format(", ".join(option.split("<br>")[0].replace("<b>", "").replace("</b>", "").strip() for option in options)), level="INFO")
while True: while True:
choice = self.utils.request_input(f"Select a {device_type} device (1-{len(devices)}): ") choice_num = show_options_dialog(title, "<br>".join(content), options, default_index=len(options) - 1)
try: if choice_num is None:
choice_num = int(choice) continue
if 1 <= choice_num <= len(devices):
selected_device = list(devices)[choice_num - 1] if device_groups:
selected_devices, _, _ = valid_combinations[choice_num]
for device in devices: else:
if device != selected_device: selected_devices = [list(devices)[choice_num]]
self._disable_device(device_type, device, devices[device])
for device in devices:
return [selected_device] if device not in selected_devices:
else: self._disable_device(device_type, device, devices[device])
print("Invalid option. Please try again.")
except ValueError: self.utils.log_message("[HARDWARE CUSTOMIZATION] Selected devices: {}".format(", ".join(selected_devices)), level="INFO")
print("Please enter a valid number.") return selected_devices
def _disable_device(self, device_type, device_name, device_props): def _disable_device(self, device_type, device_name, device_props):
if device_type == "WiFi": if device_type == "WiFi":

View File

@@ -4,8 +4,8 @@ import json
from Scripts import utils from Scripts import utils
class IntegrityChecker: class IntegrityChecker:
def __init__(self): def __init__(self, utils_instance=None):
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
def get_sha256(self, file_path, block_size=65536): def get_sha256(self, file_path, block_size=65536):
if not os.path.exists(file_path) or os.path.isdir(file_path): if not os.path.exists(file_path) or os.path.isdir(file_path):
@@ -17,7 +17,7 @@ class IntegrityChecker:
sha256.update(block) sha256.update(block)
return sha256.hexdigest() return sha256.hexdigest()
def generate_folder_manifest(self, folder_path, manifest_path=None): def generate_folder_manifest(self, folder_path, manifest_path=None, save_manifest=True):
if not os.path.isdir(folder_path): if not os.path.isdir(folder_path):
return None return None
@@ -26,8 +26,15 @@ class IntegrityChecker:
manifest_data = {} manifest_data = {}
for root, _, files in os.walk(folder_path): for root, _, files in os.walk(folder_path):
if '.git' in root or "__pycache__" in root:
continue
for name in files: for name in files:
if '.git' in name or ".pyc" in name:
continue
file_path = os.path.join(root, name) file_path = os.path.join(root, name)
relative_path = os.path.relpath(file_path, folder_path).replace('\\', '/') relative_path = os.path.relpath(file_path, folder_path).replace('\\', '/')
if relative_path == os.path.basename(manifest_path): if relative_path == os.path.basename(manifest_path):
@@ -35,7 +42,8 @@ class IntegrityChecker:
manifest_data[relative_path] = self.get_sha256(file_path) manifest_data[relative_path] = self.get_sha256(file_path)
self.utils.write_file(manifest_path, manifest_data) if save_manifest:
self.utils.write_file(manifest_path, manifest_data)
return manifest_data return manifest_data
def verify_folder_integrity(self, folder_path, manifest_path=None): def verify_folder_integrity(self, folder_path, manifest_path=None):
@@ -83,4 +91,4 @@ class IntegrityChecker:
is_valid = not any(issues.values()) is_valid = not any(issues.values())
return is_valid, issues return is_valid, issues

View File

@@ -6,6 +6,7 @@ from Scripts.datasets import codec_layouts
from Scripts import utils from Scripts import utils
import os import os
import shutil import shutil
import random
try: try:
long long
@@ -14,9 +15,11 @@ except NameError:
long = int long = int
unicode = str unicode = str
from Scripts.custom_dialogs import show_options_dialog, show_info, show_confirmation, show_checklist_dialog
class KextMaestro: class KextMaestro:
def __init__(self): def __init__(self, utils_instance=None):
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
self.matching_keys = [ self.matching_keys = [
"IOPCIMatch", "IOPCIMatch",
"IONameMatch", "IONameMatch",
@@ -77,6 +80,52 @@ class KextMaestro:
return False return False
def _select_audio_codec_layout(self, hardware_report, default_layout_id=None):
codec_id = None
audio_controller_properties = None
for codec_properties in hardware_report.get("Sound", {}).values():
if codec_properties.get("Device ID") in codec_layouts.data:
codec_id = codec_properties.get("Device ID")
if codec_properties.get("Controller Device ID"):
for device_name, device_properties in hardware_report.get("System Devices", {}).items():
if device_properties.get("Device ID") == codec_properties.get("Controller Device ID"):
audio_controller_properties = device_properties
break
break
available_layouts = codec_layouts.data.get(codec_id)
if not available_layouts:
return None, None
options = []
default_index = 0
if default_layout_id is None:
recommended_authors = ("Mirone", "InsanelyDeepak", "Toleda", "DalianSky")
recommended_layouts = [layout for layout in available_layouts if self.utils.contains_any(recommended_authors, layout.comment)]
default_layout_id = random.choice(recommended_layouts or available_layouts).id
for i, layout in enumerate(available_layouts):
options.append("{} - {}".format(layout.id, layout.comment))
if layout.id == default_layout_id:
default_index = i
while True:
content = "For best audio quality, please try multiple layouts to determine which works best with your hardware in post-install."
selected_index = show_options_dialog(
title="Choosing Codec Layout ID",
content=content,
options=options,
default_index=default_index
)
if selected_index is not None:
return available_layouts[selected_index].id, audio_controller_properties
def check_kext(self, index, target_darwin_version, allow_unsupported_kexts=False): def check_kext(self, index, target_darwin_version, allow_unsupported_kexts=False):
kext = self.kexts[index] kext = self.kexts[index]
@@ -96,9 +145,7 @@ class KextMaestro:
other_kext.checked = False other_kext.checked = False
def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi_patches): def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi_patches):
self.utils.head("Select Required Kernel Extensions") self.utils.log_message("[KEXT MAESTRO] Checking for required kernel extensions...", level="INFO")
print("")
print("Checking for required kernel extensions...")
for kext in self.kexts: for kext in self.kexts:
kext.checked = kext.required kext.checked = kext.required
@@ -122,24 +169,26 @@ class KextMaestro:
for codec_properties in hardware_report.get("Sound", {}).values(): for codec_properties in hardware_report.get("Sound", {}).values():
if codec_properties.get("Device ID") in codec_layouts.data: if codec_properties.get("Device ID") in codec_layouts.data:
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"): if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
print("\n\033[1;93mNote:\033[0m Since macOS Tahoe 26 DP2, Apple has removed AppleHDA kext and uses the Apple T2 chip for audio management.") content = (
print("To use AppleALC, you must rollback AppleHDA. Alternatively, you can use VoodooHDA.") "Since macOS Tahoe 26 DP2, Apple has removed AppleHDA and uses the Apple T2 chip for audio management.<br>"
print("") "Therefore, AppleALC is no longer functional until you rollback AppleHDA."
print("1. \033[1mAppleALC\033[0m - Requires AppleHDA rollback with \033[1;93mOpenCore Legacy Patcher\033[0m") )
print("2. \033[1mVoodooHDA\033[0m - Lower audio quality, manual injection to /Library/Extensions") options = [
print("") "<b>AppleALC</b> - Requires rollback AppleHDA with <b>OpenCore Legacy Patcher</b>",
while True: "<b>VoodooHDA</b> - Lower audio quality than use AppleHDA, injection kext into <b>/Library/Extensions</b>"
kext_option = self.utils.request_input("Select audio kext for your system: ").strip() ]
if kext_option == "1": result = show_options_dialog("Audio Kext Selection", content, options, default_index=0)
needs_oclp = True if result == 0:
selected_kexts.append("AppleALC") needs_oclp = True
break selected_kexts.append("AppleALC")
elif kext_option == "2":
break
else:
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
else: else:
selected_kexts.append("AppleALC") selected_kexts.append("AppleALC")
if "AppleALC" in selected_kexts:
audio_layout_id, audio_controller_properties = self._select_audio_codec_layout(hardware_report)
else:
audio_layout_id = None
audio_controller_properties = None
if "AMD" in hardware_report.get("CPU").get("Manufacturer") and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("21.4.0") or \ if "AMD" in hardware_report.get("CPU").get("Manufacturer") and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("21.4.0") or \
int(hardware_report.get("CPU").get("CPU Count")) > 1 and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("19.0.0"): int(hardware_report.get("CPU").get("CPU Count")) > 1 and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("19.0.0"):
@@ -166,66 +215,54 @@ class KextMaestro:
break break
if gpu_props.get("Codename") in {"Navi 21", "Navi 23"}: if gpu_props.get("Codename") in {"Navi 21", "Navi 23"}:
print("\n*** Found {} is AMD {} GPU.".format(gpu_name, gpu_props.get("Codename"))) content = (
print("") "<span style='color:red font-weight:bold'>Important: Black Screen Fix</span><br>"
print("\033[91mImportant: Black Screen Fix\033[0m") "If you experience a black screen after verbose mode:<br>"
print("If you experience a black screen after verbose mode:") "1. Use ProperTree to open config.plist<br>"
print(" 1. Use ProperTree to open config.plist") "2. Navigate to NVRAM -> Add -> 7C436110-AB2A-4BBB-A880-FE41995C9F82 -> boot-args<br>"
print(" 2. Navigate to NVRAM -> Add -> 7C436110-AB2A-4BBB-A880-FE41995C9F82 -> boot-args") "3. Remove \"-v debug=0x100 keepsyms=1\" from boot-args<br><br>"
print(" 3. Remove \"-v debug=0x100 keepsyms=1\" from boot-args") ).format(gpu_name, gpu_props.get("Codename"))
print("")
options = [
"<b>NootRX</b> - Uses latest GPU firmware",
"<b>WhateverGreen</b> - Uses original Apple firmware",
]
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"): if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
recommended_option = 1 content += (
recommended_name = "NootRX" "Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD {} GPUs.<br>"
max_option = 3 "To avoid this, you can use NootRX or choose not to install a GPU kext."
print("\033[1;93mNote:\033[0m Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD {} GPUs.".format(gpu_props.get("Codename"))) ).format(gpu_props.get("Codename"))
print("To avoid this, you can use NootRX or choose not to install a GPU kext.") options.append("<b>Don't use any kext</b>")
print("") recommended_option = 0
print("1. \033[1mNootRX\033[0m - Uses latest GPU firmware")
print("2. \033[1mWhateverGreen\033[0m - Uses original Apple firmware")
print("3. \033[1mDon't use any kext\033[0m")
else: else:
recommended_option = 2 content += (
recommended_name = "WhateverGreen" "AMD {} GPUs have two available kext options:<br>"
max_option = 2 "You can try different kexts after installation to find the best one for your system."
print("\033[1;93mNote:\033[0m") ).format(gpu_props.get("Codename"))
print("- AMD {} GPUs have two available kext options:".format(gpu_props.get("Codename"))) recommended_option = 1
print("- You can try different kexts after installation to find the best one for your system")
print("")
print("1. \033[1mNootRX\033[0m - Uses latest GPU firmware")
print("2. \033[1mWhateverGreen\033[0m - Uses original Apple firmware")
print("")
if any(other_gpu_props.get("Manufacturer") == "Intel" for other_gpu_props in hardware_report.get("GPU", {}).values()): if any(other_gpu_props.get("Manufacturer") == "Intel" for other_gpu_props in hardware_report.get("GPU", {}).values()):
print("\033[91mImportant:\033[0m NootRX kext is not compatible with Intel GPUs") show_info("NootRX Kext Warning", "NootRX kext is not compatible with Intel GPUs.<br>Automatically selecting WhateverGreen kext due to Intel GPU compatibility.")
print("Automatically selecting WhateverGreen kext due to Intel GPU compatibility") selected_kexts.append("WhateverGreen")
print("")
self.utils.request_input("Press Enter to continue...")
continue continue
kext_option = self.utils.request_input("Select kext for your AMD {} GPU (default: {}): ".format(gpu_props.get("Codename"), recommended_name)).strip() or str(recommended_option) result = show_options_dialog("AMD GPU Kext Selection", content, options, default_index=recommended_option)
if kext_option.isdigit() and 0 < int(kext_option) < max_option + 1: if result == 0:
selected_option = int(kext_option)
else:
print("\033[93mInvalid selection, using recommended option: {}\033[0m".format(recommended_option))
selected_option = recommended_option
if selected_option == 1:
selected_kexts.append("NootRX") selected_kexts.append("NootRX")
elif selected_option == 2: elif result == 1:
selected_kexts.append("WhateverGreen") selected_kexts.append("WhateverGreen")
continue continue
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"): if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
print("\n*** Found {} is AMD {} GPU.".format(gpu_name, gpu_props.get("Codename"))) content = (
print("") "Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD GPUs.<br>"
print("\033[1;93mNote:\033[0m Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD GPUs.") "The current recommendation is to not use WhateverGreen.<br>"
print("The current recommendation is to not use WhateverGreen.") "However, you can still try adding it to see if it works on your system."
print("However, you can still try adding it to see if it works on your system.") )
print("") show_info("AMD GPU Kext Warning", content)
self.utils.request_input("Press Enter to continue...")
break break
selected_kexts.append("WhateverGreen") selected_kexts.append("WhateverGreen")
@@ -252,44 +289,35 @@ class KextMaestro:
elif device_id in pci_data.BroadcomWiFiIDs[16:18] and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("20.0.0"): elif device_id in pci_data.BroadcomWiFiIDs[16:18] and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("20.0.0"):
selected_kexts.append("AirportBrcmFixup") selected_kexts.append("AirportBrcmFixup")
elif device_id in pci_data.IntelWiFiIDs: elif device_id in pci_data.IntelWiFiIDs:
print("\n*** Found {} is Intel WiFi device.".format(network_name)) airport_itlwm_content = (
print("") "<b>AirportItlwm</b> - Uses native WiFi settings menu<br>"
print("\033[1;93mNote:\033[0m Intel WiFi devices have two available kext options:") "• Provides Handoff, Universal Clipboard, Location Services, Instant Hotspot support<br>"
print("") "• Supports enterprise-level security<br>"
print("1. \033[1mAirportItlwm\033[0m - Uses native WiFi settings menu") )
print(" • Provides Handoff, Universal Clipboard, Location Services, Instant Hotspot support")
print(" • Supports enterprise-level security")
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"): if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"):
print("\033[91mSince macOS Sequoia 15\033[0m: Can work with OCLP root patch but may cause issues") airport_itlwm_content += "• <span style='color:red'>Since macOS Sequoia 15</span>: Can work with OCLP root patch but may cause issues"
elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"): elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"):
print("\033[91mOn macOS Sonoma 14\033[0m: iServices won't work unless using OCLP root patch") airport_itlwm_content += "• <span style='color:red'>On macOS Sonoma 14</span>: iServices won't work unless using OCLP root patch"
print("")
print("2. \033[1mitlwm\033[0m - More stable overall")
print(" • Works with HeliPort app instead of native WiFi settings menu")
print(" • No Apple Continuity features and enterprise-level security")
print(" • Can connect to Hidden Networks")
print("")
recommended_option = 2 if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0") else 1 itlwm_content = (
recommended_name = "itlwm" if recommended_option == 2 else "AirportItlwm" "<b>itlwm</b> - More stable overall<br>"
"• Works with <b>HeliPort</b> app instead of native WiFi settings menu<br>"
"• No Apple Continuity features and enterprise-level security<br>"
"• Can connect to Hidden Networks"
)
recommended_option = 1 if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0") else 0
options = [airport_itlwm_content, itlwm_content]
if "Beta" in os_data.get_macos_name_by_darwin(macos_version): if "Beta" in os_data.get_macos_name_by_darwin(macos_version):
print("\033[91mImportant:\033[0m For macOS Beta versions, only itlwm kext is supported") show_info("Intel WiFi Kext Selection", "For macOS Beta versions, only itlwm kext is supported.")
print("") selected_option = 1
self.utils.request_input("Press Enter to continue...")
selected_option = recommended_option
else: else:
kext_option = self.utils.request_input("Select kext for your Intel WiFi device (default: {}): ".format(recommended_name)).strip() or str(recommended_option) result = show_options_dialog("Intel WiFi Kext Selection", "Intel WiFi devices have two available kext options:", options, default_index=recommended_option)
selected_option = result if result is not None else recommended_option
if kext_option.isdigit() and 0 < int(kext_option) < 3:
selected_option = int(kext_option) if selected_option == 1:
else:
print("\033[91mInvalid selection, using recommended option: {}\033[0m".format(recommended_option))
selected_option = recommended_option
if selected_option == 2:
selected_kexts.append("itlwm") selected_kexts.append("itlwm")
else: else:
selected_kexts.append("AirportItlwm") selected_kexts.append("AirportItlwm")
@@ -297,18 +325,12 @@ class KextMaestro:
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"): if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"):
selected_kexts.append("IOSkywalkFamily") selected_kexts.append("IOSkywalkFamily")
elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"): elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"):
print("") content = (
print("\033[1;93mNote:\033[0m Since macOS Sonoma 14, iServices won't work with AirportItlwm without patches") "Since macOS Sonoma 14, iServices won't work with AirportItlwm without patches.<br><br>"
print("") "Apply OCLP root patch to fix iServices?"
while True: )
option = self.utils.request_input("Apply OCLP root patch to fix iServices? (yes/No): ").strip().lower() if show_confirmation("OpenCore Legacy Patcher Required", content):
if option == "yes": selected_kexts.append("IOSkywalkFamily")
selected_kexts.append("IOSkywalkFamily")
break
elif option == "no":
break
else:
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
elif device_id in pci_data.AtherosWiFiIDs[:8]: elif device_id in pci_data.AtherosWiFiIDs[:8]:
selected_kexts.append("corecaptureElCap") selected_kexts.append("corecaptureElCap")
if self.utils.parse_darwin_version(macos_version) > self.utils.parse_darwin_version("20.99.99"): if self.utils.parse_darwin_version(macos_version) > self.utils.parse_darwin_version("20.99.99"):
@@ -424,7 +446,7 @@ class KextMaestro:
for name in selected_kexts: for name in selected_kexts:
self.check_kext(kext_data.kext_index_by_name.get(name), macos_version, allow_unsupported_kexts) self.check_kext(kext_data.kext_index_by_name.get(name), macos_version, allow_unsupported_kexts)
return needs_oclp return needs_oclp, audio_layout_id, audio_controller_properties
def install_kexts_to_efi(self, macos_version, kexts_directory): def install_kexts_to_efi(self, macos_version, kexts_directory):
for kext in self.kexts: for kext in self.kexts:
@@ -639,72 +661,65 @@ class KextMaestro:
if not incompatible_kexts: if not incompatible_kexts:
return False return False
while True: content = (
self.utils.head("Kext Compatibility Check") "List of incompatible kexts for the current macOS version ({}):<br>"
print("\nIncompatible kexts for the current macOS version ({}):\n".format(target_darwin_version)) "<ul>"
).format(target_darwin_version)
for index, (kext_name, is_lilu_dependent) in enumerate(incompatible_kexts, start=1):
print("{:2}. {:25}{}".format(index, kext_name, " - Lilu Plugin" if is_lilu_dependent else "")) for index, (kext_name, is_lilu_dependent) in enumerate(incompatible_kexts):
content += "<li><b>{}. {}</b>".format(index + 1, kext_name)
print("\n\033[1;93mNote:\033[0m") if is_lilu_dependent:
print("- With Lilu plugins, using the \"-lilubetaall\" boot argument will force them to load.") content += " - Lilu Plugin"
print("- Forcing unsupported kexts can cause system instability. \033[0;31mProceed with caution.\033[0m") content += "</li>"
print("")
content += (
option = self.utils.request_input("Do you want to force load {} on the unsupported macOS version? (yes/No): ".format("these kexts" if len(incompatible_kexts) > 1 else "this kext")) "</ul><br>"
"<b>Note:</b><br>"
if option.lower() == "yes": "• With Lilu plugins, using the \"-lilubetaall\" boot argument will force them to load.<br>"
return True "• Forcing unsupported kexts can cause system instability. <b><span style='color:red'>Proceed with caution.</span></b><br><br>"
elif option.lower() == "no": "Do you want to force load {} on the unsupported macOS version?"
return False ).format("these kexts" if len(incompatible_kexts) > 1 else "this kext")
return show_confirmation("Incompatible Kexts", content, yes_text="Yes", no_text="No")
def kext_configuration_menu(self, macos_version): def kext_configuration_menu(self, macos_version):
current_category = None content = (
"Select kernel extensions (kexts) for your system.<br>"
"Grayed-out items are not supported by the current macOS version ({}).<br><br>"
"<b>Note:</b><br>"
"• When a plugin of a kext is selected, the entire kext will be automatically selected."
).format(macos_version)
checklist_items = []
for kext in self.kexts:
is_supported = self.utils.parse_darwin_version(kext.min_darwin_version) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(kext.max_darwin_version)
display_text = "{} - {}".format(kext.name, kext.description)
if not is_supported:
display_text += " (Unsupported)"
checklist_items.append({
"label": display_text,
"category": kext.category if kext.category else "Uncategorized",
"supported": is_supported
})
checked_indices = [i for i, kext in enumerate(self.kexts) if kext.checked]
selected_indices = show_checklist_dialog("Configure Kernel Extensions", content, checklist_items, checked_indices)
self.utils.log_message("[KEXT MAESTRO] Selected kexts: {}".format(selected_indices), level="INFO")
if selected_indices is None:
return
while True: newly_checked = [i for i in selected_indices if i not in checked_indices]
contents = []
contents.append("") allow_unsupported_kexts = self.verify_kext_compatibility(newly_checked, macos_version)
contents.append("List of available kexts:")
for index, kext in enumerate(self.kexts, start=1): for i, kext in enumerate(self.kexts):
if kext.category != current_category: if i not in selected_indices and kext.checked and not kext.required:
current_category = kext.category self.uncheck_kext(i)
category_header = "Category: {}".format(current_category if current_category else "Uncategorized")
contents.append(f"\n{category_header}\n" + "=" * len(category_header)) for i in selected_indices:
checkbox = "[*]" if kext.checked else "[ ]" self.check_kext(i, macos_version, allow_unsupported_kexts)
line = "{} {:2}. {:35} - {:60}".format(checkbox, index, kext.name, kext.description)
if kext.checked:
line = "\033[1;32m{}\033[0m".format(line)
elif not self.utils.parse_darwin_version(kext.min_darwin_version) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(kext.max_darwin_version):
line = "\033[90m{}\033[0m".format(line)
contents.append(line)
contents.append("")
contents.append("\033[1;93mNote:\033[0m")
contents.append("- Lines in gray indicate kexts that are not supported by the current macOS version ({}).".format(macos_version))
contents.append("- When a plugin of a kext is selected, the entire kext will be automatically selected.")
contents.append("- You can select multiple kexts by entering their indices separated by commas (e.g., '1, 2, 3').")
contents.append("")
contents.append("B. Back")
contents.append("Q. Quit")
contents.append("")
content = "\n".join(contents)
self.utils.adjust_window_size(content)
self.utils.head("Configure Kernel Extensions", resize=False)
print(content)
option = self.utils.request_input("Select your option: ")
if option.lower() == "b":
return
if option.lower() == "q":
self.utils.exit_program()
indices = [int(i.strip()) -1 for i in option.split(",") if i.strip().isdigit()]
allow_unsupported_kexts = self.verify_kext_compatibility(indices, macos_version)
for index in indices:
if index >= 0 and index < len(self.kexts):
kext = self.kexts[index]
if kext.checked and not kext.required:
self.uncheck_kext(index)
else:
self.check_kext(index, macos_version, allow_unsupported_kexts)

15
Scripts/pages/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
from .home_page import HomePage
from .select_hardware_report_page import SelectHardwareReportPage
from .compatibility_page import CompatibilityPage
from .configuration_page import ConfigurationPage
from .build_page import BuildPage
from .settings_page import SettingsPage
__all__ = [
"HomePage",
"SelectHardwareReportPage",
"CompatibilityPage",
"ConfigurationPage",
"BuildPage",
"SettingsPage",
]

552
Scripts/pages/build_page.py Normal file
View File

@@ -0,0 +1,552 @@
import platform
import os
import shutil
import threading
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel
from qfluentwidgets import (
SubtitleLabel, BodyLabel, CardWidget, TextEdit,
StrongBodyLabel, ProgressBar, PrimaryPushButton, FluentIcon,
ScrollArea
)
from Scripts.datasets import chipset_data
from Scripts.datasets import kext_data
from Scripts.custom_dialogs import show_confirmation
from Scripts.styles import SPACING, COLORS, RADIUS
from Scripts import ui_utils
from Scripts.widgets.config_editor import ConfigEditor
class BuildPage(ScrollArea):
build_progress_signal = pyqtSignal(str, list, int, int, bool)
build_complete_signal = pyqtSignal(bool, object)
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("buildPage")
self.controller = parent
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.build_in_progress = False
self.build_successful = False
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.enableTransparentBackground()
self._init_ui()
self._connect_signals()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self.ui_utils.create_step_indicator(4))
header_layout = QVBoxLayout()
header_layout.setSpacing(SPACING["small"])
title = SubtitleLabel("Build OpenCore EFI")
subtitle = BodyLabel("Build your customized OpenCore EFI ready for installation")
subtitle.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
header_layout.addWidget(title)
header_layout.addWidget(subtitle)
self.expandLayout.addLayout(header_layout)
self.expandLayout.addSpacing(SPACING["medium"])
self.instructions_after_content = QWidget()
self.instructions_after_content_layout = QVBoxLayout(self.instructions_after_content)
self.instructions_after_content_layout.setContentsMargins(0, 0, 0, 0)
self.instructions_after_content_layout.setSpacing(SPACING["medium"])
self.instructions_after_build_card = self.ui_utils.custom_card(
card_type="warning",
title="Before Using Your EFI",
body="Please complete these important steps before using the built EFI:",
custom_widget=self.instructions_after_content,
parent=self.scrollWidget
)
self.instructions_after_build_card.setVisible(False)
self.expandLayout.addWidget(self.instructions_after_build_card)
build_control_card = CardWidget(self.scrollWidget)
build_control_card.setBorderRadius(RADIUS["card"])
build_control_layout = QVBoxLayout(build_control_card)
build_control_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
build_control_layout.setSpacing(SPACING["medium"])
title = StrongBodyLabel("Build Control")
build_control_layout.addWidget(title)
btn_layout = QHBoxLayout()
btn_layout.setSpacing(SPACING["medium"])
self.build_btn = PrimaryPushButton(FluentIcon.DEVELOPER_TOOLS, "Build OpenCore EFI")
self.build_btn.clicked.connect(self.start_build)
btn_layout.addWidget(self.build_btn)
self.controller.build_btn = self.build_btn
self.open_result_btn = PrimaryPushButton(FluentIcon.FOLDER, "Open Result Folder")
self.open_result_btn.clicked.connect(self.open_result)
self.open_result_btn.setEnabled(False)
btn_layout.addWidget(self.open_result_btn)
self.controller.open_result_btn = self.open_result_btn
build_control_layout.addLayout(btn_layout)
self.progress_container = QWidget()
progress_layout = QVBoxLayout(self.progress_container)
progress_layout.setContentsMargins(0, SPACING["small"], 0, 0)
progress_layout.setSpacing(SPACING["medium"])
status_row = QHBoxLayout()
status_row.setSpacing(SPACING["medium"])
self.status_icon_label = QLabel()
self.status_icon_label.setFixedSize(28, 28)
status_row.addWidget(self.status_icon_label)
self.progress_label = StrongBodyLabel("Ready to build")
self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["text_secondary"]))
status_row.addWidget(self.progress_label)
status_row.addStretch()
progress_layout.addLayout(status_row)
self.progress_bar = ProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setFixedHeight(10)
self.progress_bar.setTextVisible(True)
self.controller.progress_bar = self.progress_bar
progress_layout.addWidget(self.progress_bar)
self.controller.progress_label = self.progress_label
self.progress_container.setVisible(False)
self.progress_helper = ui_utils.ProgressStatusHelper(
self.status_icon_label,
self.progress_label,
self.progress_bar,
self.progress_container
)
build_control_layout.addWidget(self.progress_container)
self.expandLayout.addWidget(build_control_card)
log_card = CardWidget(self.scrollWidget)
log_card.setBorderRadius(RADIUS["card"])
log_card_layout = QVBoxLayout(log_card)
log_card_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
log_card_layout.setSpacing(SPACING["medium"])
log_title = StrongBodyLabel("Build Log")
log_card_layout.addWidget(log_title)
log_description = BodyLabel("Detailed build process information and status updates")
log_description.setStyleSheet("color: {}; font-size: 13px;".format(COLORS["text_secondary"]))
log_card_layout.addWidget(log_description)
self.build_log = TextEdit()
self.build_log.setReadOnly(True)
self.build_log.setMinimumHeight(400)
self.build_log.setStyleSheet(f"""
TextEdit {{
background-color: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: {RADIUS["small"]}px;
padding: {SPACING["large"]}px;
font-family: "Consolas", "Monaco", "Courier New", monospace;
font-size: 13px;
line-height: 1.7;
}}
""")
self.controller.build_log = self.build_log
log_card_layout.addWidget(self.build_log)
self.log_card = log_card
self.log_card.setVisible(False)
self.expandLayout.addWidget(log_card)
self.config_editor = ConfigEditor(self.scrollWidget)
self.config_editor.setVisible(False)
self.expandLayout.addWidget(self.config_editor)
self.expandLayout.addStretch()
def _connect_signals(self):
self.build_progress_signal.connect(self._handle_build_progress)
self.build_complete_signal.connect(self._handle_build_complete)
def _handle_build_progress(self, title, steps, current_step_index, progress, done):
status = "success" if done else "loading"
if done:
message = "{} complete!".format(title)
else:
step_text = steps[current_step_index] if current_step_index < len(steps) else "Processing"
step_counter = "Step {}/{}".format(current_step_index + 1, len(steps))
message = "{}: {}...".format(step_counter, step_text)
if done:
final_progress = 100
else:
if "Building" in title:
final_progress = 40 + int(progress * 0.6)
else:
final_progress = progress
if hasattr(self, "progress_helper"):
self.progress_helper.update(status, message, final_progress)
if done:
self.controller.backend.u.log_message("[BUILD] {} complete!".format(title), "SUCCESS", to_build_log=True)
else:
step_text = steps[current_step_index] if current_step_index < len(steps) else "Processing"
self.controller.backend.u.log_message("[BUILD] Step {}/{}: {}...".format(current_step_index + 1, len(steps), step_text), "INFO", to_build_log=True)
def start_build(self):
if not self.controller.validate_prerequisites():
return
if self.controller.macos_state.needs_oclp:
content = (
"1. OpenCore Legacy Patcher allows restoring support for dropped GPUs and Broadcom WiFi on newer versions of macOS, and also enables AppleHDA on macOS Tahoe 26.<br>"
"2. OpenCore Legacy Patcher needs SIP disabled for applying custom kernel patches, which can cause instability, security risks and update issues.<br>"
"3. OpenCore Legacy Patcher does not officially support the Hackintosh community.<br><br>"
"<b><font color=\"{info_color}\">Support for macOS Tahoe 26:</font></b><br>"
"To patch macOS Tahoe 26, you must download OpenCore-Patcher 3.0.0 or newer from my repository: <a href=\"https://github.com/lzhoang2801/OpenCore-Legacy-Patcher/releases/tag/3.0.0\">lzhoang2801/OpenCore-Legacy-Patcher</a>.<br>"
"Official Dortania releases or older patches will NOT work with macOS Tahoe 26."
).format(error_color=COLORS["error"], info_color="#00BCD4")
if not show_confirmation("OpenCore Legacy Patcher Warning", content):
return
self.build_in_progress = True
self.build_successful = False
self.build_btn.setEnabled(False)
self.build_btn.setText("Building...")
self.open_result_btn.setEnabled(False)
self.progress_helper.update("loading", "Preparing to build...", 0)
self.instructions_after_build_card.setVisible(False)
self.build_log.clear()
self.log_card.setVisible(True)
thread = threading.Thread(target=self._start_build_thread, daemon=True)
thread.start()
def _start_build_thread(self):
try:
backend = self.controller.backend
backend.o.gather_bootloader_kexts(backend.k.kexts, self.controller.macos_state.darwin_version)
self._build_opencore_efi(
self.controller.hardware_state.customized_hardware,
self.controller.hardware_state.disabled_devices,
self.controller.smbios_state.model_name,
self.controller.macos_state.darwin_version,
self.controller.macos_state.needs_oclp
)
bios_requirements = self._check_bios_requirements(
self.controller.hardware_state.customized_hardware,
self.controller.hardware_state.customized_hardware
)
self.build_complete_signal.emit(True, bios_requirements)
except Exception as e:
self.build_complete_signal.emit(False, None)
def _check_bios_requirements(self, org_hardware_report, hardware_report):
requirements = []
org_firmware_type = org_hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown")
firmware_type = hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown")
if org_firmware_type == "Legacy" and firmware_type == "UEFI":
requirements.append("Enable UEFI mode (disable Legacy/CSM (Compatibility Support Module))")
secure_boot = hardware_report.get("BIOS", {}).get("Secure Boot", "Unknown")
if secure_boot != "Disabled":
requirements.append("Disable Secure Boot")
if hardware_report.get("Motherboard", {}).get("Platform") == "Desktop" and hardware_report.get("Motherboard", {}).get("Chipset") in chipset_data.IntelChipsets[112:]:
resizable_bar_enabled = any(gpu_props.get("Resizable BAR", "Disabled") == "Enabled" for gpu_props in hardware_report.get("GPU", {}).values())
if not resizable_bar_enabled:
requirements.append("Enable Above 4G Decoding")
requirements.append("Disable Resizable BAR/Smart Access Memory")
return requirements
def _build_opencore_efi(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp):
steps = [
"Copying EFI base to results folder",
"Applying ACPI patches",
"Copying kexts and snapshotting to config.plist",
"Generating config.plist",
"Cleaning up unused drivers, resources, and tools"
]
title = "Building OpenCore EFI"
current_step = 0
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
current_step += 1
backend = self.controller.backend
backend.u.create_folder(backend.result_dir, remove_content=True)
if not os.path.exists(backend.k.ock_files_dir):
raise Exception("Directory \"{}\" does not exist.".format(backend.k.ock_files_dir))
source_efi_dir = os.path.join(backend.k.ock_files_dir, "OpenCorePkg")
shutil.copytree(source_efi_dir, backend.result_dir, dirs_exist_ok=True)
config_file = os.path.join(backend.result_dir, "EFI", "OC", "config.plist")
config_data = backend.u.read_file(config_file)
if not config_data:
raise Exception("Error: The file {} does not exist.".format(config_file))
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
current_step += 1
config_data["ACPI"]["Add"] = []
config_data["ACPI"]["Delete"] = []
config_data["ACPI"]["Patch"] = []
acpi_directory = os.path.join(backend.result_dir, "EFI", "OC", "ACPI")
if backend.ac.ensure_dsdt():
backend.ac.hardware_report = hardware_report
backend.ac.disabled_devices = disabled_devices
backend.ac.acpi_directory = acpi_directory
backend.ac.smbios_model = smbios_model
backend.ac.lpc_bus_device = backend.ac.get_lpc_name()
for patch in backend.ac.patches:
if patch.checked:
if patch.name == "BATP":
patch.checked = getattr(backend.ac, patch.function_name)()
backend.k.kexts[kext_data.kext_index_by_name.get("ECEnabler")].checked = patch.checked
continue
acpi_load = getattr(backend.ac, patch.function_name)()
if not isinstance(acpi_load, dict):
continue
config_data["ACPI"]["Add"].extend(acpi_load.get("Add", []))
config_data["ACPI"]["Delete"].extend(acpi_load.get("Delete", []))
config_data["ACPI"]["Patch"].extend(acpi_load.get("Patch", []))
config_data["ACPI"]["Patch"].extend(backend.ac.dsdt_patches)
config_data["ACPI"]["Patch"] = backend.ac.apply_acpi_patches(config_data["ACPI"]["Patch"])
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
current_step += 1
kexts_directory = os.path.join(backend.result_dir, "EFI", "OC", "Kexts")
backend.k.install_kexts_to_efi(macos_version, kexts_directory)
config_data["Kernel"]["Add"] = backend.k.load_kexts(hardware_report, macos_version, kexts_directory)
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
current_step += 1
audio_layout_id = self.controller.hardware_state.audio_layout_id
audio_controller_properties = self.controller.hardware_state.audio_controller_properties
backend.co.genarate(
hardware_report,
disabled_devices,
smbios_model,
macos_version,
needs_oclp,
backend.k.kexts,
config_data,
audio_layout_id,
audio_controller_properties
)
backend.u.write_file(config_file, config_data)
progress = int((current_step / len(steps)) * 100)
self.build_progress_signal.emit(title, steps, current_step, progress, False)
files_to_remove = []
drivers_directory = os.path.join(backend.result_dir, "EFI", "OC", "Drivers")
driver_list = backend.u.find_matching_paths(drivers_directory, extension_filter=".efi")
driver_loaded = [kext.get("Path") for kext in config_data.get("UEFI").get("Drivers")]
for driver_path, type in driver_list:
if not driver_path in driver_loaded:
files_to_remove.append(os.path.join(drivers_directory, driver_path))
resources_audio_dir = os.path.join(backend.result_dir, "EFI", "OC", "Resources", "Audio")
if os.path.exists(resources_audio_dir):
files_to_remove.append(resources_audio_dir)
picker_variant = config_data.get("Misc", {}).get("Boot", {}).get("PickerVariant")
if picker_variant in (None, "Auto"):
picker_variant = "Acidanthera/GoldenGate"
if os.name == "nt":
picker_variant = picker_variant.replace("/", "\\")
resources_image_dir = os.path.join(backend.result_dir, "EFI", "OC", "Resources", "Image")
available_picker_variants = backend.u.find_matching_paths(resources_image_dir, type_filter="dir")
for variant_name, variant_type in available_picker_variants:
variant_path = os.path.join(resources_image_dir, variant_name)
if ".icns" in ", ".join(os.listdir(variant_path)):
if picker_variant not in variant_name:
files_to_remove.append(variant_path)
tools_directory = os.path.join(backend.result_dir, "EFI", "OC", "Tools")
tool_list = backend.u.find_matching_paths(tools_directory, extension_filter=".efi")
tool_loaded = [tool.get("Path") for tool in config_data.get("Misc").get("Tools")]
for tool_path, type in tool_list:
if not tool_path in tool_loaded:
files_to_remove.append(os.path.join(tools_directory, tool_path))
if "manifest.json" in os.listdir(backend.result_dir):
files_to_remove.append(os.path.join(backend.result_dir, "manifest.json"))
for file_path in files_to_remove:
try:
if os.path.isdir(file_path):
shutil.rmtree(file_path)
else:
os.remove(file_path)
except Exception as e:
backend.u.log_message("[BUILD] Failed to remove file {}: {}".format(os.path.basename(file_path), e), level="WARNING", to_build_log=True)
self.build_progress_signal.emit(title, steps, len(steps) - 1, 100, True)
def show_post_build_instructions(self, bios_requirements):
while self.instructions_after_content_layout.count():
item = self.instructions_after_content_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
if bios_requirements:
bios_header = StrongBodyLabel("1. BIOS/UEFI Settings Required:")
bios_header.setStyleSheet("color: {}; font-size: 14px;".format(COLORS["warning_text"]))
self.instructions_after_content_layout.addWidget(bios_header)
bios_text = "\n".join(["{}".format(req) for req in bios_requirements])
bios_label = BodyLabel(bios_text)
bios_label.setWordWrap(True)
bios_label.setStyleSheet("color: #424242; line-height: 1.6;")
self.instructions_after_content_layout.addWidget(bios_label)
self.instructions_after_content_layout.addSpacing(SPACING["medium"])
usb_header = StrongBodyLabel("{}. USB Port Mapping:".format(2 if bios_requirements else 1))
usb_header.setStyleSheet("color: {}; font-size: 14px;".format(COLORS["warning_text"]))
self.instructions_after_content_layout.addWidget(usb_header)
path_sep = "\\" if platform.system() == "Windows" else "/"
usb_mapping_instructions = (
"1. Use USBToolBox tool to map USB ports<br>"
"2. Add created UTBMap.kext into the EFI{path_sep}OC{path_sep}Kexts folder<br>"
"3. Remove UTBDefault.kext from the EFI{path_sep}OC{path_sep}Kexts folder<br>"
"4. Edit config.plist using ProperTree:<br>"
" a. Run OC Snapshot (Command/Ctrl + R)<br>"
" b. Enable XhciPortLimit quirk if you have more than 15 ports per controller<br>"
" c. Save the file when finished."
).format(path_sep=path_sep)
usb_label = BodyLabel(usb_mapping_instructions)
usb_label.setWordWrap(True)
usb_label.setStyleSheet("color: #424242; line-height: 1.6;")
self.instructions_after_content_layout.addWidget(usb_label)
self.instructions_after_build_card.setVisible(True)
def _handle_build_complete(self, success, bios_requirements):
self.build_in_progress = False
self.build_successful = success
if success:
self.log_card.setVisible(False)
self.progress_helper.update("success", "Build completed successfully!", 100)
self.show_post_build_instructions(bios_requirements)
self._load_configs_after_build()
self.build_btn.setText("Build OpenCore EFI")
self.build_btn.setEnabled(True)
self.open_result_btn.setEnabled(True)
success_message = "Your OpenCore EFI has been built successfully!"
if bios_requirements is not None:
success_message += " Review the important instructions below."
self.controller.update_status(success_message, "success")
else:
self.progress_helper.update("error", "Build OpenCore EFI failed", None)
self.config_editor.setVisible(False)
self.build_btn.setText("Retry Build OpenCore EFI")
self.build_btn.setEnabled(True)
self.open_result_btn.setEnabled(False)
self.controller.update_status("An error occurred during the build. Check the log for details.", "error")
def open_result(self):
result_dir = self.controller.backend.result_dir
try:
self.controller.backend.u.open_folder(result_dir)
except Exception as e:
self.controller.update_status("Failed to open result folder: {}".format(e), "warning")
def _load_configs_after_build(self):
backend = self.controller.backend
source_efi_dir = os.path.join(backend.k.ock_files_dir, "OpenCorePkg")
original_config_file = os.path.join(source_efi_dir, "EFI", "OC", "config.plist")
if not os.path.exists(original_config_file):
return
original_config = backend.u.read_file(original_config_file)
if not original_config:
return
modified_config_file = os.path.join(backend.result_dir, "EFI", "OC", "config.plist")
if not os.path.exists(modified_config_file):
return
modified_config = backend.u.read_file(modified_config_file)
if not modified_config:
return
context = {
"hardware_report": self.controller.hardware_state.hardware_report,
"macos_version": self.controller.macos_state.darwin_version,
"smbios_model": self.controller.smbios_state.model_name,
}
self.config_editor.load_configs(original_config, modified_config, context)
self.config_editor.setVisible(True)
def refresh(self):
if not self.build_in_progress:
if self.build_successful:
self.progress_container.setVisible(True)
self.open_result_btn.setEnabled(True)
else:
log_text = self.build_log.toPlainText()
if not log_text or log_text == DEFAULT_LOG_TEXT:
self.progress_container.setVisible(False)
self.log_card.setVisible(False)
self.open_result_btn.setEnabled(False)

View File

@@ -0,0 +1,557 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
from qfluentwidgets import SubtitleLabel, BodyLabel, ScrollArea, FluentIcon, GroupHeaderCardWidget, CardWidget, StrongBodyLabel
from Scripts.styles import COLORS, SPACING
from Scripts import ui_utils
from Scripts.datasets import os_data, pci_data
class CompatibilityStatusBanner:
def __init__(self, parent=None, ui_utils_instance=None, layout=None):
self.parent = parent
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.layout = layout
self.card = None
self.body_label = None
self.note_label = None
def _create_card(self, card_type, icon, title, message, note=""):
body_text = message
if note:
body_text += "<br><br><i style=\"color: {}; font-size: 12px;\">{}</i>".format(COLORS["text_secondary"], note)
if self.card:
if self.layout:
self.layout.removeWidget(self.card)
self.card.setParent(None)
self.card.deleteLater()
self.card = self.ui_utils.custom_card(
card_type=card_type,
icon=icon,
title=title,
body=body_text,
parent=self.parent
)
self.card.setVisible(True)
if self.layout:
self.layout.insertWidget(2, self.card)
return self.card
def show_error(self, title, message, note=""):
self._create_card("error", FluentIcon.CLOSE, title, message, note)
def show_success(self, title, message, note=""):
self._create_card("success", FluentIcon.ACCEPT, title, message, note)
def setVisible(self, visible):
if self.card:
self.card.setVisible(visible)
class CompatibilityPage(ScrollArea):
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("compatibilityPage")
self.controller = parent
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.contentWidget = None
self.contentLayout = None
self.native_support_label = None
self.ocl_support_label = None
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.enableTransparentBackground()
self._init_ui()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self.ui_utils.create_step_indicator(2))
header_container = QWidget()
header_layout = QHBoxLayout(header_container)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(SPACING["large"])
title_block = QWidget()
title_layout = QVBoxLayout(title_block)
title_layout.setContentsMargins(0, 0, 0, 0)
title_layout.setSpacing(SPACING["tiny"])
title_label = SubtitleLabel("Hardware Compatibility")
title_layout.addWidget(title_label)
subtitle_label = BodyLabel("Review hardware compatibility with macOS")
subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
title_layout.addWidget(subtitle_label)
header_layout.addWidget(title_block, 1)
self.expandLayout.addWidget(header_container)
self.status_banner = CompatibilityStatusBanner(self.scrollWidget, self.ui_utils, self.expandLayout)
self.expandLayout.addSpacing(SPACING["large"])
self.contentWidget = QWidget()
self.contentLayout = QVBoxLayout(self.contentWidget)
self.contentLayout.setContentsMargins(0, 0, 0, 0)
self.contentLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self.contentWidget)
self.placeholder_label = BodyLabel("Load a hardware report to see compatibility information")
self.placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.placeholder_label.setStyleSheet("color: #605E5C; padding: 40px;")
self.placeholder_label.setWordWrap(True)
self.contentLayout.addWidget(self.placeholder_label)
self.contentLayout.addStretch()
def update_status_banner(self):
if not self.controller.hardware_state.hardware_report:
self.status_banner.setVisible(False)
return
if self.controller.hardware_state.compatibility_error:
self._show_error_banner()
return
self._show_support_banner()
def _show_error_banner(self):
codes = self.controller.hardware_state.compatibility_error
if isinstance(codes, str):
codes = [codes]
code_map = {
"ERROR_MISSING_SSE4": (
"Missing required SSE4.x instruction set.",
"Your CPU is not supported by macOS versions newer than Sierra (10.12)."
),
"ERROR_NO_COMPATIBLE_GPU": (
"You cannot install macOS without a supported GPU.",
"Please do NOT spam my inbox or issue tracker about this issue anymore!"
),
"ERROR_INTEL_VMD": (
"Intel VMD controllers are not supported in macOS.",
"Please disable Intel VMD in the BIOS settings and try again with new hardware report."
),
"ERROR_NO_COMPATIBLE_STORAGE": (
"No compatible storage controller for macOS was found!",
"Consider purchasing a compatible SSD NVMe for your system."
)
}
title = "Hardware Compatibility Issue"
messages = []
notes = []
for code in codes:
msg, note = code_map.get(code, (code, ""))
messages.append(msg)
if note:
notes.append(note)
self.status_banner.show_error(
title,
"\n".join(messages),
"\n".join(notes)
)
def _show_support_banner(self):
if self.controller.macos_state.native_version:
min_ver_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.native_version[0])
max_ver_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.native_version[-1])
native_range = min_ver_name if min_ver_name == max_ver_name else "{} to {}".format(min_ver_name, max_ver_name)
message = "Native macOS support: {}".format(native_range)
if self.controller.macos_state.ocl_patched_version:
oclp_max_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.ocl_patched_version[0])
oclp_min_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.ocl_patched_version[-1])
oclp_range = oclp_min_name if oclp_min_name == oclp_max_name else "{} to {}".format(oclp_min_name, oclp_max_name)
message += "\nOpenCore Legacy Patcher extended support: {}".format(oclp_range)
self.status_banner.show_success("Hardware is Compatible", message)
else:
self.status_banner.show_error(
"Incompatible Hardware",
"No supported macOS version found for this hardware configuration."
)
def format_compatibility(self, compat_tuple):
if not compat_tuple or compat_tuple == (None, None):
return "Unsupported", "#D13438"
max_ver, min_ver = compat_tuple
if max_ver and min_ver:
max_name = os_data.get_macos_name_by_darwin(max_ver)
min_name = os_data.get_macos_name_by_darwin(min_ver)
if max_name == min_name:
return "Up to {}".format(max_name), "#0078D4"
else:
return "{} to {}".format(min_name, max_name), "#107C10"
return "Unknown", "#605E5C"
def update_display(self):
if not self.contentLayout:
return
while self.contentLayout.count() > 0:
item = self.contentLayout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
if not self.controller.hardware_state.hardware_report:
self._show_placeholder()
return
report = self.controller.hardware_state.hardware_report
cards_added = 0
cards_added += self._add_cpu_card(report)
cards_added += self._add_gpu_card(report)
cards_added += self._add_sound_card(report)
cards_added += self._add_network_card(report)
cards_added += self._add_storage_card(report)
cards_added += self._add_bluetooth_card(report)
cards_added += self._add_biometric_card(report)
cards_added += self._add_sd_card(report)
if cards_added == 0:
self._show_no_data_label()
self.contentLayout.addStretch()
self.update_status_banner()
self.scrollWidget.updateGeometry()
self.scrollWidget.update()
self.update()
def _show_placeholder(self):
self.placeholder_label = BodyLabel("Load hardware report to see compatibility information")
self.placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.placeholder_label.setStyleSheet("color: #605E5C; padding: 40px;")
self.placeholder_label.setWordWrap(True)
self.contentLayout.addWidget(self.placeholder_label)
self.contentLayout.addStretch()
def _show_no_data_label(self):
no_data_card = self.ui_utils.custom_card(
card_type="error",
icon=FluentIcon.CLOSE,
title="No compatible hardware information found in the report.",
body="Please ensure the hardware report contains valid device data.",
parent=self.scrollWidget
)
self.contentLayout.addWidget(no_data_card)
def _add_compatibility_group(self, card, title, compat):
compat_text, compat_color = self.format_compatibility(compat)
self.ui_utils.add_group_with_indent(
card,
self.ui_utils.get_compatibility_icon(compat),
title,
compat_text,
self.ui_utils.create_info_widget("", compat_color),
indent_level=1
)
def _add_cpu_card(self, report):
if "CPU" not in report: return 0
cpu_info = report["CPU"]
if not isinstance(cpu_info, dict): return 0
cpu_card = GroupHeaderCardWidget(self.scrollWidget)
cpu_card.setTitle("CPU")
name = cpu_info.get("Processor Name", "Unknown")
self.ui_utils.add_group_with_indent(
cpu_card,
self.ui_utils.colored_icon(FluentIcon.TAG, COLORS["primary"]),
"Processor",
name,
indent_level=0
)
self._add_compatibility_group(cpu_card, "macOS Compatibility", cpu_info.get("Compatibility", (None, None)))
details = []
if cpu_info.get("Codename"):
details.append("Codename: {}".format(cpu_info.get("Codename")))
if cpu_info.get("Core Count"):
details.append("Cores: {}".format(cpu_info.get("Core Count")))
if details:
self.ui_utils.add_group_with_indent(
cpu_card,
self.ui_utils.colored_icon(FluentIcon.INFO, COLORS["info"]),
"Details",
"".join(details),
indent_level=1
)
self.contentLayout.addWidget(cpu_card)
return 1
def _add_gpu_card(self, report):
if "GPU" not in report or not report["GPU"]: return 0
gpu_card = GroupHeaderCardWidget(self.scrollWidget)
gpu_card.setTitle("Graphics")
for idx, (gpu_name, gpu_info) in enumerate(report["GPU"].items()):
device_type = gpu_info.get("Device Type", "Unknown")
self.ui_utils.add_group_with_indent(
gpu_card,
self.ui_utils.colored_icon(FluentIcon.PHOTO, COLORS["primary"]),
gpu_name,
"Type: {}".format(device_type),
indent_level=0
)
self._add_compatibility_group(gpu_card, "macOS Compatibility", gpu_info.get("Compatibility", (None, None)))
if "OCLP Compatibility" in gpu_info:
oclp_compat = gpu_info.get("OCLP Compatibility")
oclp_text, oclp_color = self.format_compatibility(oclp_compat)
self.ui_utils.add_group_with_indent(
gpu_card,
self.ui_utils.colored_icon(FluentIcon.IOT, COLORS["primary"]),
"OCLP Compatibility",
oclp_text,
self.ui_utils.create_info_widget("Extended support with OpenCore Legacy Patcher", COLORS["text_secondary"]),
indent_level=1
)
if "Monitor" in report:
self._add_monitor_info(gpu_card, gpu_name, gpu_info, report["Monitor"])
self.contentLayout.addWidget(gpu_card)
return 1
def _add_monitor_info(self, gpu_card, gpu_name, gpu_info, monitors):
connected_monitors = []
for monitor_name, monitor_info in monitors.items():
if monitor_info.get("Connected GPU") == gpu_name:
connector = monitor_info.get("Connector Type", "Unknown")
monitor_str = "{} ({})".format(monitor_name, connector)
manufacturer = gpu_info.get("Manufacturer", "")
raw_device_id = gpu_info.get("Device ID", "")
device_id = raw_device_id[5:] if len(raw_device_id) > 5 else raw_device_id
if "Intel" in manufacturer and device_id.startswith(("01", "04", "0A", "0C", "0D")):
if connector == "VGA":
monitor_str += " (Unsupported)"
connected_monitors.append(monitor_str)
if connected_monitors:
self.ui_utils.add_group_with_indent(
gpu_card,
self.ui_utils.colored_icon(FluentIcon.VIEW, COLORS["info"]),
"Connected Displays",
", ".join(connected_monitors),
indent_level=1
)
def _add_sound_card(self, report):
if "Sound" not in report or not report["Sound"]: return 0
sound_card = GroupHeaderCardWidget(self.scrollWidget)
sound_card.setTitle("Audio")
for audio_device, audio_props in report["Sound"].items():
self.ui_utils.add_group_with_indent(
sound_card,
self.ui_utils.colored_icon(FluentIcon.MUSIC, COLORS["primary"]),
audio_device,
"",
indent_level=0
)
self._add_compatibility_group(sound_card, "macOS Compatibility", audio_props.get("Compatibility", (None, None)))
endpoints = audio_props.get("Audio Endpoints", [])
if endpoints:
self.ui_utils.add_group_with_indent(
sound_card,
self.ui_utils.colored_icon(FluentIcon.HEADPHONE, COLORS["info"]),
"Audio Endpoints",
", ".join(endpoints),
indent_level=1
)
self.contentLayout.addWidget(sound_card)
return 1
def _add_network_card(self, report):
if "Network" not in report or not report["Network"]: return 0
network_card = GroupHeaderCardWidget(self.scrollWidget)
network_card.setTitle("Network")
for device_name, device_props in report["Network"].items():
self.ui_utils.add_group_with_indent(
network_card,
self.ui_utils.colored_icon(FluentIcon.WIFI, COLORS["primary"]),
device_name,
"",
indent_level=0
)
self._add_compatibility_group(network_card, "macOS Compatibility", device_props.get("Compatibility", (None, None)))
if "OCLP Compatibility" in device_props:
oclp_compat = device_props.get("OCLP Compatibility")
oclp_text, oclp_color = self.format_compatibility(oclp_compat)
self.ui_utils.add_group_with_indent(
network_card,
self.ui_utils.colored_icon(FluentIcon.IOT, COLORS["primary"]),
"OCLP Compatibility",
oclp_text,
self.ui_utils.create_info_widget("Extended support with OpenCore Legacy Patcher", COLORS["text_secondary"]),
indent_level=1
)
self._add_continuity_info(network_card, device_props)
self.contentLayout.addWidget(network_card)
return 1
def _add_continuity_info(self, network_card, device_props):
device_id = device_props.get("Device ID", "")
if not device_id: return
continuity_info = ""
continuity_color = COLORS["text_secondary"]
if device_id in pci_data.BroadcomWiFiIDs:
continuity_info = "Full support (AirDrop, Handoff, Universal Clipboard, Instant Hotspot, etc.)"
continuity_color = COLORS["success"]
elif device_id in pci_data.IntelWiFiIDs:
continuity_info = "Partial (Handoff and Universal Clipboard with AirportItlwm) - AirDrop, Universal Clipboard, Instant Hotspot,... not available"
continuity_color = COLORS["warning"]
elif device_id in pci_data.AtherosWiFiIDs:
continuity_info = "Limited support (No Continuity features available). Atheros cards are not recommended for macOS."
continuity_color = COLORS["error"]
if continuity_info:
self.ui_utils.add_group_with_indent(
network_card,
self.ui_utils.colored_icon(FluentIcon.SYNC, continuity_color),
"Continuity Features",
continuity_info,
self.ui_utils.create_info_widget("", continuity_color),
indent_level=1
)
def _add_storage_card(self, report):
if "Storage Controllers" not in report or not report["Storage Controllers"]: return 0
storage_card = GroupHeaderCardWidget(self.scrollWidget)
storage_card.setTitle("Storage")
for controller_name, controller_props in report["Storage Controllers"].items():
self.ui_utils.add_group_with_indent(
storage_card,
self.ui_utils.colored_icon(FluentIcon.FOLDER, COLORS["primary"]),
controller_name,
"",
indent_level=0
)
self._add_compatibility_group(storage_card, "macOS Compatibility", controller_props.get("Compatibility", (None, None)))
disk_drives = controller_props.get("Disk Drives", [])
if disk_drives:
self.ui_utils.add_group_with_indent(
storage_card,
self.ui_utils.colored_icon(FluentIcon.FOLDER, COLORS["info"]),
"Disk Drives",
", ".join(disk_drives),
indent_level=1
)
self.contentLayout.addWidget(storage_card)
return 1
def _add_bluetooth_card(self, report):
if "Bluetooth" not in report or not report["Bluetooth"]: return 0
bluetooth_card = GroupHeaderCardWidget(self.scrollWidget)
bluetooth_card.setTitle("Bluetooth")
for bluetooth_name, bluetooth_props in report["Bluetooth"].items():
self.ui_utils.add_group_with_indent(
bluetooth_card,
self.ui_utils.colored_icon(FluentIcon.BLUETOOTH, COLORS["primary"]),
bluetooth_name,
"",
indent_level=0
)
self._add_compatibility_group(bluetooth_card, "macOS Compatibility", bluetooth_props.get("Compatibility", (None, None)))
self.contentLayout.addWidget(bluetooth_card)
return 1
def _add_biometric_card(self, report):
if "Biometric" not in report or not report["Biometric"]: return 0
bio_card = GroupHeaderCardWidget(self.scrollWidget)
bio_card.setTitle("Biometric")
self.ui_utils.add_group_with_indent(
bio_card,
self.ui_utils.colored_icon(FluentIcon.CLOSE, COLORS["warning"]),
"Hardware Limitation",
"Biometric authentication in macOS requires Apple T2 Chip, which is not available for Hackintosh systems.",
self.ui_utils.create_info_widget("", COLORS["warning"]),
indent_level=0
)
for bio_device, bio_props in report["Biometric"].items():
self.ui_utils.add_group_with_indent(
bio_card,
self.ui_utils.colored_icon(FluentIcon.FINGERPRINT, COLORS["error"]),
bio_device,
"Unsupported",
indent_level=0
)
self.contentLayout.addWidget(bio_card)
return 1
def _add_sd_card(self, report):
if "SD Controller" not in report or not report["SD Controller"]: return 0
sd_card = GroupHeaderCardWidget(self.scrollWidget)
sd_card.setTitle("SD Controller")
for controller_name, controller_props in report["SD Controller"].items():
self.ui_utils.add_group_with_indent(
sd_card,
self.ui_utils.colored_icon(FluentIcon.SAVE, COLORS["primary"]),
controller_name,
"",
indent_level=0
)
self._add_compatibility_group(sd_card, "macOS Compatibility", controller_props.get("Compatibility", (None, None)))
self.contentLayout.addWidget(sd_card)
return 1
def refresh(self):
self.update_display()

View File

@@ -0,0 +1,293 @@
import os
from PyQt6.QtWidgets import QWidget, QVBoxLayout
from PyQt6.QtCore import Qt
from qfluentwidgets import (
ScrollArea, SubtitleLabel, BodyLabel, FluentIcon,
PushSettingCard, ExpandGroupSettingCard,
SettingCard, PushButton
)
from Scripts.custom_dialogs import show_macos_version_dialog
from Scripts.styles import SPACING, COLORS
from Scripts import ui_utils
class macOSCard(SettingCard):
def __init__(self, controller, on_select_version, parent=None):
super().__init__(
FluentIcon.GLOBE,
"macOS Version",
"Target operating system version",
parent
)
self.controller = controller
self.versionLabel = BodyLabel(self.controller.macos_state.selected_version_name)
self.versionLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"]))
self.selectVersionBtn = PushButton("Select Version")
self.selectVersionBtn.clicked.connect(on_select_version)
self.selectVersionBtn.setFixedWidth(150)
self.hBoxLayout.addWidget(self.versionLabel)
self.hBoxLayout.addWidget(self.selectVersionBtn)
self.hBoxLayout.addSpacing(16)
def update_version(self):
self.versionLabel.setText(self.controller.macos_state.selected_version_name)
class AudioLayoutCard(SettingCard):
def __init__(self, controller, on_select_layout, parent=None):
super().__init__(
FluentIcon.MUSIC,
"Audio Layout ID",
"Select layout ID for your audio codec",
parent
)
self.controller = controller
layout_text = str(self.controller.hardware_state.audio_layout_id) if self.controller.hardware_state.audio_layout_id is not None else "Not configured"
self.layoutLabel = BodyLabel(layout_text)
self.layoutLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"]))
self.selectLayoutBtn = PushButton("Configure Layout")
self.selectLayoutBtn.clicked.connect(on_select_layout)
self.selectLayoutBtn.setFixedWidth(150)
self.hBoxLayout.addWidget(self.layoutLabel)
self.hBoxLayout.addWidget(self.selectLayoutBtn)
self.hBoxLayout.addSpacing(16)
self.setVisible(False)
def update_layout(self):
layout_text = str(self.controller.hardware_state.audio_layout_id) if self.controller.hardware_state.audio_layout_id is not None else "Not configured"
self.layoutLabel.setText(layout_text)
class SMBIOSModelCard(SettingCard):
def __init__(self, controller, on_select_model, parent=None):
super().__init__(
FluentIcon.TAG,
"SMBIOS Model",
"Select Mac model identifier for your system",
parent
)
self.controller = controller
model_text = self.controller.smbios_state.model_name if self.controller.smbios_state.model_name != "Not selected" else "Not configured"
self.modelLabel = BodyLabel(model_text)
self.modelLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"]))
self.selectModelBtn = PushButton("Configure Model")
self.selectModelBtn.clicked.connect(on_select_model)
self.selectModelBtn.setFixedWidth(150)
self.hBoxLayout.addWidget(self.modelLabel)
self.hBoxLayout.addWidget(self.selectModelBtn)
self.hBoxLayout.addSpacing(16)
def update_model(self):
model_text = self.controller.smbios_state.model_name if self.controller.smbios_state.model_name != "Not selected" else "Not configured"
self.modelLabel.setText(model_text)
class ConfigurationPage(ScrollArea):
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("configurationPage")
self.controller = parent
self.settings = self.controller.backend.settings
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.enableTransparentBackground()
self.status_card = None
self._init_ui()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self.ui_utils.create_step_indicator(3))
header_container = QWidget()
header_layout = QVBoxLayout(header_container)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(SPACING["tiny"])
title_label = SubtitleLabel("Configuration")
header_layout.addWidget(title_label)
subtitle_label = BodyLabel("Configure your OpenCore EFI settings")
subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
header_layout.addWidget(subtitle_label)
self.expandLayout.addWidget(header_container)
self.expandLayout.addSpacing(SPACING["large"])
self.status_start_index = self.expandLayout.count()
self._update_status_card()
self.macos_card = macOSCard(self.controller, self.select_macos_version, self.scrollWidget)
self.expandLayout.addWidget(self.macos_card)
self.acpi_card = PushSettingCard(
"Configure Patches",
FluentIcon.DEVELOPER_TOOLS,
"ACPI Patches",
"Customize system ACPI table modifications for hardware compatibility",
self.scrollWidget
)
self.acpi_card.clicked.connect(self.customize_acpi_patches)
self.expandLayout.addWidget(self.acpi_card)
self.kexts_card = PushSettingCard(
"Manage Kexts",
FluentIcon.CODE,
"Kernel Extensions",
"Configure kexts required for your hardware",
self.scrollWidget
)
self.kexts_card.clicked.connect(self.customize_kexts)
self.expandLayout.addWidget(self.kexts_card)
self.audio_layout_card = None
self.audio_layout_card_index = None
self.audio_layout_card = AudioLayoutCard(self.controller, self.customize_audio_layout, self.scrollWidget)
self.expandLayout.addWidget(self.audio_layout_card)
self.smbios_card = SMBIOSModelCard(self.controller, self.customize_smbios_model, self.scrollWidget)
self.expandLayout.addWidget(self.smbios_card)
self.expandLayout.addStretch()
def _update_status_card(self):
if self.status_card is not None:
self.expandLayout.removeWidget(self.status_card)
self.status_card.deleteLater()
self.status_card = None
disabled_devices = self.controller.hardware_state.disabled_devices or {}
status_text = ""
status_color = COLORS["text_secondary"]
bg_color = COLORS["bg_card"]
icon = FluentIcon.INFO
if disabled_devices:
status_text = "Hardware components excluded from configuration"
status_color = COLORS["text_secondary"]
bg_color = COLORS["warning_bg"]
elif not self.controller.hardware_state.hardware_report:
status_text = "Please select hardware report first"
elif not self.controller.macos_state.darwin_version:
status_text = "Please select target macOS version first"
else:
status_text = "All hardware components are compatible and enabled"
status_color = COLORS["success"]
bg_color = COLORS["success_bg"]
icon = FluentIcon.ACCEPT
self.status_card = ExpandGroupSettingCard(
icon,
"Compatibility Status",
status_text,
self.scrollWidget
)
if disabled_devices:
for device_name, device_info in disabled_devices.items():
self.ui_utils.add_group_with_indent(
self.status_card,
FluentIcon.CLOSE,
device_name,
"Incompatible" if device_info.get("Compatibility") == (None, None) else "Disabled",
)
else:
pass
self.expandLayout.insertWidget(self.status_start_index, self.status_card)
def select_macos_version(self):
if not self.controller.validate_prerequisites(require_darwin_version=False, require_customized_hardware=False):
return
selected_version = show_macos_version_dialog(
self.controller.macos_state.native_version,
self.controller.macos_state.ocl_patched_version,
self.controller.macos_state.suggested_version
)
if selected_version:
self.controller.apply_macos_version(selected_version)
self.controller.update_status("macOS version updated to {}".format(self.controller.macos_state.selected_version_name), "success")
if hasattr(self, "macos_card"):
self.macos_card.update_version()
def customize_acpi_patches(self):
if not self.controller.validate_prerequisites():
return
self.controller.backend.ac.customize_patch_selection()
self.controller.update_status("ACPI patches configuration updated successfully", "success")
def customize_kexts(self):
if not self.controller.validate_prerequisites():
return
self.controller.backend.k.kext_configuration_menu(self.controller.macos_state.darwin_version)
self.controller.update_status("Kext configuration updated successfully", "success")
def customize_audio_layout(self):
if not self.controller.validate_prerequisites():
return
audio_layout_id, audio_controller_properties = self.controller.backend.k._select_audio_codec_layout(
self.controller.hardware_state.hardware_report,
default_layout_id=self.controller.hardware_state.audio_layout_id
)
if audio_layout_id is not None:
self.controller.hardware_state.audio_layout_id = audio_layout_id
self.controller.hardware_state.audio_controller_properties = audio_controller_properties
self._update_audio_layout_card_visibility()
self.controller.update_status("Audio layout updated to {}".format(audio_layout_id), "success")
def customize_smbios_model(self):
if not self.controller.validate_prerequisites():
return
current_model = self.controller.smbios_state.model_name
selected_model = self.controller.backend.s.customize_smbios_model(self.controller.hardware_state.customized_hardware, current_model, self.controller.macos_state.darwin_version, self.controller.window())
if selected_model and selected_model != current_model:
self.controller.smbios_state.model_name = selected_model
self.controller.backend.s.smbios_specific_options(self.controller.hardware_state.customized_hardware, selected_model, self.controller.macos_state.darwin_version, self.controller.backend.ac.patches, self.controller.backend.k)
if hasattr(self, "smbios_card"):
self.smbios_card.update_model()
self.controller.update_status("SMBIOS model updated to {}".format(selected_model), "success")
def _update_audio_layout_card_visibility(self):
if self.controller.hardware_state.audio_layout_id is not None:
self.audio_layout_card.setVisible(True)
self.audio_layout_card.update_layout()
else:
self.audio_layout_card.setVisible(False)
def update_display(self):
self._update_status_card()
if hasattr(self, "macos_card"):
self.macos_card.update_version()
self._update_audio_layout_card_visibility()
if hasattr(self, "smbios_card"):
self.smbios_card.update_model()
def refresh(self):
self.update_display()

168
Scripts/pages/home_page.py Normal file
View File

@@ -0,0 +1,168 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFrame
from PyQt6.QtCore import Qt
from qfluentwidgets import SubtitleLabel, BodyLabel, CardWidget, StrongBodyLabel, FluentIcon, ScrollArea
from Scripts.styles import COLORS, SPACING
from Scripts import ui_utils
class HomePage(ScrollArea):
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("homePage")
self.controller = parent
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.enableTransparentBackground()
self.scrollWidget.setStyleSheet("QWidget { background: transparent; }")
self._init_ui()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
self.expandLayout.addWidget(self._create_title_label())
self.expandLayout.addWidget(self._create_hero_section())
self.expandLayout.addWidget(self._create_note_card())
self.expandLayout.addWidget(self._create_warning_card())
self.expandLayout.addWidget(self._create_guide_card())
self.expandLayout.addStretch()
def _create_title_label(self):
title_label = SubtitleLabel("Welcome to OpCore Simplify")
title_label.setStyleSheet("font-size: 24px; font-weight: bold;")
return title_label
def _create_hero_section(self):
hero_card = CardWidget()
hero_layout = QHBoxLayout(hero_card)
hero_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
hero_layout.setSpacing(SPACING["large"])
hero_text = QVBoxLayout()
hero_text.setSpacing(SPACING["medium"])
hero_title = StrongBodyLabel("Introduction")
hero_title.setStyleSheet("font-size: 18px; color: {};".format(COLORS["primary"]))
hero_text.addWidget(hero_title)
hero_body = BodyLabel(
"A specialized tool that streamlines OpenCore EFI creation by automating the essential setup process and providing standardized configurations.<br>"
"Designed to reduce manual effort while ensuring accuracy in your Hackintosh journey."
)
hero_body.setWordWrap(True)
hero_body.setStyleSheet("line-height: 1.6; font-size: 14px;")
hero_text.addWidget(hero_body)
hero_layout.addLayout(hero_text, 2)
robot_icon = self.ui_utils.build_icon_label(FluentIcon.ROBOT, COLORS["primary"], size=64)
hero_layout.addWidget(robot_icon, 1, Qt.AlignmentFlag.AlignVCenter)
return hero_card
def _create_note_card(self):
return self.ui_utils.custom_card(
card_type="note",
title="OpenCore Legacy Patcher 3.0.0 - Now Supports macOS Tahoe 26!",
body=(
"The long awaited version 3.0.0 of OpenCore Legacy Patcher is here, bringing <b>initial support for macOS Tahoe 26</b> to the community!<br><br>"
"<b>Please Note:</b><br>"
"- Only OpenCore-Patcher 3.0.0 from the <a href=\"https://github.com/lzhoang2801/OpenCore-Legacy-Patcher/releases/tag/3.0.0\" style=\"color: #0078D4; text-decoration: none;\">lzhoang2801/OpenCore-Legacy-Patcher</a> repository provides support for macOS Tahoe 26 with early patches.<br>"
"- Official Dortania releases or older patches <b>will NOT work</b> with macOS Tahoe 26."
)
)
def _create_warning_card(self):
return self.ui_utils.custom_card(
card_type="warning",
title="WARNING",
body=(
"While OpCore Simplify significantly reduces setup time, the Hackintosh journey still requires:<br><br>"
"- Understanding basic concepts from the <a href=\"https://dortania.github.io/OpenCore-Install-Guide/\" style=\"color: #F57C00; text-decoration: none;\">Dortania Guide</a><br>"
"- Testing and troubleshooting during the installation process.<br>"
"- Patience and persistence in resolving any issues that arise.<br><br>"
"Our tool does not guarantee a successful installation in the first attempt, but it should help you get started."
)
)
def _create_guide_card(self):
guide_card = CardWidget()
guide_layout = QVBoxLayout(guide_card)
guide_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
guide_layout.setSpacing(SPACING["medium"])
guide_title = StrongBodyLabel("Getting Started")
guide_title.setStyleSheet("font-size: 18px;")
guide_layout.addWidget(guide_title)
step_items = [
(FluentIcon.FOLDER_ADD, "1. Select Hardware Report", "Select hardware report of target system you want to build EFI for."),
(FluentIcon.CHECKBOX, "2. Check Compatibility", "Review hardware compatibility with macOS."),
(FluentIcon.EDIT, "3. Configure Settings", "Customize ACPI patches, kexts, and config for your OpenCore EFI."),
(FluentIcon.DEVELOPER_TOOLS, "4. Build EFI", "Generate your OpenCore EFI."),
]
for idx, (icon, title, desc) in enumerate(step_items):
guide_layout.addWidget(self._create_guide_row(icon, title, desc))
if idx < len(step_items) - 1:
guide_layout.addWidget(self._create_divider())
return guide_card
def _create_guide_row(self, icon, title, desc):
row = QWidget()
row_layout = QHBoxLayout(row)
row_layout.setContentsMargins(0, 0, 0, 0)
row_layout.setSpacing(SPACING["medium"])
icon_container = QWidget()
icon_container.setFixedWidth(40)
icon_layout = QVBoxLayout(icon_container)
icon_layout.setContentsMargins(0, 0, 0, 0)
icon_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter)
row_icon = self.ui_utils.build_icon_label(icon, COLORS["primary"], size=24)
icon_layout.addWidget(row_icon)
row_layout.addWidget(icon_container)
text_col = QVBoxLayout()
text_col.setSpacing(SPACING["tiny"])
title_label = StrongBodyLabel(title)
title_label.setStyleSheet("font-size: 14px;")
desc_label = BodyLabel(desc)
desc_label.setWordWrap(True)
desc_label.setStyleSheet("color: {}; line-height: 1.4;".format(COLORS["text_secondary"]))
text_col.addWidget(title_label)
text_col.addWidget(desc_label)
row_layout.addLayout(text_col)
return row
def _create_divider(self):
divider = QFrame()
divider.setFrameShape(QFrame.Shape.HLine)
divider.setStyleSheet("color: {};".format(COLORS["border_light"]))
return divider
def refresh(self):
pass

View File

@@ -0,0 +1,485 @@
import os
import threading
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFileDialog, QLabel
from qfluentwidgets import (
PushButton, SubtitleLabel, BodyLabel, CardWidget, FluentIcon,
StrongBodyLabel, PrimaryPushButton, ProgressBar,
IconWidget, ExpandGroupSettingCard
)
from Scripts.datasets import os_data
from Scripts.custom_dialogs import show_info, show_confirmation
from Scripts.state import HardwareReportState, macOSVersionState, SMBIOSState
from Scripts.styles import SPACING, COLORS
from Scripts import ui_utils
class ReportDetailsGroup(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.INFO,
"Hardware Report Details",
"View selected report paths and validation status",
parent
)
self.reportIcon = IconWidget(FluentIcon.INFO)
self.reportIcon.setFixedSize(16, 16)
self.reportIcon.setVisible(False)
self.acpiIcon = IconWidget(FluentIcon.INFO)
self.acpiIcon.setFixedSize(16, 16)
self.acpiIcon.setVisible(False)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.reportCard = self.addGroup(
FluentIcon.DOCUMENT,
"Report Path",
"Not selected",
self.reportIcon
)
self.acpiCard = self.addGroup(
FluentIcon.FOLDER,
"ACPI Directory",
"Not selected",
self.acpiIcon
)
self.reportCard.contentLabel.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
self.acpiCard.contentLabel.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
def update_status(self, section, path, status_type, message):
card = self.reportCard if section == "report" else self.acpiCard
icon_widget = self.reportIcon if section == "report" else self.acpiIcon
if path and path != "Not selected":
path = os.path.normpath(path)
card.setContent(path)
card.setToolTip(message if message else path)
icon = FluentIcon.INFO
color = COLORS["text_secondary"]
if status_type == "success":
color = COLORS["text_primary"]
icon = FluentIcon.ACCEPT
elif status_type == "error":
color = COLORS["error"]
icon = FluentIcon.CANCEL
elif status_type == "warning":
color = COLORS["warning"]
icon = FluentIcon.INFO
card.contentLabel.setStyleSheet("color: {};".format(color))
icon_widget.setIcon(icon)
icon_widget.setVisible(True)
class SelectHardwareReportPage(QWidget):
export_finished_signal = pyqtSignal(bool, str, str, str)
load_report_progress_signal = pyqtSignal(str, str, int)
load_report_finished_signal = pyqtSignal(bool, str, str, str)
report_validated_signal = pyqtSignal(str, str)
compatibility_checked_signal = pyqtSignal()
def __init__(self, parent, ui_utils_instance=None):
super().__init__(parent)
self.setObjectName("SelectHardwareReport")
self.controller = parent
self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils()
self._connect_signals()
self._init_ui()
def _connect_signals(self):
self.export_finished_signal.connect(self._handle_export_finished)
self.load_report_progress_signal.connect(self._handle_load_report_progress)
self.load_report_finished_signal.connect(self._handle_load_report_finished)
self.report_validated_signal.connect(self._handle_report_validated)
self.compatibility_checked_signal.connect(self._handle_compatibility_checked)
def _init_ui(self):
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.main_layout.setSpacing(SPACING["large"])
self.main_layout.addWidget(self.ui_utils.create_step_indicator(1))
header_layout = QVBoxLayout()
header_layout.setSpacing(SPACING["small"])
title = SubtitleLabel("Select Hardware Report")
subtitle = BodyLabel("Select hardware report of target system you want to build EFI for")
subtitle.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
header_layout.addWidget(title)
header_layout.addWidget(subtitle)
self.main_layout.addLayout(header_layout)
self.main_layout.addSpacing(SPACING["medium"])
self.create_instructions_card()
self.create_action_card()
self.create_report_details_group()
self.main_layout.addStretch()
def create_instructions_card(self):
card = self.ui_utils.custom_card(
card_type="note",
title="Quick Guide",
body=(
"<b>Windows Users:</b> Click <span style=\"color:#0078D4; font-weight:600;\">Export Hardware Report</span> button to generate hardware report for current system. Alternatively, you can manually generate hardware report using Hardware Sniffer tool.<br>"
"<b>Linux/macOS Users:</b> Please transfer a report generated on Windows. Native generation is not supported."
)
)
self.main_layout.addWidget(card)
def create_action_card(self):
self.action_card = CardWidget()
layout = QVBoxLayout(self.action_card)
layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
layout.setSpacing(SPACING["medium"])
title = StrongBodyLabel("Select Methods")
layout.addWidget(title)
btn_layout = QHBoxLayout()
btn_layout.setSpacing(SPACING["medium"])
self.select_btn = PrimaryPushButton(FluentIcon.FOLDER_ADD, "Select Hardware Report")
self.select_btn.clicked.connect(self.select_hardware_report)
btn_layout.addWidget(self.select_btn)
if os.name == "nt":
self.export_btn = PushButton(FluentIcon.DOWNLOAD, "Export Hardware Report")
self.export_btn.clicked.connect(self.export_hardware_report)
btn_layout.addWidget(self.export_btn)
layout.addLayout(btn_layout)
self.progress_container = QWidget()
progress_layout = QVBoxLayout(self.progress_container)
progress_layout.setContentsMargins(0, SPACING["small"], 0, 0)
progress_layout.setSpacing(SPACING["medium"])
status_row = QHBoxLayout()
status_row.setSpacing(SPACING["medium"])
self.status_icon_label = QLabel()
self.status_icon_label.setFixedSize(28, 28)
status_row.addWidget(self.status_icon_label)
self.progress_label = StrongBodyLabel("Ready")
self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["text_secondary"]))
status_row.addWidget(self.progress_label)
status_row.addStretch()
progress_layout.addLayout(status_row)
self.progress_bar = ProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setFixedHeight(10)
self.progress_bar.setTextVisible(True)
progress_layout.addWidget(self.progress_bar)
self.progress_container.setVisible(False)
layout.addWidget(self.progress_container)
self.progress_helper = ui_utils.ProgressStatusHelper(
self.status_icon_label,
self.progress_label,
self.progress_bar,
self.progress_container
)
self.main_layout.addWidget(self.action_card)
def create_report_details_group(self):
self.report_group = ReportDetailsGroup(self)
self.main_layout.addWidget(self.report_group)
def select_report_file(self):
report_path, _ = QFileDialog.getOpenFileName(
self, "Select Hardware Report", "", "JSON Files (*.json)"
)
return report_path if report_path else None
def select_acpi_folder(self):
acpi_dir = QFileDialog.getExistingDirectory(self, "Select ACPI Folder", "")
return acpi_dir if acpi_dir else None
def select_hardware_report(self):
report_path = self.select_report_file()
if not report_path:
return
report_dir = os.path.dirname(report_path)
potential_acpi = os.path.join(report_dir, "ACPI")
acpi_dir = None
if os.path.isdir(potential_acpi):
if show_confirmation("ACPI Folder Detected", "Found an ACPI folder at: {}\n\nDo you want to use this ACPI folder?".format(potential_acpi)):
acpi_dir = potential_acpi
if not acpi_dir:
acpi_dir = self.select_acpi_folder()
if not acpi_dir:
return
self.load_hardware_report(report_path, acpi_dir)
def set_detail_status(self, section, path, status_type, message):
self.report_group.update_status(section, path, status_type, message)
def suggest_macos_version(self):
if not self.controller.hardware_state.hardware_report or not self.controller.macos_state.native_version:
return None
hardware_report = self.controller.hardware_state.hardware_report
native_macos_version = self.controller.macos_state.native_version
suggested_macos_version = native_macos_version[1]
for device_type in ("GPU", "Network", "Bluetooth", "SD Controller"):
if device_type in hardware_report:
for device_name, device_props in hardware_report[device_type].items():
if device_props.get("Compatibility", (None, None)) != (None, None):
if device_type == "GPU" and device_props.get("Device Type") == "Integrated GPU":
device_id = device_props.get("Device ID", " " * 8)[5:]
if device_props.get("Manufacturer") == "AMD" or device_id.startswith(("59", "87C0")):
suggested_macos_version = "22.99.99"
elif device_id.startswith(("09", "19")):
suggested_macos_version = "21.99.99"
if self.controller.backend.u.parse_darwin_version(suggested_macos_version) > self.controller.backend.u.parse_darwin_version(device_props.get("Compatibility")[0]):
suggested_macos_version = device_props.get("Compatibility")[0]
while True:
if "Beta" in os_data.get_macos_name_by_darwin(suggested_macos_version):
suggested_macos_version = "{}{}".format(
int(suggested_macos_version[:2]) - 1, suggested_macos_version[2:])
else:
break
self.controller.macos_state.suggested_version = suggested_macos_version
def load_hardware_report(self, report_path, acpi_dir, from_export=False):
self.controller.hardware_state = HardwareReportState(report_path=report_path, acpi_dir=acpi_dir)
self.controller.macos_state = macOSVersionState()
self.controller.smbios_state = SMBIOSState()
self.controller.backend.ac.acpi.acpi_tables = {}
self.controller.backend.ac.acpi.dsdt = None
self.controller.compatibilityPage.update_display()
self.controller.configurationPage.update_display()
if not from_export:
self.progress_container.setVisible(True)
self.select_btn.setEnabled(False)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(False)
progress_offset = 40 if from_export else 0
self.progress_helper.update("loading", "Validating report...", progress_offset)
self.report_group.setExpand(True)
def load_thread():
try:
progress_scale = 0.5 if from_export else 1.0
def get_progress(base_progress):
return progress_offset + int(base_progress * progress_scale)
self.load_report_progress_signal.emit("loading", "Validating report...", get_progress(10))
is_valid, errors, warnings, validated_data = self.controller.backend.v.validate_report(report_path)
if not is_valid or errors:
error_msg = "Report Errors:\n" + "\n".join(errors)
self.load_report_finished_signal.emit(False, "validation_error", report_path, acpi_dir)
return
self.load_report_progress_signal.emit("loading", "Validating report...", get_progress(30))
self.report_validated_signal.emit(report_path, "Hardware report validated successfully.")
self.load_report_progress_signal.emit("loading", "Checking compatibility...", get_progress(35))
self.controller.hardware_state.hardware_report = validated_data
self.controller.hardware_state.hardware_report, self.controller.macos_state.native_version, self.controller.macos_state.ocl_patched_version, self.controller.hardware_state.compatibility_error = self.controller.backend.c.check_compatibility(validated_data)
self.load_report_progress_signal.emit("loading", "Checking compatibility...", get_progress(55))
self.compatibility_checked_signal.emit()
if self.controller.hardware_state.compatibility_error:
error_msg = self.controller.hardware_state.compatibility_error
if isinstance(error_msg, list):
error_msg = "\n".join(error_msg)
self.load_report_finished_signal.emit(False, "compatibility_error", report_path, acpi_dir)
return
self.load_report_progress_signal.emit("loading", "Loading ACPI tables...", get_progress(60))
self.controller.backend.ac.read_acpi_tables(acpi_dir)
self.load_report_progress_signal.emit("loading", "Loading ACPI tables...", get_progress(90))
if not self.controller.backend.ac._ensure_dsdt():
self.load_report_finished_signal.emit(False, "acpi_error", report_path, acpi_dir)
return
self.load_report_finished_signal.emit(True, "success", report_path, acpi_dir)
except Exception as e:
self.load_report_finished_signal.emit(False, "Exception: {}".format(e), report_path, acpi_dir)
thread = threading.Thread(target=load_thread, daemon=True)
thread.start()
def _handle_load_report_progress(self, status, message, progress):
self.progress_helper.update(status, message, progress)
def _handle_report_validated(self, report_path, message):
self.set_detail_status("report", report_path, "success", message)
def _handle_compatibility_checked(self):
self.controller.compatibilityPage.update_display()
def _handle_load_report_finished(self, success, error_type, report_path, acpi_dir):
self.select_btn.setEnabled(True)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(True)
if success:
count = len(self.controller.backend.ac.acpi.acpi_tables)
self.set_detail_status("acpi", acpi_dir, "success", "ACPI Tables loaded: {} tables found.".format(count))
self.progress_helper.update("success", "Hardware report loaded successfully", 100)
self.controller.update_status("Hardware report loaded successfully", "success")
self.suggest_macos_version()
self.controller.configurationPage.update_display()
else:
if error_type == "validation_error":
is_valid, errors, warnings, validated_data = self.controller.backend.v.validate_report(report_path)
msg = "Report Errors:\n" + "\n".join(errors)
self.set_detail_status("report", report_path, "error", msg)
self.progress_helper.update("error", "Report validation failed", None)
show_info("Report Validation Failed", "The hardware report has errors:\n{}\n\nPlease select a valid report file.".format("\n".join(errors)))
elif error_type == "compatibility_error":
error_msg = self.controller.hardware_state.compatibility_error
if isinstance(error_msg, list):
error_msg = "\n".join(error_msg)
compat_text = "\nCompatibility Error:\n{}".format(error_msg)
self.set_detail_status("report", report_path, "error", compat_text)
show_info("Incompatible Hardware", "Your hardware is not compatible with macOS:\n\n" + error_msg)
elif error_type == "acpi_error":
self.set_detail_status("acpi", acpi_dir, "error", "No ACPI tables found in selected folder.")
self.progress_helper.update("error", "No ACPI tables found", None)
show_info("No ACPI tables", "No ACPI tables found in ACPI folder.")
else:
self.progress_helper.update("error", "Error: {}".format(error_type), None)
self.controller.update_status("Failed to load hardware report: {}".format(error_type), "error")
def export_hardware_report(self):
self.progress_container.setVisible(True)
self.select_btn.setEnabled(False)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(False)
self.progress_helper.update("loading", "Gathering Hardware Sniffer...", 10)
current_dir = os.path.dirname(os.path.realpath(__file__))
main_dir = os.path.dirname(os.path.dirname(current_dir))
report_dir = os.path.join(main_dir, "SysReport")
def export_thread():
try:
hardware_sniffer = self.controller.backend.o.gather_hardware_sniffer()
if not hardware_sniffer:
self.export_finished_signal.emit(False, "Hardware Sniffer not found", "", "")
return
self.export_finished_signal.emit(True, "gathering_complete", hardware_sniffer, report_dir)
except Exception as e:
self.export_finished_signal.emit(False, "Exception gathering sniffer: {}".format(e), "", "")
thread = threading.Thread(target=export_thread, daemon=True)
thread.start()
def _handle_export_finished(self, success, message, hardware_sniffer_or_error, report_dir):
if not success:
self.progress_container.setVisible(False)
self.select_btn.setEnabled(True)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(True)
self.progress_helper.update("error", "Export failed", 0)
self.controller.update_status(hardware_sniffer_or_error, "error")
return
if message == "gathering_complete":
self.progress_helper.update("loading", "Exporting hardware report...", 50)
def run_export_thread():
try:
output = self.controller.backend.r.run({
"args": [hardware_sniffer_or_error, "-e", "-o", report_dir]
})
success = output[-1] == 0
error_message = ""
report_path = ""
acpi_dir = ""
if success:
report_path = os.path.join(report_dir, "Report.json")
acpi_dir = os.path.join(report_dir, "ACPI")
error_message = "Export successful"
else:
error_code = output[-1]
if error_code == 3: error_message = "Error collecting hardware."
elif error_code == 4: error_message = "Error generating hardware report."
elif error_code == 5: error_message = "Error dumping ACPI tables."
else: error_message = "Unknown error."
paths = "{}|||{}".format(report_path, acpi_dir) if report_path and acpi_dir else ""
self.export_finished_signal.emit(success, "export_complete", error_message, paths)
except Exception as e:
self.export_finished_signal.emit(False, "export_complete", "Exception: {}".format(e), "")
thread = threading.Thread(target=run_export_thread, daemon=True)
thread.start()
return
if message == "export_complete":
self.progress_container.setVisible(False)
self.select_btn.setEnabled(True)
if hasattr(self, "export_btn"):
self.export_btn.setEnabled(True)
self.controller.backend.u.log_message("[EXPORT] Export at: {}".format(report_dir), level="INFO")
if success:
if report_dir and "|||" in report_dir:
report_path, acpi_dir = report_dir.split("|||", 1)
else:
report_path = ""
acpi_dir = ""
if report_path and acpi_dir:
self.load_hardware_report(report_path, acpi_dir, from_export=True)
else:
self.progress_helper.update("error", "Export completed but paths are invalid", None)
self.controller.update_status("Export completed but paths are invalid", "error")
else:
self.progress_helper.update("error", "Export failed: {}".format(hardware_sniffer_or_error), None)
self.controller.update_status("Export failed: {}".format(hardware_sniffer_or_error), "error")

View File

@@ -0,0 +1,271 @@
import os
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QFileDialog
)
from PyQt6.QtCore import Qt
from qfluentwidgets import (
ScrollArea, BodyLabel, PushButton, LineEdit, FluentIcon,
SettingCardGroup, SwitchSettingCard, ComboBoxSettingCard,
PushSettingCard, SpinBox,
OptionsConfigItem, OptionsValidator, HyperlinkCard,
StrongBodyLabel, CaptionLabel, SettingCard, SubtitleLabel,
setTheme, Theme
)
from Scripts.custom_dialogs import show_confirmation
from Scripts.styles import COLORS, SPACING
class SettingsPage(ScrollArea):
def __init__(self, parent):
super().__init__(parent)
self.setObjectName("settingsPage")
self.controller = parent
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.settings = self.controller.backend.settings
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.enableTransparentBackground()
self._init_ui()
def _init_ui(self):
self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"])
self.expandLayout.setSpacing(SPACING["large"])
header_container = QWidget()
header_layout = QVBoxLayout(header_container)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.setSpacing(SPACING["tiny"])
title_label = SubtitleLabel("Settings")
header_layout.addWidget(title_label)
subtitle_label = BodyLabel("Configure OpCore Simplify preferences")
subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"]))
header_layout.addWidget(subtitle_label)
self.expandLayout.addWidget(header_container)
self.expandLayout.addSpacing(SPACING["medium"])
self.build_output_group = self.create_build_output_group()
self.expandLayout.addWidget(self.build_output_group)
self.macos_group = self.create_macos_version_group()
self.expandLayout.addWidget(self.macos_group)
#self.appearance_group = self.create_appearance_group()
#self.expandLayout.addWidget(self.appearance_group)
self.update_group = self.create_update_settings_group()
self.expandLayout.addWidget(self.update_group)
self.advanced_group = self.create_advanced_group()
self.expandLayout.addWidget(self.advanced_group)
self.help_group = self.create_help_group()
self.expandLayout.addWidget(self.help_group)
self.bottom_widget = QWidget()
bottom_layout = QHBoxLayout(self.bottom_widget)
bottom_layout.setContentsMargins(0, SPACING["large"], 0, SPACING["large"])
bottom_layout.setSpacing(SPACING["medium"])
bottom_layout.addStretch()
reset_btn = PushButton("Reset All to Defaults", self.bottom_widget)
reset_btn.setIcon(FluentIcon.CANCEL)
reset_btn.clicked.connect(self.reset_to_defaults)
bottom_layout.addWidget(reset_btn)
self.expandLayout.addWidget(self.bottom_widget)
for card in self.findChildren(SettingCard):
card.setIconSize(18, 18)
def _update_widget_value(self, widget, value):
if widget is None:
return
if isinstance(widget, SwitchSettingCard):
widget.switchButton.setChecked(value)
elif isinstance(widget, (ComboBoxSettingCard, OptionsConfigItem)):
widget.setValue(value)
elif isinstance(widget, SpinBox):
widget.setValue(value)
elif isinstance(widget, LineEdit):
widget.setText(value)
elif isinstance(widget, PushSettingCard):
widget.setContent(value or "Use temporary directory (default)")
def create_build_output_group(self):
group = SettingCardGroup("Build Output", self.scrollWidget)
self.output_dir_card = PushSettingCard(
"Browse",
FluentIcon.FOLDER,
"Output Directory",
self.settings.get("build_output_directory") or "Use temporary directory (default)",
group
)
self.output_dir_card.setObjectName("build_output_directory")
self.output_dir_card.clicked.connect(self.browse_output_directory)
group.addSettingCard(self.output_dir_card)
return group
def create_macos_version_group(self):
group = SettingCardGroup("macOS Version", self.scrollWidget)
self.include_beta_card = SwitchSettingCard(
FluentIcon.UPDATE,
"Include beta version",
"Show major beta macOS versions in version selection menus. Enable to test new macOS releases.",
configItem=None,
parent=group
)
self.include_beta_card.setObjectName("include_beta_versions")
self.include_beta_card.switchButton.setChecked(self.settings.get_include_beta_versions())
self.include_beta_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("include_beta_versions", checked))
group.addSettingCard(self.include_beta_card)
return group
def create_appearance_group(self):
group = SettingCardGroup("Appearance", self.scrollWidget)
theme_values = [
"Light",
#"Dark",
]
theme_value = self.settings.get_theme()
if theme_value not in theme_values:
theme_value = "Light"
self.theme_config = OptionsConfigItem(
"Appearance",
"Theme",
theme_value,
OptionsValidator(theme_values)
)
def on_theme_changed(value):
self.settings.set("theme", value)
if value == "Dark":
setTheme(Theme.DARK)
else:
setTheme(Theme.LIGHT)
self.theme_config.valueChanged.connect(on_theme_changed)
self.theme_card = ComboBoxSettingCard(
self.theme_config,
FluentIcon.BRUSH,
"Theme",
"Selects the application color theme.",
theme_values,
group
)
self.theme_card.setObjectName("theme")
group.addSettingCard(self.theme_card)
return group
def create_update_settings_group(self):
group = SettingCardGroup("Updates & Downloads", self.scrollWidget)
self.auto_update_card = SwitchSettingCard(
FluentIcon.UPDATE,
"Check for updates on startup",
"Automatically checks for new OpCore Simplify updates when the application launches to keep you up to date",
configItem=None,
parent=group
)
self.auto_update_card.setObjectName("auto_update_check")
self.auto_update_card.switchButton.setChecked(self.settings.get_auto_update_check())
self.auto_update_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("auto_update_check", checked))
group.addSettingCard(self.auto_update_card)
return group
def create_advanced_group(self):
group = SettingCardGroup("Advanced Settings", self.scrollWidget)
self.debug_logging_card = SwitchSettingCard(
FluentIcon.DEVELOPER_TOOLS,
"Enable debug logging",
"Enables detailed debug logging throughout the application for advanced troubleshooting and diagnostics",
configItem=None,
parent=group
)
self.debug_logging_card.setObjectName("enable_debug_logging")
self.debug_logging_card.switchButton.setChecked(self.settings.get_enable_debug_logging())
self.debug_logging_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("enable_debug_logging", checked))
group.addSettingCard(self.debug_logging_card)
return group
def create_help_group(self):
group = SettingCardGroup("Help & Documentation", self.scrollWidget)
self.opencore_docs_card = HyperlinkCard(
"https://dortania.github.io/OpenCore-Install-Guide/",
"OpenCore Install Guide",
FluentIcon.BOOK_SHELF,
"OpenCore Documentation",
"Complete guide for installing macOS with OpenCore",
group
)
group.addSettingCard(self.opencore_docs_card)
self.troubleshoot_card = HyperlinkCard(
"https://dortania.github.io/OpenCore-Install-Guide/troubleshooting/troubleshooting.html",
"Troubleshooting",
FluentIcon.HELP,
"Troubleshooting Guide",
"Solutions to common OpenCore installation issues",
group
)
group.addSettingCard(self.troubleshoot_card)
self.github_card = HyperlinkCard(
"https://github.com/lzhoang2801/OpCore-Simplify",
"View on GitHub",
FluentIcon.GITHUB,
"OpCore-Simplify Repository",
"Report issues, contribute, or view the source code",
group
)
group.addSettingCard(self.github_card)
return group
def browse_output_directory(self):
folder = QFileDialog.getExistingDirectory(
self,
"Select Build Output Directory",
os.path.expanduser("~")
)
if folder:
self.settings.set("build_output_directory", folder)
self.output_dir_card.setContent(folder)
self.controller.update_status("Output directory updated successfully", "success")
def reset_to_defaults(self):
result = show_confirmation("Reset Settings", "Are you sure you want to reset all settings to their default values?")
if result:
self.settings.settings = self.settings.defaults.copy()
self.settings.save_settings()
for widget in self.findChildren(QWidget):
key = widget.objectName()
if key and key in self.settings.defaults:
default_value = self.settings.defaults.get(key)
self._update_widget_value(widget, default_value)
self.controller.update_status("All settings reset to defaults", "success")

View File

@@ -4,10 +4,10 @@ import os
import re import re
class ReportValidator: class ReportValidator:
def __init__(self): def __init__(self, utils_instance=None):
self.errors = [] self.errors = []
self.warnings = [] self.warnings = []
self.u = utils.Utils() self.u = utils_instance if utils_instance else utils.Utils()
self.PATTERNS = { self.PATTERNS = {
"not_empty": r".+", "not_empty": r".+",
@@ -244,17 +244,17 @@ class ReportValidator:
if expected_type: if expected_type:
if not isinstance(data, expected_type): if not isinstance(data, expected_type):
type_name = expected_type.__name__ if hasattr(expected_type, "__name__") else str(expected_type) type_name = expected_type.__name__ if hasattr(expected_type, "__name__") else str(expected_type)
self.errors.append(f"{path}: Expected type {type_name}, got {type(data).__name__}") self.errors.append("{}: Expected type {}, got {}".format(path, type_name, type(data).__name__))
return None return None
if isinstance(data, str): if isinstance(data, str):
pattern = rule.get("pattern") pattern = rule.get("pattern")
if pattern is not None: if pattern is not None:
if not re.match(pattern, data): if not re.match(pattern, data):
self.errors.append(f"{path}: Value '{data}' does not match pattern '{pattern}'") self.errors.append("{}: Value '{}' does not match pattern '{}'".format(path, data, pattern))
return None return None
elif not re.match(self.PATTERNS["not_empty"], data): elif not re.match(self.PATTERNS["not_empty"], data):
self.errors.append(f"{path}: Value '{data}' does not match pattern '{self.PATTERNS['not_empty']}'") self.errors.append("{}: Value '{}' does not match pattern '{}'".format(path, data, self.PATTERNS["not_empty"]))
return None return None
cleaned_data = data cleaned_data = data
@@ -265,53 +265,30 @@ class ReportValidator:
for key, value in data.items(): for key, value in data.items():
if key in schema_keys: if key in schema_keys:
cleaned_val = self._validate_node(value, schema_keys[key], f"{path}.{key}") cleaned_val = self._validate_node(value, schema_keys[key], "{}.".format(path, key))
if cleaned_val is not None: if cleaned_val is not None:
cleaned_data[key] = cleaned_val cleaned_data[key] = cleaned_val
elif "values_rule" in rule: elif "values_rule" in rule:
cleaned_val = self._validate_node(value, rule["values_rule"], f"{path}.{key}") cleaned_val = self._validate_node(value, rule["values_rule"], "{}.".format(path, key))
if cleaned_val is not None: if cleaned_val is not None:
cleaned_data[key] = cleaned_val cleaned_data[key] = cleaned_val
else: else:
if schema_keys: if schema_keys:
self.warnings.append(f"{path}: Unknown key '{key}'") self.warnings.append("{}: Unknown key '{}'".format(path, key))
for key, key_rule in schema_keys.items(): for key, key_rule in schema_keys.items():
if key_rule.get("required", True) and key not in cleaned_data: if key_rule.get("required", True) and key not in cleaned_data:
self.errors.append(f"{path}: Missing required key '{key}'") self.errors.append("{}: Missing required key '{}'".format(path, key))
elif isinstance(data, list): elif isinstance(data, list):
item_rule = rule.get("item_rule") item_rule = rule.get("item_rule")
if item_rule: if item_rule:
cleaned_data = [] cleaned_data = []
for i, item in enumerate(data): for i, item in enumerate(data):
cleaned_val = self._validate_node(item, item_rule, f"{path}[{i}]") cleaned_val = self._validate_node(item, item_rule, "{}[{}]".format(path, i))
if cleaned_val is not None: if cleaned_val is not None:
cleaned_data.append(cleaned_val) cleaned_data.append(cleaned_val)
else: else:
cleaned_data = list(data) cleaned_data = list(data)
return cleaned_data return cleaned_data
def show_validation_report(self, report_path, is_valid, errors, warnings):
self.u.head("Validation Report")
print("")
print("Validation report for: {}".format(report_path))
print("")
if is_valid:
print("Hardware report is valid!")
else:
print("Hardware report is not valid! Please check the errors and warnings below.")
if errors:
print("")
print("\033[31mErrors ({}):\033[0m".format(len(errors)))
for i, error in enumerate(errors, 1):
print(" {}. {}".format(i, error))
if warnings:
print("")
print("\033[33mWarnings ({}):\033[0m".format(len(warnings)))
for i, warning in enumerate(warnings, 1):
print(" {}. {}".format(i, warning))

View File

@@ -20,14 +20,14 @@ else:
MAX_ATTEMPTS = 3 MAX_ATTEMPTS = 3
class ResourceFetcher: class ResourceFetcher:
def __init__(self, headers=None): def __init__(self, utils_instance=None, integrity_checker_instance=None, headers=None):
self.request_headers = headers or { self.request_headers = headers or {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
} }
self.utils = utils_instance if utils_instance else utils.Utils()
self.buffer_size = 16 * 1024 self.buffer_size = 16 * 1024
self.ssl_context = self.create_ssl_context() self.ssl_context = self.create_ssl_context()
self.integrity_checker = integrity_checker.IntegrityChecker() self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker()
self.utils = utils.Utils()
def create_ssl_context(self): def create_ssl_context(self):
try: try:
@@ -36,9 +36,10 @@ class ResourceFetcher:
import certifi import certifi
cafile = certifi.where() cafile = certifi.where()
ssl_context = ssl.create_default_context(cafile=cafile) ssl_context = ssl.create_default_context(cafile=cafile)
self.utils.log_message("[RESOURCE FETCHER] Created SSL context", level="INFO")
except Exception as e: except Exception as e:
print("Failed to create SSL context: {}".format(e))
ssl_context = ssl._create_unverified_context() ssl_context = ssl._create_unverified_context()
self.utils.log_message("[RESOURCE FETCHER] Created unverified SSL context", level="INFO")
return ssl_context return ssl_context
def _make_request(self, resource_url, timeout=10): def _make_request(self, resource_url, timeout=10):
@@ -48,13 +49,13 @@ class ResourceFetcher:
return urlopen(Request(resource_url, headers=headers), timeout=timeout, context=self.ssl_context) return urlopen(Request(resource_url, headers=headers), timeout=timeout, context=self.ssl_context)
except socket.timeout as e: except socket.timeout as e:
print("Timeout error: {}".format(e)) self.utils.log_message("[RESOURCE FETCHER] Timeout error: {}".format(e), level="ERROR", to_build_log=True)
except ssl.SSLError as e: except ssl.SSLError as e:
print("SSL error: {}".format(e)) self.utils.log_message("[RESOURCE FETCHER] SSL error: {}".format(e), level="ERROR", to_build_log=True)
except (URLError, socket.gaierror) as e: except (URLError, socket.gaierror) as e:
print("Connection error: {}".format(e)) self.utils.log_message("[RESOURCE FETCHER] Connection error: {}".format(e), level="ERROR", to_build_log=True)
except Exception as e: except Exception as e:
print("Request failed: {}".format(e)) self.utils.log_message("[RESOURCE FETCHER] Request failed: {}".format(e), level="ERROR", to_build_log=True)
return None return None
@@ -62,12 +63,14 @@ class ResourceFetcher:
attempt = 0 attempt = 0
response = None response = None
while attempt < 3: self.utils.log_message("[RESOURCE FETCHER] Fetching and parsing content from {}".format(resource_url), level="INFO")
while attempt < MAX_ATTEMPTS:
response = self._make_request(resource_url) response = self._make_request(resource_url)
if not response: if not response:
attempt += 1 attempt += 1
print("Failed to fetch content from {}. Retrying...".format(resource_url)) self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True)
continue continue
if response.getcode() == 200: if response.getcode() == 200:
@@ -76,7 +79,7 @@ class ResourceFetcher:
attempt += 1 attempt += 1
if not response: if not response:
print("Failed to fetch content from {}".format(resource_url)) self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}".format(resource_url), level="ERROR", to_build_log=True)
return None return None
content = response.read() content = response.read()
@@ -85,12 +88,12 @@ class ResourceFetcher:
try: try:
content = gzip.decompress(content) content = gzip.decompress(content)
except Exception as e: except Exception as e:
print("Failed to decompress gzip content: {}".format(e)) self.utils.log_message("[RESOURCE FETCHER] Failed to decompress gzip content: {}".format(e), level="ERROR", to_build_log=True)
elif response.info().get("Content-Encoding") == "deflate": elif response.info().get("Content-Encoding") == "deflate":
try: try:
content = zlib.decompress(content) content = zlib.decompress(content)
except Exception as e: except Exception as e:
print("Failed to decompress deflate content: {}".format(e)) self.utils.log_message("[RESOURCE FETCHER] Failed to decompress deflate content: {}".format(e), level="ERROR", to_build_log=True)
try: try:
if content_type == "json": if content_type == "json":
@@ -100,7 +103,7 @@ class ResourceFetcher:
else: else:
return content.decode("utf-8") return content.decode("utf-8")
except Exception as e: except Exception as e:
print("Error parsing content as {}: {}".format(content_type, e)) self.utils.log_message("[RESOURCE FETCHER] Error parsing content as {}: {}".format(content_type, e), level="ERROR", to_build_log=True)
return None return None
@@ -150,20 +153,19 @@ class ResourceFetcher:
else: else:
progress = "{} {:.1f}MB downloaded".format(speed_str, bytes_downloaded/(1024*1024)) progress = "{} {:.1f}MB downloaded".format(speed_str, bytes_downloaded/(1024*1024))
print(" " * 80, end="\r") self.utils.log_message("[RESOURCE FETCHER] Download progress: {}".format(progress), level="INFO", to_build_log=True)
print(progress, end="\r")
print()
def download_and_save_file(self, resource_url, destination_path, sha256_hash=None): def download_and_save_file(self, resource_url, destination_path, sha256_hash=None):
attempt = 0 attempt = 0
self.utils.log_message("[RESOURCE FETCHER] Downloading and saving file from {} to {}".format(resource_url, destination_path), level="INFO")
while attempt < MAX_ATTEMPTS: while attempt < MAX_ATTEMPTS:
attempt += 1 attempt += 1
response = self._make_request(resource_url) response = self._make_request(resource_url)
if not response: if not response:
print("Failed to fetch content from {}. Retrying...".format(resource_url)) self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True)
continue continue
with open(destination_path, "wb") as local_file: with open(destination_path, "wb") as local_file:
@@ -171,24 +173,24 @@ class ResourceFetcher:
if os.path.exists(destination_path) and os.path.getsize(destination_path) > 0: if os.path.exists(destination_path) and os.path.getsize(destination_path) > 0:
if sha256_hash: if sha256_hash:
print("Verifying SHA256 checksum...") self.utils.log_message("[RESOURCE FETCHER] Verifying SHA256 checksum...", level="INFO", to_build_log=True)
downloaded_hash = self.integrity_checker.get_sha256(destination_path) downloaded_hash = self.integrity_checker.get_sha256(destination_path)
if downloaded_hash.lower() == sha256_hash.lower(): if downloaded_hash.lower() == sha256_hash.lower():
print("Checksum verified successfully.") self.utils.log_message("[RESOURCE FETCHER] Checksum verified successfully.", level="INFO", to_build_log=True)
return True return True
else: else:
print("Checksum mismatch! Removing file and retrying download...") self.utils.log_message("[RESOURCE FETCHER] Checksum mismatch! Removing file and retrying download...", level="WARNING", to_build_log=True)
os.remove(destination_path) os.remove(destination_path)
continue continue
else: else:
print("No SHA256 hash provided. Downloading file without verification.") self.utils.log_message("[RESOURCE FETCHER] No SHA256 hash provided. Downloading file without verification.", level="INFO", to_build_log=True)
return True return True
if os.path.exists(destination_path): if os.path.exists(destination_path):
os.remove(destination_path) os.remove(destination_path)
if attempt < MAX_ATTEMPTS: if attempt < MAX_ATTEMPTS:
print("Download failed for {}. Retrying...".format(resource_url)) self.utils.log_message("[RESOURCE FETCHER] Download failed for {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True)
print("Failed to download {} after {} attempts.".format(resource_url, MAX_ATTEMPTS)) self.utils.log_message("[RESOURCE FETCHER] Failed to download {} after {} attempts.".format(resource_url, MAX_ATTEMPTS), level="ERROR", to_build_log=True)
return False return False

53
Scripts/settings.py Normal file
View File

@@ -0,0 +1,53 @@
import os
from Scripts import utils
class Settings:
def __init__(self, utils_instance=None):
self.u = utils_instance if utils_instance else utils.Utils()
self.defaults = {
"build_output_directory": "",
"include_beta_versions": False,
"theme": "Light",
"auto_update_check": True,
"enable_debug_logging": False,
}
self.settings_file = self._get_settings_file_path()
self.settings = self.load_settings()
def _get_settings_file_path(self):
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(script_dir, "settings.json")
def load_settings(self):
try:
loaded_settings = self.u.read_file(self.settings_file)
if loaded_settings is not None:
return loaded_settings
except Exception as e:
print("Error loading settings: {}".format(e))
return self.defaults.copy()
def save_settings(self):
try:
self.u.write_file(self.settings_file, self.settings)
except Exception as e:
print("Error saving settings: {}".format(e))
def get(self, key, default=None):
return self.settings.get(key, self.defaults.get(key, default))
def set(self, key, value):
self.settings[key] = value
self.save_settings()
def __getattr__(self, name):
if name.startswith("get_"):
key = name[4:]
if key in self.defaults:
return lambda: self.get(key)
raise AttributeError("\"{}\" object has no attribute \"{}\"".format(type(self).__name__, name))

View File

@@ -1,9 +1,11 @@
from Scripts.datasets.mac_model_data import mac_devices from Scripts.datasets.mac_model_data import mac_devices
from Scripts.datasets import kext_data from Scripts.datasets import kext_data
from Scripts.datasets import os_data from Scripts.datasets import os_data
from Scripts.custom_dialogs import show_smbios_selection_dialog
from Scripts import gathering_files from Scripts import gathering_files
from Scripts import run from Scripts import run
from Scripts import utils from Scripts import utils
from Scripts import settings
import os import os
import uuid import uuid
import random import random
@@ -12,10 +14,11 @@ import platform
os_name = platform.system() os_name = platform.system()
class SMBIOS: class SMBIOS:
def __init__(self): def __init__(self, gathering_files_instance=None, run_instance=None, utils_instance=None, settings_instance=None):
self.g = gathering_files.gatheringFiles() self.g = gathering_files_instance if gathering_files_instance else gathering_files.gatheringFiles()
self.run = run.Run().run self.run = run_instance.run if run_instance else run.Run().run
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
self.settings = settings_instance if settings_instance else settings.Settings()
self.script_dir = os.path.dirname(os.path.realpath(__file__)) self.script_dir = os.path.dirname(os.path.realpath(__file__))
def check_macserial(self, retry_count=0): def check_macserial(self, retry_count=0):
@@ -28,6 +31,7 @@ class SMBIOS:
elif os_name == "Darwin": elif os_name == "Darwin":
macserial_binary = ["macserial"] macserial_binary = ["macserial"]
else: else:
self.utils.log_message("[SMBIOS] Unknown OS for macserial", level="ERROR")
raise Exception("Unknown OS for macserial") raise Exception("Unknown OS for macserial")
for binary in macserial_binary: for binary in macserial_binary:
@@ -36,6 +40,7 @@ class SMBIOS:
return macserial_path return macserial_path
if retry_count >= max_retries: if retry_count >= max_retries:
self.utils.log_message("[SMBIOS] Failed to find macserial after {} attempts".format(max_retries), level="ERROR")
raise Exception("Failed to find macserial after {} attempts".format(max_retries)) raise Exception("Failed to find macserial after {} attempts".format(max_retries))
download_history = self.utils.read_file(self.g.download_history_file) download_history = self.utils.read_file(self.g.download_history_file)
@@ -68,13 +73,21 @@ class SMBIOS:
else: else:
serial = output[0].splitlines()[0].split(" | ") serial = output[0].splitlines()[0].split(" | ")
return { smbios_info = {
"MLB": "A" + "0"*15 + "Z" if not serial else serial[-1], "MLB": "A" + "0"*15 + "Z" if not serial else serial[-1],
"ROM": random_mac_address, "ROM": random_mac_address,
"SystemProductName": smbios_model, "SystemProductName": smbios_model,
"SystemSerialNumber": "A" + "0"*10 + "9" if not serial else serial[0], "SystemSerialNumber": "A" + "0"*10 + "9" if not serial else serial[0],
"SystemUUID": str(uuid.uuid4()).upper(), "SystemUUID": str(uuid.uuid4()).upper(),
} }
self.utils.log_message("[SMBIOS] Generated SMBIOS info: MLB: {}..., ROM: {}..., SystemProductName: {}, SystemSerialNumber: {}..., SystemUUID: {}...".format(
smbios_info["MLB"][:5],
smbios_info["ROM"][:5],
smbios_info["SystemProductName"],
smbios_info["SystemSerialNumber"][:5],
smbios_info["SystemUUID"].split("-")[0]), level="INFO")
return smbios_info
def smbios_specific_options(self, hardware_report, smbios_model, macos_version, acpi_patches, kext_maestro): def smbios_specific_options(self, hardware_report, smbios_model, macos_version, acpi_patches, kext_maestro):
for patch in acpi_patches: for patch in acpi_patches:
@@ -160,79 +173,45 @@ class SMBIOS:
elif "Ice Lake" in codename: elif "Ice Lake" in codename:
smbios_model = "MacBookAir9,1" smbios_model = "MacBookAir9,1"
self.utils.log_message("[SMBIOS] Suggested SMBIOS model: {}".format(smbios_model), level="INFO")
return smbios_model return smbios_model
def customize_smbios_model(self, hardware_report, selected_smbios_model, macos_version): def customize_smbios_model(self, hardware_report, selected_smbios_model, macos_version, parent=None):
current_category = None
default_smbios_model = self.select_smbios_model(hardware_report, macos_version) default_smbios_model = self.select_smbios_model(hardware_report, macos_version)
show_all_models = False
is_laptop = "Laptop" == hardware_report.get("Motherboard").get("Platform") is_laptop = "Laptop" == hardware_report.get("Motherboard").get("Platform")
macos_name = os_data.get_macos_name_by_darwin(macos_version)
while True:
incompatible_models_by_index = [] items = []
contents = [] for index, device in enumerate(mac_devices):
contents.append("") is_supported = self.utils.parse_darwin_version(device.initial_support) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(device.last_supported_version)
if show_all_models:
contents.append("List of available SMBIOS:") platform_match = True
else: if is_laptop and not device.name.startswith("MacBook"):
contents.append("List of compatible SMBIOS:") platform_match = False
for index, device in enumerate(mac_devices, start=1): elif not is_laptop and device.name.startswith("MacBook"):
isSupported = self.utils.parse_darwin_version(device.initial_support) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(device.last_supported_version) platform_match = False
if device.name not in (default_smbios_model, selected_smbios_model) and not show_all_models and (not isSupported or (is_laptop and not device.name.startswith("MacBook")) or (not is_laptop and device.name.startswith("MacBook"))):
incompatible_models_by_index.append(index - 1)
continue
category = ""
for char in device.name:
if char.isdigit():
break
category += char
if category != current_category:
current_category = category
category_header = "Category: {}".format(current_category if current_category else "Uncategorized")
contents.append(f"\n{category_header}\n" + "=" * len(category_header))
checkbox = "[*]" if device.name == selected_smbios_model else "[ ]"
line = "{} {:2}. {:15} - {:10} {:20}{}".format(checkbox, index, device.name, device.cpu, "({})".format(device.cpu_generation), "" if not device.discrete_gpu else " - {}".format(device.discrete_gpu)) is_compatible = is_supported and platform_match
if device.name == selected_smbios_model:
line = "\033[1;32m{}\033[0m".format(line) category = ""
elif not isSupported: for char in device.name:
line = "\033[90m{}\033[0m".format(line) if char.isdigit():
contents.append(line) break
contents.append("") category += char
contents.append("\033[1;93mNote:\033[0m")
contents.append("- Lines in gray indicate mac models that are not officially supported by {}.".format(os_data.get_macos_name_by_darwin(macos_version))) gpu_str = "" if not device.discrete_gpu else " - {}".format(device.discrete_gpu)
contents.append("") label = "{} - {} ({}){}".format(device.name, device.cpu, device.cpu_generation, gpu_str)
if not show_all_models:
contents.append("A. Show all models")
else:
contents.append("C. Show compatible models only")
if selected_smbios_model != default_smbios_model:
contents.append("R. Restore default SMBIOS model ({})".format(default_smbios_model))
contents.append("")
contents.append("B. Back")
contents.append("Q. Quit")
contents.append("")
content = "\n".join(contents)
self.utils.adjust_window_size(content) items.append({
self.utils.head("Customize SMBIOS Model", resize=False) 'name': device.name,
print(content) 'label': label,
option = self.utils.request_input("Select your option: ") 'category': category,
if option.lower() == "q": 'is_supported': is_supported,
self.utils.exit_program() 'is_compatible': is_compatible
if option.lower() == "b": })
return selected_smbios_model
if option.lower() == "r" and selected_smbios_model != default_smbios_model: content = "Lines in gray indicate mac models that are not officially supported by {}.".format(macos_name)
return default_smbios_model
if option.lower() in ("a", "c"): result = show_smbios_selection_dialog("Customize SMBIOS Model", content, items, selected_smbios_model, default_smbios_model)
show_all_models = not show_all_models
continue return result if result else selected_smbios_model
if option.strip().isdigit():
index = int(option) - 1
if index >= 0 and index < len(mac_devices):
if not show_all_models and index in incompatible_models_by_index:
continue
selected_smbios_model = mac_devices[index].name

36
Scripts/state.py Normal file
View File

@@ -0,0 +1,36 @@
from dataclasses import dataclass, field
from typing import Optional, Dict, List, Any
@dataclass
class HardwareReportState:
report_path: str = "Not selected"
acpi_dir: str = "Not selected"
hardware_report: Optional[Dict[str, Any]] = None
compatibility_error: Optional[str] = None
customized_hardware: Optional[Dict[str, Any]] = None
disabled_devices: Optional[Dict[str, str]] = None
audio_layout_id: Optional[int] = None
audio_controller_properties: Optional[Dict[str, Any]] = None
@dataclass
class macOSVersionState:
suggested_version: Optional[str] = None
selected_version_name: str = "Not selected"
darwin_version: str = ""
native_version: Optional[tuple] = None
ocl_patched_version: Optional[tuple] = None
needs_oclp: bool = False
@dataclass
class SMBIOSState:
model_name: str = "Not selected"
@dataclass
class BuildState:
in_progress: bool = False
successful: bool = False
log_messages: List[str] = field(default_factory=list)

68
Scripts/styles.py Normal file
View File

@@ -0,0 +1,68 @@
from typing import Final
COLORS: Final[dict[str, str]] = {
"primary": "#0078D4",
"primary_dark": "#005A9E",
"primary_light": "#4CC2FF",
"primary_hover": "#106EBE",
"bg_main": "#FFFFFF",
"bg_secondary": "#F3F3F3",
"bg_sidebar": "#F7F7F7",
"bg_hover": "#E8E8E8",
"bg_selected": "#0078D4",
"bg_card": "#FAFAFA",
"text_primary": "#000000",
"text_secondary": "#605E5C",
"text_tertiary": "#8A8886",
"text_sidebar": "#201F1E",
"text_sidebar_selected": "#FFFFFF",
"success": "#107C10",
"warning": "#FF8C00",
"error": "#E81123",
"info": "#0078D4",
"note_bg": "#E3F2FD",
"note_border": "#2196F3",
"note_text": "#1565C0",
"warning_bg": "#FFF3E0",
"warning_border": "#FF9800",
"warning_text": "#F57C00",
"success_bg": "#F3FAF3",
"border": "#D1D1D1",
"border_light": "#EDEBE9",
"border_focus": "#0078D4",
}
SPACING: Final[dict[str, int]] = {
"tiny": 4,
"small": 8,
"medium": 12,
"large": 16,
"xlarge": 20,
"xxlarge": 24,
"xxxlarge": 32,
}
SIZES: Final[dict[str, int]] = {
"sidebar_width": 220,
"sidebar_item_height": 40,
"button_height": 32,
"button_padding_x": 16,
"button_padding_y": 6,
"input_height": 32,
"icon_size": 16,
}
RADIUS: Final[dict[str, int]] = {
"small": 4,
"medium": 6,
"large": 8,
"xlarge": 10,
"button": 4,
"card": 8,
}

183
Scripts/ui_utils.py Normal file
View File

@@ -0,0 +1,183 @@
from typing import Optional, Tuple, TYPE_CHECKING
from PyQt6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor
from qfluentwidgets import FluentIcon, BodyLabel, CardWidget, StrongBodyLabel
from .styles import SPACING, COLORS, RADIUS
if TYPE_CHECKING:
from qfluentwidgets import GroupHeaderCardWidget, CardGroupWidget
class ProgressStatusHelper:
def __init__(self, status_icon_label, progress_label, progress_bar, progress_container):
self.status_icon_label = status_icon_label
self.progress_label = progress_label
self.progress_bar = progress_bar
self.progress_container = progress_container
def update(self, status, message, progress=None):
icon_size = 28
icon_map = {
"loading": (FluentIcon.SYNC, COLORS["primary"]),
"success": (FluentIcon.COMPLETED, COLORS["success"]),
"error": (FluentIcon.CLOSE, COLORS["error"]),
"warning": (FluentIcon.INFO, COLORS["warning"]),
}
if status in icon_map:
icon, color = icon_map[status]
pixmap = icon.icon(color=color).pixmap(icon_size, icon_size)
self.status_icon_label.setPixmap(pixmap)
self.progress_label.setText(message)
if status == "success":
self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["success"]))
elif status == "error":
self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["error"]))
elif status == "warning":
self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["warning"]))
else:
self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["primary"]))
if progress is not None:
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(progress)
else:
self.progress_bar.setRange(0, 0)
self.progress_container.setVisible(True)
class UIUtils:
def __init__(self):
pass
def build_icon_label(self, icon: FluentIcon, color: str, size: int = 32) -> QLabel:
label = QLabel()
label.setPixmap(icon.icon(color=color).pixmap(size, size))
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setFixedSize(size + 12, size + 12)
return label
def create_info_widget(self, text: str, color: Optional[str] = None) -> QWidget:
if not text:
return QWidget()
label = BodyLabel(text)
label.setWordWrap(True)
if color:
label.setStyleSheet("color: {};".format(color))
return label
def colored_icon(self, icon: FluentIcon, color_hex: str) -> FluentIcon:
if not icon or not color_hex:
return icon
tint = QColor(color_hex)
return icon.colored(tint, tint)
def get_compatibility_icon(self, compat_tuple: Optional[Tuple[Optional[str], Optional[str]]]) -> FluentIcon:
if not compat_tuple or compat_tuple == (None, None):
return self.colored_icon(FluentIcon.CLOSE, COLORS["error"])
return self.colored_icon(FluentIcon.ACCEPT, COLORS["success"])
def add_group_with_indent(self, card: "GroupHeaderCardWidget", icon: FluentIcon, title: str, content: str, widget: Optional[QWidget] = None, indent_level: int = 0) -> "CardGroupWidget":
if widget is None:
widget = QWidget()
group = card.addGroup(icon, title, content, widget)
if indent_level > 0:
base_margin = 24
indent = 20 * indent_level
group.hBoxLayout.setContentsMargins(base_margin + indent, 10, 24, 10)
return group
def create_step_indicator(self, step_number: int, total_steps: int = 4, color: str = "#0078D4") -> BodyLabel:
label = BodyLabel("STEP {} OF {}".format(step_number, total_steps))
label.setStyleSheet("color: {}; font-weight: bold;".format(color))
return label
def create_vertical_spacer(self, spacing: int = SPACING["medium"]) -> QWidget:
spacer = QWidget()
spacer.setFixedHeight(spacing)
return spacer
def custom_card(self, card_type: str = "note", icon: Optional[FluentIcon] = None, title: str = "", body: str = "", custom_widget: Optional[QWidget] = None, parent: Optional[QWidget] = None) -> CardWidget:
card_styles = {
"note": {
"bg": COLORS["note_bg"],
"text": COLORS["note_text"],
"border": "rgba(21, 101, 192, 0.2)",
"default_icon": FluentIcon.INFO
},
"warning": {
"bg": COLORS["warning_bg"],
"text": COLORS["warning_text"],
"border": "rgba(245, 124, 0, 0.25)",
"default_icon": FluentIcon.MEGAPHONE
},
"success": {
"bg": COLORS["success_bg"],
"text": COLORS["success"],
"border": "rgba(16, 124, 16, 0.2)",
"default_icon": FluentIcon.COMPLETED
},
"error": {
"bg": "#FFEBEE",
"text": COLORS["error"],
"border": "rgba(232, 17, 35, 0.25)",
"default_icon": FluentIcon.CLOSE
},
"info": {
"bg": COLORS["note_bg"],
"text": COLORS["info"],
"border": "rgba(0, 120, 212, 0.2)",
"default_icon": FluentIcon.INFO
}
}
style = card_styles.get(card_type, card_styles["note"])
if icon is None:
icon = style["default_icon"]
card = CardWidget(parent)
card.setStyleSheet(f"""
CardWidget {{
background-color: {style["bg"]};
border: 1px solid {style["border"]};
border-radius: {RADIUS["card"]}px;
}}
""")
main_layout = QHBoxLayout(card)
main_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
main_layout.setSpacing(SPACING["large"])
icon_label = self.build_icon_label(icon, style["text"], size=40)
main_layout.addWidget(icon_label, 0, Qt.AlignmentFlag.AlignVCenter)
text_layout = QVBoxLayout()
text_layout.setSpacing(SPACING["small"])
if title:
title_label = StrongBodyLabel(title)
title_label.setStyleSheet("color: {}; font-size: 16px;".format(style["text"]))
text_layout.addWidget(title_label)
if body:
body_label = BodyLabel(body)
body_label.setWordWrap(True)
body_label.setOpenExternalLinks(True)
body_label.setStyleSheet("color: #424242; line-height: 1.6;")
text_layout.addWidget(body_label)
if custom_widget:
text_layout.addWidget(custom_widget)
main_layout.addLayout(text_layout)
return card

View File

@@ -1,18 +1,38 @@
import os import os
import sys
import json import json
import plistlib import plistlib
import shutil import shutil
import re import re
import binascii import binascii
import subprocess import subprocess
import pathlib
import zipfile import zipfile
import tempfile import tempfile
import traceback
import contextlib
import logging
class Utils: class Utils:
def __init__(self, script_name = "OpCore Simplify"): def __init__(self):
self.script_name = script_name self.gui_handler = None
self.logger = logging.getLogger("OpCoreSimplify")
@contextlib.contextmanager
def safe_block(self, task_name="Operation", suppress_error=True):
try:
yield
except Exception as e:
error_details = "".join(traceback.format_exc())
self.log_message("Error during '{}': {}\n{}".format(task_name, str(e), error_details), level="ERROR")
if not suppress_error:
raise
def log_message(self, message, level="INFO", to_build_log=False):
log_level = getattr(logging, level.upper(), logging.INFO)
extra = {'to_build_log': to_build_log}
self.logger.log(log_level, message, extra=extra)
return True
def clean_temporary_dir(self): def clean_temporary_dir(self):
temporary_dir = tempfile.gettempdir() temporary_dir = tempfile.gettempdir()
@@ -26,6 +46,7 @@ class Utils:
try: try:
shutil.rmtree(os.path.join(temporary_dir, file)) shutil.rmtree(os.path.join(temporary_dir, file))
except Exception as e: except Exception as e:
self.log_message("[UTILS] Failed to remove temp directory {}: {}".format(file, e), "Error")
pass pass
def get_temporary_dir(self): def get_temporary_dir(self):
@@ -127,23 +148,6 @@ class Utils:
def contains_any(self, data, search_item, start=0, end=None): def contains_any(self, data, search_item, start=0, end=None):
return next((item for item in data[start:end] if item.lower() in search_item.lower()), None) return next((item for item in data[start:end] if item.lower() in search_item.lower()), None)
def normalize_path(self, path):
path = re.sub(r'^[\'"]+|[\'"]+$', '', path)
path = path.strip()
path = os.path.expanduser(path)
if os.name == 'nt':
path = path.replace('\\', '/')
path = re.sub(r'/+', '/', path)
else:
path = path.replace('\\', '')
path = os.path.normpath(path)
return str(pathlib.Path(path).resolve())
def parse_darwin_version(self, darwin_version): def parse_darwin_version(self, darwin_version):
major, minor, patch = map(int, darwin_version.split('.')) major, minor, patch = map(int, darwin_version.split('.'))
@@ -156,78 +160,4 @@ class Utils:
else: else:
subprocess.run(['xdg-open', folder_path]) subprocess.run(['xdg-open', folder_path])
elif os.name == 'nt': elif os.name == 'nt':
os.startfile(folder_path) os.startfile(folder_path)
def request_input(self, prompt="Press Enter to continue..."):
if sys.version_info[0] < 3:
user_response = raw_input(prompt)
else:
user_response = input(prompt)
if not isinstance(user_response, str):
user_response = str(user_response)
return user_response
def progress_bar(self, title, steps, current_step_index, done=False):
self.head(title)
print("")
if done:
for step in steps:
print(" [\033[92m✓\033[0m] {}".format(step))
else:
for i, step in enumerate(steps):
if i < current_step_index:
print(" [\033[92m✓\033[0m] {}".format(step))
elif i == current_step_index:
print(" [\033[1;93m>\033[0m] {}...".format(step))
else:
print(" [ ] {}".format(step))
print("")
def head(self, text = None, width = 68, resize=True):
if resize:
self.adjust_window_size()
os.system('cls' if os.name=='nt' else 'clear')
if text == None:
text = self.script_name
separator = "" * (width - 2)
title = " {} ".format(text)
if len(title) > width - 2:
title = title[:width-4] + "..."
title = title.center(width - 2)
print("{}\n{}\n{}".format(separator, title, separator))
def adjust_window_size(self, content=""):
lines = content.splitlines()
rows = len(lines)
cols = max(len(line) for line in lines) if lines else 0
print('\033[8;{};{}t'.format(max(rows+6, 30), max(cols+2, 100)))
def exit_program(self):
self.head()
width = 68
print("")
print("For more information, to report errors, or to contribute to the product:".center(width))
print("")
separator = "" * (width - 4)
print(f"{separator}")
contacts = {
"Facebook": "https://www.facebook.com/macforce2601",
"Telegram": "https://t.me/lzhoang2601",
"GitHub": "https://github.com/lzhoang2801/OpCore-Simplify"
}
for platform, link in contacts.items():
line = f" * {platform}: {link}"
print(f"{line.ljust(width - 4)}")
print(f"{separator}")
print("")
print("Thank you for using our program!".center(width))
print("")
self.request_input("Press Enter to exit.".center(width))
sys.exit(0)

View File

@@ -0,0 +1,29 @@
def format_value(value):
if value is None:
return "None"
elif isinstance(value, bool):
return "True" if value else "False"
elif isinstance(value, (bytes, bytearray)):
return value.hex().upper()
elif isinstance(value, str):
return value
return str(value)
def get_value_type(value):
if value is None:
return None
elif isinstance(value, dict):
return "Dictionary"
elif isinstance(value, list):
return "Array"
elif isinstance(value, (bytes, bytearray)):
return "Data"
elif isinstance(value, bool):
return "Boolean"
elif isinstance(value, (int, float)):
return "Number"
elif isinstance(value, str):
return "String"
return "String"

View File

@@ -0,0 +1,310 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTreeWidgetItem, QHeaderView, QAbstractItemView
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
from PyQt6.QtGui import QBrush, QColor
from qfluentwidgets import CardWidget, TreeWidget, BodyLabel, StrongBodyLabel
from Scripts.datasets.config_tooltips import get_tooltip
from Scripts.value_formatters import format_value, get_value_type
from Scripts.styles import SPACING, COLORS, RADIUS
class ConfigEditor(QWidget):
config_changed = pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("configEditor")
self.original_config = None
self.modified_config = None
self.context = {}
self.mainLayout = QVBoxLayout(self)
self._init_ui()
def _init_ui(self):
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
card = CardWidget()
card.setBorderRadius(RADIUS["card"])
card_layout = QVBoxLayout(card)
card_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"])
card_layout.setSpacing(SPACING["medium"])
title = StrongBodyLabel("Config Editor")
card_layout.addWidget(title)
description = BodyLabel("View differences between original and modified config.plist")
description.setStyleSheet("color: {}; font-size: 13px;".format(COLORS["text_secondary"]))
card_layout.addWidget(description)
self.tree = TreeWidget()
self.tree.setHeaderLabels(["Key", "", "Original", "Modified"])
self.tree.setColumnCount(4)
self.tree.setRootIsDecorated(True)
self.tree.setItemsExpandable(True)
self.tree.setExpandsOnDoubleClick(False)
self.tree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.tree.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.tree.itemExpanded.connect(self._update_tree_height)
self.tree.itemCollapsed.connect(self._update_tree_height)
header = self.tree.header()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.tree.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
card_layout.addWidget(self.tree)
self.mainLayout.addWidget(card)
def load_configs(self, original, modified, context=None):
self.original_config = original
self.modified_config = modified
self.context = context or {}
self.tree.clear()
self._render_config(self.original_config, self.modified_config, [])
QTimer.singleShot(0, self._update_tree_height)
def _get_all_keys_from_both_configs(self, original, modified):
original_keys = set(original.keys()) if isinstance(original, dict) else set()
modified_keys = set(modified.keys()) if isinstance(modified, dict) else set()
return sorted(original_keys | modified_keys)
def _determine_change_type(self, original_value, modified_value, key_in_original, key_in_modified):
if not key_in_original:
return "added"
elif not key_in_modified:
return "removed"
elif original_value != modified_value:
return "modified"
return None
def _get_effective_value(self, original_value, modified_value):
return modified_value if modified_value is not None else original_value
def _get_safe_value(self, value, default = None):
return value if value is not None else default
def _set_value_columns(self, item, original_value, modified_value, change_type, is_dict=False, is_array=False):
if original_value is not None:
if is_dict:
item.setText(2, "<object: {} keys>".format(len(original_value)))
elif is_array:
item.setText(2, "<array: {} items>".format(len(original_value)))
else:
item.setText(2, format_value(original_value))
item.setData(2, Qt.ItemDataRole.UserRole, get_value_type(original_value))
else:
item.setText(2, "")
if change_type is not None and modified_value is not None:
if is_dict:
item.setText(3, "<object: {} keys>".format(len(modified_value)))
elif is_array:
item.setText(3, "<array: {} items>".format(len(modified_value)))
else:
item.setText(3, format_value(modified_value))
item.setData(3, Qt.ItemDataRole.UserRole, get_value_type(modified_value))
else:
item.setText(3, "")
def _build_path_string(self, path_parts):
if not path_parts:
return ""
return ".".join(path_parts)
def _build_array_path(self, path_parts, index):
return path_parts + [f"[{index}]"]
def _render_config(self, original, modified, path_parts, parent_item=None):
if parent_item is None:
parent_item = self.tree.invisibleRootItem()
all_keys = self._get_all_keys_from_both_configs(original, modified)
for key in all_keys:
current_path_parts = path_parts + [key]
current_path = self._build_path_string(current_path_parts)
original_value = original.get(key) if isinstance(original, dict) else None
modified_value = modified.get(key) if isinstance(modified, dict) else None
key_in_original = key in original if isinstance(original, dict) else False
key_in_modified = key in modified if isinstance(modified, dict) else False
change_type = self._determine_change_type(
original_value, modified_value, key_in_original, key_in_modified
)
effective_value = self._get_effective_value(original_value, modified_value)
if isinstance(effective_value, list):
self._render_array(
original_value if isinstance(original_value, list) else [],
modified_value if isinstance(modified_value, list) else [],
current_path_parts,
parent_item
)
continue
item = QTreeWidgetItem(parent_item)
item.setText(0, key)
item.setData(0, Qt.ItemDataRole.UserRole, current_path)
self._apply_highlighting(item, change_type)
self._setup_tooltip(item, current_path, modified_value, original_value)
if isinstance(effective_value, dict):
item.setData(3, Qt.ItemDataRole.UserRole, "dict")
self._set_value_columns(item, original_value, modified_value, change_type, is_dict=True)
self._render_config(
self._get_safe_value(original_value, {}),
self._get_safe_value(modified_value, {}),
current_path_parts,
item
)
else:
self._set_value_columns(item, original_value, modified_value, change_type)
def _render_array(self, original_array, modified_array, path_parts, parent_item):
change_type = self._determine_change_type(
original_array, modified_array,
original_array is not None, modified_array is not None
)
effective_original = self._get_safe_value(original_array, [])
effective_modified = self._get_safe_value(modified_array, [])
path_string = self._build_path_string(path_parts)
array_key = path_parts[-1] if path_parts else "array"
item = QTreeWidgetItem(parent_item)
item.setText(0, array_key)
item.setData(0, Qt.ItemDataRole.UserRole, path_string)
item.setData(3, Qt.ItemDataRole.UserRole, "array")
self._apply_highlighting(item, change_type)
self._setup_tooltip(item, path_string, modified_array, original_array)
self._set_value_columns(item, original_array, modified_array, change_type, is_array=True)
original_len = len(effective_original)
modified_len = len(effective_modified)
max_len = max(original_len, modified_len)
for i in range(max_len):
original_element = effective_original[i] if i < original_len else None
modified_element = effective_modified[i] if i < modified_len else None
element_change_type = self._determine_change_type(
original_element, modified_element,
original_element is not None, modified_element is not None
)
effective_element = self._get_effective_value(original_element, modified_element)
if effective_element is None:
continue
element_path_parts = self._build_array_path(path_parts, i)
element_path = self._build_path_string(element_path_parts)
element_item = QTreeWidgetItem(item)
element_item.setText(0, "[{}]".format(i))
element_item.setData(0, Qt.ItemDataRole.UserRole, element_path)
self._apply_highlighting(element_item, element_change_type)
self._setup_tooltip(element_item, element_path, modified_element, original_element)
if isinstance(effective_element, dict):
element_item.setData(3, Qt.ItemDataRole.UserRole, "dict")
self._set_value_columns(element_item, original_element, modified_element, element_change_type, is_dict=True)
self._render_config(
self._get_safe_value(original_element, {}),
self._get_safe_value(modified_element, {}),
element_path_parts,
element_item
)
elif isinstance(effective_element, list):
element_item.setData(3, Qt.ItemDataRole.UserRole, "array")
self._set_value_columns(element_item, original_element, modified_element, element_change_type, is_array=True)
self._render_array(
self._get_safe_value(original_element, []),
self._get_safe_value(modified_element, []),
element_path_parts,
element_item
)
else:
self._set_value_columns(element_item, original_element, modified_element, element_change_type)
def _apply_highlighting(self, item, change_type=None):
if change_type == "added":
color = "#E3F2FD"
status_text = "A"
elif change_type == "removed":
color = "#FFEBEE"
status_text = "R"
elif change_type == "modified":
color = "#FFF9C4"
status_text = "M"
else:
color = None
status_text = ""
item.setText(1, status_text)
if color:
brush = QBrush(QColor(color))
else:
brush = QBrush()
for col in range(4):
item.setBackground(col, brush)
def _setup_tooltip(self, item, key_path, value, original_value=None):
tooltip_text = get_tooltip(key_path, value, original_value, self.context)
item.setToolTip(0, tooltip_text)
def _calculate_tree_height(self):
if self.tree.topLevelItemCount() == 0:
return self.tree.header().height() if self.tree.header().isVisible() else 0
header_height = self.tree.header().height() if self.tree.header().isVisible() else 0
first_item = self.tree.topLevelItem(0)
row_height = 24
if first_item:
rect = self.tree.visualItemRect(first_item)
if rect.height() > 0:
row_height = rect.height()
else:
font_metrics = self.tree.fontMetrics()
row_height = font_metrics.height() + 6
def count_visible_rows(item):
count = 1
if item.isExpanded():
for i in range(item.childCount()):
count += count_visible_rows(item.child(i))
return count
total_rows = 0
for i in range(self.tree.topLevelItemCount()):
total_rows += count_visible_rows(self.tree.topLevelItem(i))
padding = 10
return header_height + (total_rows * row_height) + padding
def _update_tree_height(self):
height = self._calculate_tree_height()
if height > 0:
self.tree.setFixedHeight(height)

View File

@@ -1,14 +1,15 @@
from Scripts import run from Scripts import run
from Scripts import utils from Scripts import utils
from Scripts.custom_dialogs import ask_network_count, show_info, show_confirmation
import platform import platform
import json import json
os_name = platform.system() os_name = platform.system()
class WifiProfileExtractor: class WifiProfileExtractor:
def __init__(self): def __init__(self, run_instance=None, utils_instance=None):
self.run = run.Run().run self.run = run_instance.run if run_instance else run.Run().run
self.utils = utils.Utils() self.utils = utils_instance if utils_instance else utils.Utils()
def get_authentication_type(self, authentication_type): def get_authentication_type(self, authentication_type):
authentication_type = authentication_type.lower() authentication_type = authentication_type.lower()
@@ -27,14 +28,16 @@ class WifiProfileExtractor:
return None return None
def validate_wifi_password(self, authentication_type=None, password=None): def validate_wifi_password(self, authentication_type=None, password=None):
print("Validating password with authentication type: {}".format(authentication_type))
if password is None: if password is None:
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Password is not found", level="INFO")
return None return None
if authentication_type is None: if authentication_type is None:
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Authentication type is not found", level="INFO")
return password return password
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Validating password for \"{}\" with {} authentication type".format(password, authentication_type), level="INFO")
if authentication_type == "open": if authentication_type == "open":
return "" return ""
@@ -103,30 +106,14 @@ class WifiProfileExtractor:
return self.validate_wifi_password(authentication_type, password) return self.validate_wifi_password(authentication_type, password)
def ask_network_count(self, total_networks): def ask_network_count(self, total_networks):
self.utils.head("WiFi Network Retrieval") if self.utils.gui_handler:
print("") result = ask_network_count(total_networks)
print("Found {} WiFi networks on this device.".format(total_networks)) if result == 'a':
print("") return total_networks
print("How many networks would you like to process?") return int(result)
print(" 1-{} - Specific number (default: 5)".format(total_networks))
print(" A - All available networks")
print("")
num_choice = self.utils.request_input("Enter your choice: ").strip().lower() or "5" return 5
if num_choice == "a":
print("Will process all available networks.")
return total_networks
try:
max_networks = min(int(num_choice), total_networks)
print("Will process up to {} networks.".format(max_networks))
return max_networks
except:
max_networks = min(5, total_networks)
print("Invalid choice. Will process up to {} networks.".format(max_networks))
return max_networks
def process_networks(self, ssid_list, max_networks, get_password_func): def process_networks(self, ssid_list, max_networks, get_password_func):
networks = [] networks = []
processed_count = 0 processed_count = 0
@@ -137,39 +124,35 @@ class WifiProfileExtractor:
ssid = ssid_list[processed_count] ssid = ssid_list[processed_count]
try: try:
print("") self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving password for \"{}\" ({} of {})".format(ssid, processed_count + 1, len(ssid_list)), level="INFO", to_build_log=True)
print("Processing {}/{}: {}".format(processed_count + 1, len(ssid_list), ssid))
if os_name == "Darwin":
print("Please enter your administrator name and password or click 'Deny' to skip this network.")
password = get_password_func(ssid) password = get_password_func(ssid)
if password is not None: if password is not None:
if (ssid, password) not in networks: if (ssid, password) not in networks:
consecutive_failures = 0 consecutive_failures = 0
networks.append((ssid, password)) networks.append((ssid, password))
print("Successfully retrieved password.") self.utils.log_message("[WIFI PROFILE EXTRACTOR] Successfully retrieved password for \"{}\"".format(ssid), level="INFO", to_build_log=True)
if len(networks) == max_networks: if len(networks) == max_networks:
break break
else: else:
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Could not retrieve password for \"{}\"".format(ssid), level="INFO", to_build_log=True)
consecutive_failures += 1 if os_name == "Darwin" else 0 consecutive_failures += 1 if os_name == "Darwin" else 0
print("Could not retrieve password for this network.")
if consecutive_failures >= max_consecutive_failures: if consecutive_failures >= max_consecutive_failures:
continue_input = self.utils.request_input("\nUnable to retrieve passwords. Continue trying? (Yes/no): ").strip().lower() or "yes" result = show_confirmation("WiFi Profile Extractor", "Unable to retrieve passwords. Continue trying?")
if continue_input != "yes": if not result:
break break
consecutive_failures = 0 consecutive_failures = 0
except Exception as e: except Exception as e:
consecutive_failures += 1 if os_name == "Darwin" else 0 consecutive_failures += 1 if os_name == "Darwin" else 0
print("Error processing network '{}': {}".format(ssid, str(e))) self.utils.log_message("[WIFI PROFILE EXTRACTOR] Error processing network \"{}\": {}".format(ssid, str(e)), level="ERROR", to_build_log=True)
if consecutive_failures >= max_consecutive_failures: if consecutive_failures >= max_consecutive_failures:
continue_input = self.utils.request_input("\nUnable to retrieve passwords. Continue trying? (Yes/no): ").strip().lower() or "yes" result = show_confirmation("WiFi Profile Extractor", "Unable to retrieve passwords. Continue trying?")
if continue_input != "yes": if not result:
break break
consecutive_failures = 0 consecutive_failures = 0
@@ -177,12 +160,11 @@ class WifiProfileExtractor:
processed_count += 1 processed_count += 1
if processed_count >= max_networks and len(networks) < max_networks and processed_count < len(ssid_list): if processed_count >= max_networks and len(networks) < max_networks and processed_count < len(ssid_list):
continue_input = self.utils.request_input("\nOnly retrieved {}/{} networks. Try more to reach your target? (Yes/no): ".format(len(networks), max_networks)).strip().lower() or "yes"
if continue_input != "yes":
break
consecutive_failures = 0 result = show_confirmation("WiFi Profile Extractor", "Only retrieved {}/{} networks. Try more to reach your target?".format(len(networks), max_networks))
if not result:
break
return networks return networks
@@ -201,10 +183,12 @@ class WifiProfileExtractor:
max_networks = self.ask_network_count(len(ssid_list)) max_networks = self.ask_network_count(len(ssid_list))
self.utils.head("Administrator Authentication Required") if self.utils.gui_handler:
print("") content = (
print("To retrieve WiFi passwords from the Keychain, macOS will prompt") "To retrieve WiFi passwords from the Keychain, macOS will prompt<br>"
print("you for administrator credentials for each WiFi network.") "you for administrator credentials for each WiFi network."
)
show_info("Administrator Authentication Required", content)
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_macos) return self.process_networks(ssid_list, max_networks, self.get_wifi_password_macos)
@@ -232,9 +216,7 @@ class WifiProfileExtractor:
max_networks = len(ssid_list) max_networks = len(ssid_list)
self.utils.head("WiFi Profile Extractor") self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving passwords for {} network(s)".format(len(ssid_list)), level="INFO", to_build_log=True)
print("")
print("Retrieving passwords for {} network(s)...".format(len(ssid_list)))
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_windows) return self.process_networks(ssid_list, max_networks, self.get_wifi_password_windows)
@@ -253,9 +235,7 @@ class WifiProfileExtractor:
max_networks = len(ssid_list) max_networks = len(ssid_list)
self.utils.head("WiFi Profile Extractor") self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving passwords for {} network(s)".format(len(ssid_list)), level="INFO", to_build_log=True)
print("")
print("Retrieving passwords for {} network(s)...".format(len(ssid_list)))
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_linux) return self.process_networks(ssid_list, max_networks, self.get_wifi_password_linux)
@@ -286,31 +266,21 @@ class WifiProfileExtractor:
return interfaces return interfaces
def get_profiles(self): def get_profiles(self):
os_name = platform.system() content = (
"<b>Note:</b><br>"
self.utils.head("WiFi Profile Extractor") "<ul>"
print("") "<li>When using itlwm kext, WiFi appears as Ethernet in macOS</li>"
print("\033[1;93mNote:\033[0m") "<li>You'll need Heliport app to manage WiFi connections in macOS</li>"
print("- When using itlwm kext, WiFi appears as Ethernet in macOS") "<li>This step will enable auto WiFi connections at boot time<br>"
print("- You'll need Heliport app to manage WiFi connections in macOS") "and is useful for users installing macOS via Recovery OS</li>"
print("- This step will enable auto WiFi connections at boot time") "</ul><br>"
print(" and is useful for users installing macOS via Recovery OS") "Would you like to scan for WiFi profiles?"
print("") )
if not show_confirmation("WiFi Profile Extractor", content):
return []
while True:
user_input = self.utils.request_input("Would you like to scan for WiFi profiles? (Yes/no): ").strip().lower()
if user_input == "yes":
break
elif user_input == "no":
return []
else:
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
profiles = [] profiles = []
self.utils.head("Detecting WiFi Profiles") self.utils.log_message("[WIFI PROFILE EXTRACTOR] Detecting WiFi Profiles", level="INFO", to_build_log=True)
print("")
print("Scanning for WiFi profiles...")
if os_name == "Windows": if os_name == "Windows":
profiles = self.get_preferred_networks_windows() profiles = self.get_preferred_networks_windows()
@@ -321,31 +291,17 @@ class WifiProfileExtractor:
if wifi_interfaces: if wifi_interfaces:
for interface in wifi_interfaces: for interface in wifi_interfaces:
print("Checking interface: {}".format(interface)) self.utils.log_message("[WIFI PROFILE EXTRACTOR] Checking interface: {}".format(interface), level="INFO", to_build_log=True)
interface_profiles = self.get_preferred_networks_macos(interface) interface_profiles = self.get_preferred_networks_macos(interface)
if interface_profiles: if interface_profiles:
profiles = interface_profiles profiles = interface_profiles
break break
else: else:
print("No WiFi interfaces detected.") self.utils.log_message("[WIFI PROFILE EXTRACTOR] No WiFi interfaces detected.", level="INFO", to_build_log=True)
if not profiles: if not profiles:
self.utils.head("WiFi Profile Extractor") self.utils.log_message("[WIFI PROFILE EXTRACTOR] No WiFi profiles with saved passwords were found.", level="INFO", to_build_log=True)
print("")
print("No WiFi profiles with saved passwords were found.")
self.utils.request_input()
self.utils.head("WiFi Profile Extractor") self.utils.log_message("[WIFI PROFILE EXTRACTOR] Successfully applied {} WiFi profiles".format(len(profiles)), level="INFO", to_build_log=True)
print("")
print("Found the following WiFi profiles with saved passwords:")
print("")
print("Index SSID Password")
print("-------------------------------------------------------")
for index, (ssid, password) in enumerate(profiles, start=1):
print("{:<6} {:<32} {:<8}".format(index, ssid[:31] + "..." if len(ssid) > 31 else ssid, password[:12] + "..." if len(password) > 12 else password))
print("")
print("Successfully applied {} WiFi profiles.".format(len(profiles)))
print("")
self.utils.request_input()
return profiles return profiles

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
certifi
PyQt6
pyqt6-sip
PyQt6-Fluent-Widgets

View File

@@ -1,201 +1,260 @@
import os
import tempfile
import shutil
import sys
from PyQt6.QtCore import QThread, pyqtSignal
from Scripts import resource_fetcher from Scripts import resource_fetcher
from Scripts import github from Scripts import github
from Scripts import run from Scripts import run
from Scripts import utils from Scripts import utils
import os from Scripts import integrity_checker
import tempfile from Scripts.custom_dialogs import show_update_dialog, show_info, show_confirmation
import shutil
class UpdateCheckerThread(QThread):
update_available = pyqtSignal(dict)
check_failed = pyqtSignal(str)
no_update = pyqtSignal()
def __init__(self, updater_instance):
super().__init__()
self.updater = updater_instance
def run(self):
try:
remote_manifest = self.updater.get_remote_manifest()
if not remote_manifest:
self.check_failed.emit("Could not fetch update information from GitHub.\n\nPlease check your internet connection and try again later.")
return
local_manifest = self.updater.get_local_manifest()
if not local_manifest:
self.check_failed.emit("Could not generate local manifest.\n\nPlease try again later.")
return
files_to_update = self.updater.compare_manifests(local_manifest, remote_manifest)
if not files_to_update:
self.no_update.emit()
else:
self.update_available.emit(files_to_update)
except Exception as e:
self.check_failed.emit("An error occurred during update check:\n\n{}".format(str(e)))
class Updater: class Updater:
def __init__(self): def __init__(self, utils_instance=None, github_instance=None, resource_fetcher_instance=None, run_instance=None, integrity_checker_instance=None):
self.github = github.Github() self.utils = utils_instance if utils_instance else utils.Utils()
self.fetcher = resource_fetcher.ResourceFetcher() self.github = github_instance if github_instance else github.Github(utils_instance=self.utils)
self.run = run.Run().run self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher(utils_instance=self.utils)
self.utils = utils.Utils() self.run = run_instance.run if run_instance else run.Run().run
self.sha_version = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sha_version.txt") self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker(utils_instance=self.utils)
self.remote_manifest_url = "https://nightly.link/lzhoang2801/OpCore-Simplify/workflows/generate-manifest/main/manifest.json.zip"
self.download_repo_url = "https://github.com/lzhoang2801/OpCore-Simplify/archive/refs/heads/main.zip" self.download_repo_url = "https://github.com/lzhoang2801/OpCore-Simplify/archive/refs/heads/main.zip"
self.temporary_dir = tempfile.mkdtemp() self.temporary_dir = tempfile.mkdtemp()
self.current_step = 0 self.root_dir = os.path.dirname(os.path.realpath(__file__))
def get_current_sha_version(self): def get_remote_manifest(self, dialog=None):
print("Checking current version...") if dialog:
try: dialog.update_progress(10, "Fetching remote manifest...")
current_sha_version = self.utils.read_file(self.sha_version)
if not current_sha_version:
print("SHA version information is missing.")
return "missing_sha_version"
return current_sha_version.decode()
except Exception as e:
print("Error reading current SHA version: {}".format(str(e)))
return "error_reading_sha_version"
def get_latest_sha_version(self):
print("Fetching latest version from GitHub...")
try:
commits = self.github.get_commits("lzhoang2801", "OpCore-Simplify")
return commits["commitGroups"][0]["commits"][0]["oid"]
except Exception as e:
print("Error fetching latest SHA version: {}".format(str(e)))
return None try:
temp_manifest_zip_path = os.path.join(self.temporary_dir, "remote_manifest.json.zip")
success = self.fetcher.download_and_save_file(self.remote_manifest_url, temp_manifest_zip_path)
if not success or not os.path.exists(temp_manifest_zip_path):
return None
def download_update(self): self.utils.extract_zip_file(temp_manifest_zip_path, self.temporary_dir)
self.current_step += 1
print("") remote_manifest_path = os.path.join(self.temporary_dir, "manifest.json")
print("Step {}: Creating temporary directory...".format(self.current_step)) manifest_data = self.utils.read_file(remote_manifest_path)
if dialog:
dialog.update_progress(20, "Manifest downloaded successfully")
return manifest_data
except Exception as e:
self.utils.log_message("[UPDATER] Error fetching remote manifest: {}".format(str(e)), level="ERROR")
return None
def get_local_manifest(self, dialog=None):
if dialog:
dialog.update_progress(40, "Generating local manifest...")
try:
manifest_data = self.integrity_checker.generate_folder_manifest(self.root_dir, save_manifest=False)
if dialog:
dialog.update_progress(50, "Local manifest generated")
return manifest_data
except Exception as e:
self.utils.log_message("[UPDATER] Error generating local manifest: {}".format(str(e)), level="ERROR")
return None
def compare_manifests(self, local_manifest, remote_manifest):
if not local_manifest or not remote_manifest:
return None
files_to_update = {
"modified": [],
"missing": [],
"new": []
}
local_files = set(local_manifest.keys())
remote_files = set(remote_manifest.keys())
for file_path in local_files & remote_files:
if local_manifest[file_path] != remote_manifest[file_path]:
files_to_update["modified"].append(file_path)
files_to_update["missing"] = list(remote_files - local_files)
files_to_update["new"] = list(local_files - remote_files)
total_changes = len(files_to_update["modified"]) + len(files_to_update["missing"])
return files_to_update if total_changes > 0 else None
def download_update(self, dialog=None):
if dialog:
dialog.update_progress(60, "Creating temporary directory...")
try: try:
self.utils.create_folder(self.temporary_dir) self.utils.create_folder(self.temporary_dir)
print(" Temporary directory created.")
self.current_step += 1 if dialog:
print("Step {}: Downloading update package...".format(self.current_step)) dialog.update_progress(65, "Downloading update package...")
print(" ", end="")
file_path = os.path.join(self.temporary_dir, os.path.basename(self.download_repo_url))
self.fetcher.download_and_save_file(self.download_repo_url, file_path)
if os.path.exists(file_path) and os.path.getsize(file_path) > 0: file_path = os.path.join(self.temporary_dir, "update.zip")
print(" Update package downloaded ({:.1f} KB)".format(os.path.getsize(file_path)/1024)) success = self.fetcher.download_and_save_file(self.download_repo_url, file_path)
self.current_step += 1 if not success or not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
print("Step {}: Extracting files...".format(self.current_step))
self.utils.extract_zip_file(file_path)
print(" Files extracted successfully")
return True
else:
print(" Download failed or file is empty")
return False return False
if dialog:
dialog.update_progress(75, "Extracting files...")
self.utils.extract_zip_file(file_path, self.temporary_dir)
if dialog:
dialog.update_progress(80, "Files extracted successfully")
return True
except Exception as e: except Exception as e:
print(" Error during download/extraction: {}".format(str(e))) self.utils.log_message("[UPDATER] Error during download/extraction: {}".format(str(e)), level="ERROR")
return False return False
def update_files(self): def update_files(self, files_to_update, dialog=None):
self.current_step += 1 if not files_to_update:
print("Step {}: Updating files...".format(self.current_step)) return True
try: try:
target_dir = os.path.join(self.temporary_dir, "OpCore-Simplify-main") target_dir = os.path.join(self.temporary_dir, "OpCore-Simplify-main")
if not os.path.exists(target_dir): if not os.path.exists(target_dir):
target_dir = os.path.join(self.temporary_dir, "main", "OpCore-Simplify-main") self.utils.log_message("[UPDATER] Target directory not found: {}".format(target_dir), level="ERROR")
if not os.path.exists(target_dir):
print(" Could not locate extracted files directory")
return False return False
file_paths = self.utils.find_matching_paths(target_dir, type_filter="file")
total_files = len(file_paths) all_files = files_to_update["modified"] + files_to_update["missing"]
print(" Found {} files to update".format(total_files)) total_files = len(all_files)
if dialog:
dialog.update_progress(85, "Updating {} files...".format(total_files))
updated_count = 0 updated_count = 0
for index, (path, type) in enumerate(file_paths, start=1): for index, relative_path in enumerate(all_files, start=1):
source = os.path.join(target_dir, path) source = os.path.join(target_dir, relative_path)
destination = source.replace(target_dir, os.path.dirname(os.path.realpath(__file__)))
if not os.path.exists(source):
self.utils.log_message("[UPDATER] Source file not found: {}".format(source), level="ERROR")
continue
destination = os.path.join(self.root_dir, relative_path)
self.utils.create_folder(os.path.dirname(destination)) self.utils.create_folder(os.path.dirname(destination))
print(" Updating [{}/{}]: {}".format(index, total_files, os.path.basename(path)), end="\r") self.utils.log_message("[UPDATER] Updating [{}/{}]: {}".format(index, total_files, os.path.basename(relative_path)), level="INFO")
if dialog:
progress = 85 + int((index / total_files) * 10)
dialog.update_progress(progress, "Updating [{}/{}]: {}".format(index, total_files, os.path.basename(relative_path)))
try: try:
shutil.move(source, destination) shutil.move(source, destination)
updated_count += 1 updated_count += 1
if ".command" in os.path.splitext(path)[-1] and os.name != "nt": if ".command" in os.path.splitext(relative_path)[-1] and os.name != "nt":
self.run({ self.run({
"args": ["chmod", "+x", destination] "args": ["chmod", "+x", destination]
}) })
except Exception as e: except Exception as e:
print(" Failed to update {}: {}".format(path, str(e))) self.utils.log_message("[UPDATER] Failed to update {}: {}".format(relative_path, str(e)), level="ERROR")
print("") if dialog:
print(" Successfully updated {}/{} files".format(updated_count, total_files)) dialog.update_progress(95, "Successfully updated {}/{} files".format(updated_count, total_files))
self.current_step += 1 if os.path.exists(self.temporary_dir):
print("Step {}: Cleaning up temporary files...".format(self.current_step)) shutil.rmtree(self.temporary_dir)
shutil.rmtree(self.temporary_dir)
print(" Cleanup complete") if dialog:
dialog.update_progress(100, "Update completed!")
return True return True
except Exception as e: except Exception as e:
print(" Error during file update: {}".format(str(e))) self.utils.log_message("[UPDATER] Error during file update: {}".format(str(e)), level="ERROR")
return False return False
def save_latest_sha_version(self, latest_sha): def run_update(self):
try: checker_thread = UpdateCheckerThread(self)
self.utils.write_file(self.sha_version, latest_sha.encode())
self.current_step += 1
print("Step {}: Version information updated.".format(self.current_step))
return True
except Exception as e:
print("Failed to save version information: {}".format(str(e)))
return False
def run_update(self):
self.utils.head("Check for Updates")
print("")
current_sha_version = self.get_current_sha_version() def on_update_available(files_to_update):
latest_sha_version = self.get_latest_sha_version() checker_thread.quit()
checker_thread.wait()
print("")
if latest_sha_version is None:
print("Could not verify the latest version from GitHub.")
print("Current script SHA version: {}".format(current_sha_version))
print("Please check your internet connection and try again later.")
print("")
while True: if not show_confirmation("An update is available!", "Would you like to update now?", yes_text="Update", no_text="Later"):
user_input = self.utils.request_input("Do you want to skip the update process? (yes/No): ").strip().lower() return False
if user_input == "yes":
print("")
print("Update process skipped.")
return False
elif user_input == "no":
print("")
print("Continuing with update using default version check...")
latest_sha_version = "update_forced_by_user"
break
else:
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
else:
print("Current script SHA version: {}".format(current_sha_version))
print("Latest script SHA version: {}".format(latest_sha_version))
print("")
if latest_sha_version != current_sha_version:
print("Update available!")
print("Updating from version {} to {}".format(current_sha_version, latest_sha_version))
print("")
print("Starting update process...")
if not self.download_update(): dialog = show_update_dialog("Updating", "Starting update process...")
print("") dialog.show()
print(" Update failed: Could not download or extract update package")
try:
if not self.download_update(dialog):
dialog.close()
show_info("Update Failed", "Could not download or extract update package.\n\nPlease check your internet connection and try again.")
return
if not self.update_files(files_to_update, dialog):
dialog.close()
show_info("Update Failed", "Could not update files.\n\nPlease try again later.")
return
dialog.close()
show_info("Update Complete", "Update completed successfully!\n\nThe program needs to restart to complete the update process.")
os.execv(sys.executable, ["python3"] + sys.argv)
except Exception as e:
dialog.close()
self.utils.log_message("[UPDATER] Error during update: {}".format(str(e)), level="ERROR")
show_info("Update Error", "An error occurred during the update process:\n\n{}".format(str(e)))
finally:
if os.path.exists(self.temporary_dir): if os.path.exists(self.temporary_dir):
self.current_step += 1 try:
print("Step {}: Cleaning up temporary files...".format(self.current_step)) shutil.rmtree(self.temporary_dir)
shutil.rmtree(self.temporary_dir) except:
print(" Cleanup complete") pass
return False def on_check_failed(error_message):
checker_thread.quit()
if not self.update_files(): checker_thread.wait()
print("") show_info("Update Check Failed", error_message)
print(" Update failed: Could not update files")
return False def on_no_update():
checker_thread.quit()
if not self.save_latest_sha_version(latest_sha_version): checker_thread.wait()
print("")
print(" Update completed but version information could not be saved") checker_thread.update_available.connect(on_update_available)
checker_thread.check_failed.connect(on_check_failed)
print("") checker_thread.no_update.connect(on_no_update)
print("Update completed successfully!")
print("") checker_thread.start()
print("The program needs to restart to complete the update process.")
return True
else:
print("You are already using the latest version")
return False