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.
" "2. OpenCore Legacy Patcher needs SIP disabled for applying custom kernel patches, which can cause instability, security risks and update issues.
" "3. OpenCore Legacy Patcher does not officially support the Hackintosh community.

" "Support for macOS Tahoe 26:
" "To patch macOS Tahoe 26, you must download OpenCore-Patcher 3.0.0 or newer from my repository: lzhoang2801/OpenCore-Legacy-Patcher.
" "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
" "2. Add created UTBMap.kext into the EFI{path_sep}OC{path_sep}Kexts folder
" "3. Remove UTBDefault.kext from the EFI{path_sep}OC{path_sep}Kexts folder
" "4. Edit config.plist using ProperTree:
" " a. Run OC Snapshot (Command/Ctrl + R)
" " b. Enable XhciPortLimit quirk if you have more than 15 ports per controller
" " 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)