mirror of
https://github.com/outbackdingo/OpCore-Simplify.git
synced 2026-01-27 02:19:41 +00:00
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:
57
.github/workflows/generate-manifest.yml
vendored
Normal file
57
.github/workflows/generate-manifest.yml
vendored
Normal 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
|
||||
@@ -299,9 +299,56 @@ if /i "!just_installing!" == "TRUE" (
|
||||
)
|
||||
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
|
||||
REM Python found
|
||||
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=!args:"=!"
|
||||
if "!args!"=="" (
|
||||
|
||||
@@ -283,6 +283,42 @@ prompt_and_download() {
|
||||
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() {
|
||||
local python= version=
|
||||
# Verify our target exists
|
||||
@@ -310,6 +346,11 @@ main() {
|
||||
prompt_and_download
|
||||
return 1
|
||||
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
|
||||
"$python" "$dir/$target" "${args[@]}"
|
||||
}
|
||||
|
||||
@@ -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 sys
|
||||
import re
|
||||
import shutil
|
||||
import platform
|
||||
import traceback
|
||||
import time
|
||||
|
||||
class OCPE:
|
||||
def __init__(self):
|
||||
self.u = utils.Utils("OpCore Simplify")
|
||||
self.u.clean_temporary_dir()
|
||||
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()
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QFont
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from qfluentwidgets import FluentWindow, NavigationItemPosition, FluentIcon, InfoBar, InfoBarPosition
|
||||
|
||||
def select_hardware_report(self):
|
||||
self.ac.dsdt = self.ac.acpi.acpi_tables = None
|
||||
from Scripts.datasets import os_data
|
||||
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:
|
||||
self.u.head("Select hardware report")
|
||||
print("")
|
||||
if os.name == "nt":
|
||||
print("\033[1;93mNote:\033[0m")
|
||||
print("- Ensure you are using the latest version of Hardware Sniffer before generating the hardware report.")
|
||||
print("- Hardware Sniffer will not collect information related to Resizable BAR option of GPU (disabled by default) and monitor connections in Windows PE.")
|
||||
print("")
|
||||
print("E. Export hardware report (Recommended)")
|
||||
print("")
|
||||
print("Q. Quit")
|
||||
print("")
|
||||
WINDOW_MIN_SIZE = (1000, 700)
|
||||
WINDOW_DEFAULT_SIZE = (1200, 800)
|
||||
|
||||
|
||||
class OCS(FluentWindow):
|
||||
open_result_folder_signal = pyqtSignal(str)
|
||||
|
||||
PLATFORM_FONTS = {
|
||||
"Windows": "Segoe UI",
|
||||
"Darwin": "SF Pro Display",
|
||||
"Linux": "Ubuntu"
|
||||
}
|
||||
|
||||
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 ""))
|
||||
if user_input.lower() == "q":
|
||||
self.u.exit_program()
|
||||
if user_input.lower() == "e":
|
||||
hardware_sniffer = self.o.gather_hardware_sniffer()
|
||||
self._init_state()
|
||||
self._setup_window()
|
||||
self._connect_signals()
|
||||
self._setup_backend_handlers()
|
||||
self.init_navigation()
|
||||
|
||||
if not hardware_sniffer:
|
||||
continue
|
||||
def _init_state(self):
|
||||
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")
|
||||
print("")
|
||||
print("Exporting hardware report to {}...".format(report_dir))
|
||||
|
||||
output = self.r.run({
|
||||
"args":[hardware_sniffer, "-e", "-o", report_dir]
|
||||
})
|
||||
|
||||
if output[-1] != 0:
|
||||
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."
|
||||
|
||||
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)
|
||||
font = QFont()
|
||||
system = platform.system()
|
||||
font_family = self.PLATFORM_FONTS.get(system, "Ubuntu")
|
||||
font.setFamily(font_family)
|
||||
font.setStyleHint(QFont.StyleHint.SansSerif)
|
||||
self.setFont(font)
|
||||
|
||||
def _restore_window_geometry(self):
|
||||
saved_geometry = self.settings.get("window_geometry")
|
||||
|
||||
if saved_geometry and isinstance(saved_geometry, dict):
|
||||
x = saved_geometry.get("x")
|
||||
y = saved_geometry.get("y")
|
||||
width = saved_geometry.get("width", WINDOW_DEFAULT_SIZE[0])
|
||||
height = saved_geometry.get("height", WINDOW_DEFAULT_SIZE[1])
|
||||
|
||||
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)
|
||||
if not is_valid or errors:
|
||||
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
|
||||
x = screen_geometry.left() + (screen_geometry.width() - window_width) // 2
|
||||
y = screen_geometry.top() + (screen_geometry.height() - window_height) // 2
|
||||
|
||||
def show_oclp_warning(self):
|
||||
while True:
|
||||
self.u.head("OpenCore Legacy Patcher Warning")
|
||||
print("")
|
||||
print("1. OpenCore Legacy Patcher is the only solution to enable dropped GPU and Broadcom WiFi")
|
||||
print(" support in newer macOS versions, as well as to bring back AppleHDA for macOS Tahoe 26.")
|
||||
print("")
|
||||
print("2. OpenCore Legacy Patcher disables macOS security features including SIP and AMFI, which may")
|
||||
print(" lead to issues such as requiring full installers for updates, application crashes, and")
|
||||
print(" system instability.")
|
||||
print("")
|
||||
print("3. OpenCore Legacy Patcher is not officially supported for Hackintosh community.")
|
||||
print("")
|
||||
print("\033[1;91mImportant:\033[0m")
|
||||
print("Please consider these risks carefully before proceeding.")
|
||||
print("")
|
||||
print("\033[1;96mSupport for macOS Tahoe 26:\033[0m")
|
||||
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.")
|
||||
print("Older or official Dortania releases are NOT supported for Tahoe 26.")
|
||||
print("")
|
||||
option = self.u.request_input("Do you want to continue with OpenCore Legacy Patcher? (yes/No): ").strip().lower()
|
||||
if option == "yes":
|
||||
return True
|
||||
elif option == "no":
|
||||
self.setGeometry(x, y, window_width, window_height)
|
||||
else:
|
||||
self.resize(*WINDOW_DEFAULT_SIZE)
|
||||
|
||||
def _save_window_geometry(self):
|
||||
geometry = self.geometry()
|
||||
window_geometry = {
|
||||
"x": geometry.x(),
|
||||
"y": geometry.y(),
|
||||
"width": geometry.width(),
|
||||
"height": geometry.height()
|
||||
}
|
||||
self.settings.set("window_geometry", window_geometry)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._save_window_geometry()
|
||||
super().closeEvent(event)
|
||||
|
||||
def _connect_signals(self):
|
||||
self.backend.log_message_signal.connect(
|
||||
lambda message, level, to_build_log: (
|
||||
[
|
||||
self.build_log.append(line)
|
||||
for line in (message.splitlines() or [""])
|
||||
]
|
||||
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
|
||||
|
||||
def select_macos_version(self, hardware_report, native_macos_version, ocl_patched_macos_version):
|
||||
suggested_macos_version = native_macos_version[1]
|
||||
version_pattern = re.compile(r'^(\d+)(?:\.(\d+)(?:\.(\d+))?)?$')
|
||||
|
||||
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.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"
|
||||
]
|
||||
if require_customized_hardware:
|
||||
if not self.hardware_state.customized_hardware:
|
||||
if show_status:
|
||||
self.update_status("Please reload hardware report and select target macOS version to continue", "WARNING")
|
||||
return False
|
||||
|
||||
title = "Building OpenCore EFI"
|
||||
return True
|
||||
|
||||
self.u.progress_bar(title, steps, 0)
|
||||
self.u.create_folder(self.result_dir, remove_content=True)
|
||||
def apply_macos_version(self, version):
|
||||
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):
|
||||
raise Exception("Directory '{}' does not exist.".format(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)
|
||||
|
||||
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")
|
||||
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)
|
||||
self.backend.ac.select_acpi_patches(self.hardware_state.customized_hardware, self.hardware_state.disabled_devices)
|
||||
|
||||
if not config_data:
|
||||
raise Exception("Error: The file {} does not exist.".format(config_file))
|
||||
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)
|
||||
|
||||
self.u.progress_bar(title, steps, 1)
|
||||
config_data["ACPI"]["Add"] = []
|
||||
config_data["ACPI"]["Delete"] = []
|
||||
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()
|
||||
if audio_layout_id is not None:
|
||||
self.hardware_state.audio_layout_id = audio_layout_id
|
||||
self.hardware_state.audio_controller_properties = audio_controller_properties
|
||||
|
||||
for patch in self.ac.patches:
|
||||
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
|
||||
self.backend.s.smbios_specific_options(self.hardware_state.customized_hardware, self.smbios_state.model_name, version, self.backend.ac.patches, self.backend.k)
|
||||
|
||||
acpi_load = getattr(self.ac, patch.function_name)()
|
||||
self.configurationPage.update_display()
|
||||
|
||||
if not isinstance(acpi_load, dict):
|
||||
continue
|
||||
def setup_exception_hook(self):
|
||||
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", []))
|
||||
config_data["ACPI"]["Delete"].extend(acpi_load.get("Delete", []))
|
||||
config_data["ACPI"]["Patch"].extend(acpi_load.get("Patch", []))
|
||||
|
||||
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:
|
||||
error_details = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
|
||||
error_message = "Uncaught exception detected:\n{}".format(error_details)
|
||||
|
||||
self.backend.u.log_message(error_message, level="ERROR")
|
||||
|
||||
try:
|
||||
if os.path.isdir(file_path):
|
||||
shutil.rmtree(file_path)
|
||||
else:
|
||||
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))")
|
||||
sys.__stderr__.write("\n[CRITICAL ERROR] {}\n".format(error_message))
|
||||
except:
|
||||
pass
|
||||
|
||||
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
|
||||
sys.excepthook = handle_exception
|
||||
|
||||
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):
|
||||
hardware_report_path = None
|
||||
native_macos_version = None
|
||||
disabled_devices = None
|
||||
macos_version = None
|
||||
ocl_patched_macos_version = None
|
||||
needs_oclp = False
|
||||
smbios_model = None
|
||||
|
||||
while True:
|
||||
self.u.head()
|
||||
print("")
|
||||
print(" Hardware Report: {}".format(hardware_report_path or 'Not selected'))
|
||||
if hardware_report_path:
|
||||
print("")
|
||||
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 ''))
|
||||
print(" SMBIOS: {}".format(smbios_model or 'Not selected'))
|
||||
if disabled_devices:
|
||||
print(" Disabled Devices:")
|
||||
for device, _ in disabled_devices.items():
|
||||
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()
|
||||
if __name__ == "__main__":
|
||||
backend = Backend()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_default_gui_handler(app)
|
||||
|
||||
window = OCS(backend)
|
||||
window.setup_exception_hook()
|
||||
window.show()
|
||||
|
||||
if backend.settings.get_auto_update_check():
|
||||
updater.Updater(
|
||||
utils_instance=backend.u,
|
||||
github_instance=backend.github,
|
||||
resource_fetcher_instance=backend.resource_fetcher,
|
||||
run_instance=backend.r,
|
||||
integrity_checker_instance=backend.integrity_checker
|
||||
).run_update()
|
||||
|
||||
sys.exit(app.exec())
|
||||
47
README.md
47
README.md
@@ -19,23 +19,6 @@
|
||||
</p>
|
||||
</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**
|
||||
|
||||
1. **Comprehensive Hardware and macOS Support**
|
||||
@@ -101,39 +84,30 @@
|
||||
- On **macOS**, run `OpCore-Simplify.command`.
|
||||
- On **Linux**, run `OpCore-Simplify.py` with existing Python interpreter.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
4. **Verifying hardware compatibility**:
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
- OpCore Simplify will automatically apply essential ACPI patches and kexts.
|
||||
- You can manually review and customize these settings as needed.
|
||||
|
||||

|
||||

|
||||
|
||||
5. **Building OpenCore EFI**:
|
||||
6. **Building OpenCore 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.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
6. **USB Mapping**:
|
||||
- After building your EFI, follow the steps for mapping USB ports.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
@@ -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.
|
||||
- [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**
|
||||
|
||||
@@ -168,4 +143,4 @@ Distributed under the BSD 3-Clause License. See `LICENSE` for more information.
|
||||
|
||||
## 🌟 **Star History**
|
||||
|
||||
[](https://star-history.com/#lzhoang2801/OpCore-Simplify&Date)
|
||||
[](https://star-history.com/#lzhoang2801/OpCore-Simplify&Date)
|
||||
@@ -8,6 +8,7 @@ from Scripts import smbios
|
||||
from Scripts import dsdt
|
||||
from Scripts import run
|
||||
from Scripts import utils
|
||||
from Scripts.custom_dialogs import show_checklist_dialog
|
||||
import os
|
||||
import binascii
|
||||
import re
|
||||
@@ -17,11 +18,11 @@ import sys
|
||||
import plistlib
|
||||
|
||||
class ACPIGuru:
|
||||
def __init__(self):
|
||||
self.acpi = dsdt.DSDT()
|
||||
self.smbios = smbios.SMBIOS()
|
||||
self.run = run.Run().run
|
||||
self.utils = utils.Utils()
|
||||
def __init__(self, dsdt_instance=None, smbios_instance=None, run_instance=None, utils_instance=None):
|
||||
self.acpi = dsdt_instance if dsdt_instance else dsdt.DSDT()
|
||||
self.smbios = smbios_instance if smbios_instance else smbios.SMBIOS()
|
||||
self.run = run_instance.run if run_instance else run.Run().run
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
self.patches = acpi_patch_data.patches
|
||||
self.hardware_report = None
|
||||
self.disabled_devices = None
|
||||
@@ -118,9 +119,7 @@ class ACPIGuru:
|
||||
def read_acpi_tables(self, path):
|
||||
if not path:
|
||||
return
|
||||
self.utils.head("Loading ACPI Table(s)")
|
||||
print("by CorpNewt")
|
||||
print("")
|
||||
self.utils.log_message("[ACPI GURU] Loading ACPI Table(s) from {}".format(path), level="INFO")
|
||||
tables = []
|
||||
trouble_dsdt = None
|
||||
fixed = False
|
||||
@@ -129,10 +128,10 @@ class ACPIGuru:
|
||||
# Clear any existing tables so we load anew
|
||||
self.acpi.acpi_tables = {}
|
||||
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)):
|
||||
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)
|
||||
if not tables:
|
||||
# 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")):
|
||||
# Rerun this function with that updated path
|
||||
return self.read_acpi_tables(os.path.join(path,"ACPI"))
|
||||
print(" - No valid .aml files were found!")
|
||||
print("")
|
||||
self.utils.log_message("[ACPI GURU] No valid .aml files were found!", level="ERROR")
|
||||
#self.u.grab("Press [enter] to return...")
|
||||
self.utils.request_input()
|
||||
# Restore any prior tables
|
||||
self.acpi.acpi_tables = prior_tables
|
||||
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
|
||||
# and try to load that as-is. If it doesn't load, we'll have to
|
||||
# manage everything with temp folders
|
||||
dsdt_list = [x for x in tables if self.acpi._table_signature(path,x) == "DSDT"]
|
||||
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):
|
||||
print(" - {}".format(d))
|
||||
print("\nOnly one is allowed at a time. Please remove one of the above and try again.")
|
||||
print("")
|
||||
self.utils.log_message("[ACPI GURU] Found DSDT file: {}".format(d), level="INFO")
|
||||
self.utils.log_message("[ACPI GURU] Only one is allowed at a time. Please remove one of the above and try again.", level="ERROR")
|
||||
#self.u.grab("Press [enter] to return...")
|
||||
self.utils.request_input()
|
||||
# Restore any prior tables
|
||||
self.acpi.acpi_tables = prior_tables
|
||||
return
|
||||
# Get the DSDT, if any
|
||||
dsdt = dsdt_list[0] if len(dsdt_list) else None
|
||||
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]:
|
||||
trouble_dsdt = dsdt
|
||||
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):
|
||||
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]:
|
||||
print("\nDone.")
|
||||
self.utils.log_message("[ACPI GURU] Done.", level="INFO")
|
||||
# If it loads fine - just return the path
|
||||
# to the parent directory
|
||||
return os.path.dirname(path)
|
||||
if not self.acpi._table_signature(path) == "DSDT":
|
||||
# Not a DSDT, we aren't applying pre-patches
|
||||
print("\n{} could not be disassembled!".format(os.path.basename(path)))
|
||||
print("")
|
||||
self.utils.log_message("[ACPI GURU] {} could not be disassembled!".format(os.path.basename(path)), level="ERROR")
|
||||
#self.u.grab("Press [enter] to return...")
|
||||
self.utils.request_input()
|
||||
# Restore any prior tables
|
||||
self.acpi.acpi_tables = prior_tables
|
||||
return
|
||||
@@ -194,10 +187,8 @@ class ACPIGuru:
|
||||
tables.append(os.path.basename(path))
|
||||
path = os.path.dirname(path)
|
||||
else:
|
||||
print("Passed file/folder does not exist!")
|
||||
print("")
|
||||
self.utils.log_message("[ACPI GURU] Passed file/folder does not exist!", level="ERROR")
|
||||
#self.u.grab("Press [enter] to return...")
|
||||
self.utils.request_input()
|
||||
# Restore any prior tables
|
||||
self.acpi.acpi_tables = prior_tables
|
||||
return
|
||||
@@ -214,22 +205,22 @@ class ACPIGuru:
|
||||
# Get a reference to the new trouble file
|
||||
trouble_path = os.path.join(temp,trouble_dsdt)
|
||||
# Now we try patching it
|
||||
print("Checking available pre-patches...")
|
||||
print("Loading {} into memory...".format(trouble_dsdt))
|
||||
self.utils.log_message("[ACPI GURU] Checking available pre-patches...", level="INFO")
|
||||
self.utils.log_message("[ACPI GURU] Loading {} into memory...".format(trouble_dsdt), level="INFO")
|
||||
with open(trouble_path,"rb") as f:
|
||||
d = f.read()
|
||||
res = self.acpi.check_output(path)
|
||||
target_name = self.get_unique_name(trouble_dsdt,res,name_append="-Patched")
|
||||
self.dsdt_patches = []
|
||||
print("Iterating patches...\n")
|
||||
self.utils.log_message("[ACPI GURU] Iterating patches...", level="INFO")
|
||||
for p in self.pre_patches:
|
||||
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"])
|
||||
if d.count(find) == 1:
|
||||
self.dsdt_patches.append(p) # Retain the patch
|
||||
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
|
||||
with open(trouble_path,"wb") as f:
|
||||
f.write(d) # Write the updated file
|
||||
@@ -237,7 +228,7 @@ class ACPIGuru:
|
||||
if self.acpi.load(trouble_path)[0]:
|
||||
fixed = True
|
||||
# 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)
|
||||
# Save to the local file
|
||||
#with open(os.path.join(res,target_name),"wb") as f:
|
||||
@@ -246,10 +237,8 @@ class ACPIGuru:
|
||||
#self.patch_warn()
|
||||
break
|
||||
if not fixed:
|
||||
print("\n{} could not be disassembled!".format(trouble_dsdt))
|
||||
print("")
|
||||
self.utils.log_message("[ACPI GURU] {} could not be disassembled!".format(trouble_dsdt), level="ERROR")
|
||||
#self.u.grab("Press [enter] to return...")
|
||||
self.utils.request_input()
|
||||
if temp:
|
||||
shutil.rmtree(temp,ignore_errors=True)
|
||||
# Restore any prior tables
|
||||
@@ -257,26 +246,26 @@ class ACPIGuru:
|
||||
return
|
||||
# Let's load the rest of the tables
|
||||
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)
|
||||
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,
|
||||
":" if failed else ""
|
||||
))
|
||||
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
|
||||
if not loaded_tables:
|
||||
self.acpi.acpi_tables = prior_tables
|
||||
else:
|
||||
if len(tables) > 1:
|
||||
print("") # Newline for readability
|
||||
print("Done.")
|
||||
#if len(tables) > 1:
|
||||
# print("") # Newline for readability
|
||||
self.utils.log_message("[ACPI GURU] Done.", level="INFO")
|
||||
# If we had to patch the DSDT, or if not all tables loaded,
|
||||
# make sure we get interaction from the user to continue
|
||||
if trouble_dsdt or not loaded_tables or failed:
|
||||
print("")
|
||||
pass
|
||||
#self.u.grab("Press [enter] to return...")
|
||||
#self.utils.request_input()
|
||||
if temp:
|
||||
@@ -293,7 +282,7 @@ class ACPIGuru:
|
||||
# Got it already
|
||||
return True
|
||||
# Need to prompt
|
||||
self.select_acpi_tables()
|
||||
#self.select_acpi_tables()
|
||||
self.dsdt = self.acpi.get_dsdt_or_only()
|
||||
if self._ensure_dsdt(allow_any=allow_any):
|
||||
return True
|
||||
@@ -3214,20 +3203,6 @@ DefinitionBlock ("", "SSDT", 2, "ZPSS", "WMIS", 0x00000000)
|
||||
"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):
|
||||
for index, patch in enumerate(self.patches):
|
||||
if patch.name == name:
|
||||
@@ -3235,6 +3210,7 @@ DefinitionBlock ("", "SSDT", 2, "ZPSS", "WMIS", 0x00000000)
|
||||
return None
|
||||
|
||||
def select_acpi_patches(self, hardware_report, disabled_devices):
|
||||
self.utils.log_message("[ACPI GURU] Selecting ACPI patches...", level="INFO")
|
||||
selected_patches = []
|
||||
|
||||
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:
|
||||
selected_patches.append("WMIS")
|
||||
|
||||
self.utils.log_message("[ACPI GURU] Selected patches: {}".format(", ".join(selected_patches)), level="INFO")
|
||||
for patch in self.patches:
|
||||
patch.checked = patch.name in selected_patches
|
||||
|
||||
def customize_patch_selection(self):
|
||||
while True:
|
||||
contents = []
|
||||
contents.append("")
|
||||
contents.append("List of available patches:")
|
||||
contents.append("")
|
||||
for index, kext in enumerate(self.patches, start=1):
|
||||
checkbox = "[*]" if kext.checked else "[ ]"
|
||||
items = []
|
||||
checked_indices = []
|
||||
|
||||
for i, patch in enumerate(self.patches):
|
||||
label = f"{patch.name} - {patch.description}"
|
||||
items.append(label)
|
||||
if patch.checked:
|
||||
checked_indices.append(i)
|
||||
|
||||
line = "{} {:2}. {:15} - {:60}".format(checkbox, index, kext.name, kext.description)
|
||||
if kext.checked:
|
||||
line = "\033[1;32m{}\033[0m".format(line)
|
||||
contents.append(line)
|
||||
contents.append("")
|
||||
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
|
||||
result = show_checklist_dialog("Configure ACPI Patches", "Select ACPI patches you want to apply:", items, checked_indices)
|
||||
|
||||
if result is not None:
|
||||
for i, patch in enumerate(self.patches):
|
||||
patch.checked = i in result
|
||||
140
Scripts/backend.py
Normal file
140
Scripts/backend.py
Normal 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))
|
||||
@@ -3,39 +3,14 @@ from Scripts.datasets import os_data
|
||||
from Scripts.datasets import pci_data
|
||||
from Scripts.datasets import codec_layouts
|
||||
from Scripts import utils
|
||||
import time
|
||||
from Scripts import settings
|
||||
|
||||
class CompatibilityChecker:
|
||||
def __init__(self):
|
||||
self.utils = utils.Utils()
|
||||
def __init__(self, utils_instance=None, settings_instance=None):
|
||||
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):
|
||||
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)
|
||||
|
||||
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:
|
||||
print("")
|
||||
print("Missing required SSE4.x instruction set.")
|
||||
print("Your CPU is not supported by macOS versions newer than Sierra (10.12).")
|
||||
print("")
|
||||
self.utils.request_input()
|
||||
self.utils.exit_program()
|
||||
self.error_codes.append("ERROR_MISSING_SSE4")
|
||||
return
|
||||
|
||||
self.max_native_macos_version = max_version
|
||||
self.min_native_macos_version = min_version
|
||||
|
||||
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():
|
||||
gpu_manufacturer = gpu_props.get("Manufacturer")
|
||||
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):
|
||||
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
|
||||
|
||||
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])
|
||||
|
||||
if max_supported_gpu_version == min_supported_gpu_version and max_supported_gpu_version == None:
|
||||
print("")
|
||||
print("You cannot install macOS without a supported GPU.")
|
||||
print("Please do NOT spam my inbox or issue tracker about this issue anymore!")
|
||||
print("")
|
||||
self.utils.request_input()
|
||||
self.utils.exit_program()
|
||||
self.error_codes.append("ERROR_NO_COMPATIBLE_GPU")
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
max_version = min_version = None
|
||||
@@ -213,19 +154,9 @@ class CompatibilityChecker:
|
||||
|
||||
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):
|
||||
print(" \033[1;93mNote:\033[0m Biometric authentication in macOS requires Apple T2 Chip,")
|
||||
print(" which is not available for Hackintosh systems.")
|
||||
print("")
|
||||
for biometric_device, biometric_props in self.hardware_report.get("Biometric", {}).items():
|
||||
for _, biometric_props in self.hardware_report.get("Biometric", {}).items():
|
||||
biometric_props["Compatibility"] = (None, None)
|
||||
print("{}- {}: {}".format(" "*3, biometric_device, self.show_macos_compatibility(biometric_props.get("Compatibility"))))
|
||||
|
||||
def check_network_compatibility(self):
|
||||
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"):
|
||||
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):
|
||||
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():
|
||||
if controller_props.get("Bus Type") != "PCI":
|
||||
continue
|
||||
@@ -301,28 +208,17 @@ class CompatibilityChecker:
|
||||
min_version = os_data.get_lowest_darwin_version()
|
||||
|
||||
if device_id in pci_data.IntelVMDIDs:
|
||||
print("")
|
||||
print("Intel VMD controllers are not supported in macOS.")
|
||||
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()
|
||||
self.error_codes.append("ERROR_INTEL_VMD")
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
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()):
|
||||
print("")
|
||||
print("No compatible storage controller for macOS was found!")
|
||||
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()
|
||||
self.error_codes.append("ERROR_NO_COMPATIBLE_STORAGE")
|
||||
return
|
||||
|
||||
def check_bluetooth_compatibility(self):
|
||||
for bluetooth_name, bluetooth_props in self.hardware_report.get("Bluetooth", {}).items():
|
||||
@@ -339,8 +235,6 @@ class CompatibilityChecker:
|
||||
max_version = min_version = None
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
print("{}- {}: {}".format(" "*3, controller_name, self.show_macos_compatibility(controller_props.get("Compatibility"))))
|
||||
|
||||
def check_compatibility(self, hardware_report):
|
||||
self.hardware_report = hardware_report
|
||||
self.ocl_patched_macos_version = None
|
||||
self.error_codes = []
|
||||
|
||||
self.utils.head("Compatibility Checker")
|
||||
print("")
|
||||
print("Checking compatibility with macOS for the following devices:")
|
||||
print("")
|
||||
self.utils.log_message("[COMPATIBILITY CHECKER] Starting compatibility check...", level="INFO")
|
||||
|
||||
steps = [
|
||||
('CPU', self.check_cpu_compatibility),
|
||||
@@ -379,15 +269,13 @@ class CompatibilityChecker:
|
||||
('SD Controller', self.check_sd_controller_compatibility)
|
||||
]
|
||||
|
||||
index = 0
|
||||
for device_type, function in steps:
|
||||
if self.hardware_report.get(device_type):
|
||||
index += 1
|
||||
print("{}. {}:".format(index, device_type))
|
||||
time.sleep(0.25)
|
||||
function()
|
||||
|
||||
print("")
|
||||
self.utils.request_input()
|
||||
if self.error_codes:
|
||||
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
|
||||
@@ -1,3 +1,5 @@
|
||||
import copy
|
||||
import os
|
||||
from Scripts.datasets import chipset_data
|
||||
from Scripts.datasets import cpu_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 smbios
|
||||
from Scripts import utils
|
||||
from Scripts.custom_dialogs import show_options_dialog
|
||||
import random
|
||||
|
||||
class ConfigProdigy:
|
||||
def __init__(self):
|
||||
self.g = gathering_files.gatheringFiles()
|
||||
self.smbios = smbios.SMBIOS()
|
||||
self.utils = utils.Utils()
|
||||
def __init__(self, gathering_files_instance=None, smbios_instance=None, utils_instance=None):
|
||||
self.g = gathering_files_instance if gathering_files_instance else gathering_files.gatheringFiles()
|
||||
self.smbios = smbios_instance if smbios_instance else smbios.SMBIOS()
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
self.cpuids = {
|
||||
"Ivy Bridge": "A9060300",
|
||||
"Haswell": "C3060300",
|
||||
@@ -237,76 +240,7 @@ class ConfigProdigy:
|
||||
|
||||
return dict(sorted(igpu_properties.items(), key=lambda item: item[0]))
|
||||
|
||||
def select_audio_codec_layout(self, hardware_report, config=None, controller_required=False):
|
||||
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):
|
||||
def deviceproperties(self, hardware_report, disabled_devices, macos_version, kexts, audio_layout_id=None, audio_controller_properties=None):
|
||||
deviceproperties_add = {}
|
||||
|
||||
def add_device_property(pci_path, properties):
|
||||
@@ -349,11 +283,8 @@ class ConfigProdigy:
|
||||
"model": gpu_name
|
||||
})
|
||||
|
||||
if kexts[kext_data.kext_index_by_name.get("AppleALC")].checked:
|
||||
selected_layout_id, audio_controller_properties = self.select_audio_codec_layout(hardware_report, controller_required=True)
|
||||
|
||||
if selected_layout_id and audio_controller_properties:
|
||||
add_device_property(audio_controller_properties.get("PCI Path"), {"layout-id": selected_layout_id})
|
||||
if audio_layout_id is not None and audio_controller_properties is not None:
|
||||
add_device_property(audio_controller_properties.get("PCI Path"), {"layout-id": audio_layout_id})
|
||||
|
||||
for network_name, network_props in hardware_report.get("Network", {}).items():
|
||||
device_id = network_props.get("Device ID")
|
||||
@@ -502,7 +433,7 @@ class ConfigProdigy:
|
||||
|
||||
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 = [
|
||||
"-v",
|
||||
"debug=0x100",
|
||||
@@ -566,10 +497,8 @@ class ConfigProdigy:
|
||||
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"))
|
||||
elif kext.name == "AppleALC":
|
||||
selected_layout_id, _ = self.select_audio_codec_layout(hardware_report, config)
|
||||
|
||||
if selected_layout_id:
|
||||
boot_args.append("alcid={}".format(selected_layout_id))
|
||||
if audio_layout_id is not None and audio_controller_properties is None:
|
||||
boot_args.append("alcid={}".format(audio_layout_id))
|
||||
elif kext.name == "VoodooI2C":
|
||||
boot_args.append("-vi2c-force-polling")
|
||||
elif kext.name == "CpuTopologyRebuild":
|
||||
@@ -611,7 +540,7 @@ class ConfigProdigy:
|
||||
|
||||
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 - 2"]
|
||||
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"]["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)
|
||||
spoof_cpuid = self.spoof_cpuid(
|
||||
@@ -685,7 +614,7 @@ class ConfigProdigy:
|
||||
config["Misc"]["Tools"] = []
|
||||
|
||||
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"]["prev-lang:kbd"] = self.utils.hex_to_bytes("")
|
||||
|
||||
|
||||
461
Scripts/custom_dialogs.py
Normal file
461
Scripts/custom_dialogs.py
Normal 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
|
||||
47
Scripts/datasets/config_tooltips.py
Normal file
47
Scripts/datasets/config_tooltips.py
Normal 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
|
||||
@@ -1,3 +1,9 @@
|
||||
from Scripts.settings import Settings
|
||||
|
||||
settings = Settings()
|
||||
|
||||
INCLUDE_BETA = settings.get_include_beta_versions()
|
||||
|
||||
class macOSVersionInfo:
|
||||
def __init__(self, name, macos_version, release_status = "final"):
|
||||
self.name = name
|
||||
@@ -17,7 +23,7 @@ macos_versions = [
|
||||
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]:
|
||||
if include_beta:
|
||||
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:
|
||||
if data.darwin_version == int(darwin_version[:2]):
|
||||
return "macOS {} {}{}".format(data.name, data.macos_version, "" if data.release_status == "final" else " (Beta)")
|
||||
return None
|
||||
return None
|
||||
@@ -7,13 +7,11 @@ from Scripts import run
|
||||
from Scripts import utils
|
||||
|
||||
class DSDT:
|
||||
def __init__(self, **kwargs):
|
||||
#self.dl = downloader.Downloader()
|
||||
self.github = github.Github()
|
||||
self.fetcher = resource_fetcher.ResourceFetcher()
|
||||
self.r = run.Run()
|
||||
#self.u = utils.Utils("SSDT Time")
|
||||
self.u = utils.Utils()
|
||||
def __init__(self, utils_instance=None, github_instance=None, resource_fetcher_instance=None, run_instance=None):
|
||||
self.u = utils_instance if utils_instance else utils.Utils()
|
||||
self.github = github_instance if github_instance else github.Github()
|
||||
self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
|
||||
self.r = run_instance if run_instance else run.Run()
|
||||
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_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)
|
||||
|
||||
def _download_and_extract(self, temp, url):
|
||||
self.u.head("Gathering Files")
|
||||
print("")
|
||||
print("Please wait for download iasl...")
|
||||
print("")
|
||||
self.u.log_message("[DSDT] Downloading iasl...", level="INFO")
|
||||
ztemp = tempfile.mkdtemp(dir=temp)
|
||||
zfile = os.path.basename(url)
|
||||
#print("Downloading {}".format(os.path.basename(url)))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from Scripts.custom_dialogs import show_info
|
||||
from Scripts import github
|
||||
from Scripts import kext_maestro
|
||||
from Scripts import integrity_checker
|
||||
@@ -11,12 +12,12 @@ import platform
|
||||
os_name = platform.system()
|
||||
|
||||
class gatheringFiles:
|
||||
def __init__(self):
|
||||
self.utils = utils.Utils()
|
||||
self.github = github.Github()
|
||||
self.kext = kext_maestro.KextMaestro()
|
||||
self.fetcher = resource_fetcher.ResourceFetcher()
|
||||
self.integrity_checker = integrity_checker.IntegrityChecker()
|
||||
def __init__(self, utils_instance=None, github_instance=None, kext_maestro_instance=None, integrity_checker_instance=None, resource_fetcher_instance=None):
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
self.github = github_instance if github_instance else github.Github()
|
||||
self.kext = kext_maestro_instance if kext_maestro_instance else kext_maestro.KextMaestro()
|
||||
self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
|
||||
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.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"
|
||||
@@ -85,6 +86,7 @@ class gatheringFiles:
|
||||
|
||||
def move_bootloader_kexts_to_product_directory(self, product_name):
|
||||
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))
|
||||
|
||||
temp_product_dir = os.path.join(self.temporary_dir, product_name)
|
||||
@@ -139,9 +141,7 @@ class gatheringFiles:
|
||||
return True
|
||||
|
||||
def gather_bootloader_kexts(self, kexts, macos_version):
|
||||
self.utils.head("Gathering Files")
|
||||
print("")
|
||||
print("Please wait for download OpenCorePkg, kexts and macserial...")
|
||||
self.utils.log_message("[GATHERING FILES] Please wait for download OpenCorePkg, kexts and macserial...", level="INFO", to_build_log=True)
|
||||
|
||||
download_history = self.utils.read_file(self.download_history_file)
|
||||
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"))
|
||||
|
||||
if product_download_index is None:
|
||||
print("\n")
|
||||
print("Could not find download URL for {}.".format(product_name))
|
||||
self.utils.log_message("[GATHERING FILES] Could not find download URL for {}.".format(product_name), level="WARNING", to_build_log=True)
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
print("")
|
||||
print("Updating" if product_history_index is not None else "Please wait for download", end=" ")
|
||||
print("{}...".format(product_name))
|
||||
self.utils.log_message("[GATHERING FILES] Updating {}...".format(product_name), level="INFO", to_build_log=True)
|
||||
if product_download_url:
|
||||
print("from {}".format(product_download_url))
|
||||
print("")
|
||||
self.utils.log_message("[GATHERING FILES] Downloading from {}".format(product_download_url), level="INFO", to_build_log=True)
|
||||
else:
|
||||
print("")
|
||||
print("Could not find download URL for {}.".format(product_name))
|
||||
print("")
|
||||
self.utils.request_input()
|
||||
self.utils.log_message("[GATHERING FILES] Could not find download URL for {}.".format(product_name), level="ERROR", to_build_log=True)
|
||||
shutil.rmtree(self.temporary_dir, ignore_errors=True)
|
||||
return False
|
||||
|
||||
@@ -231,9 +224,10 @@ class gatheringFiles:
|
||||
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)
|
||||
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
|
||||
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))
|
||||
|
||||
self.utils.extract_zip_file(zip_path)
|
||||
@@ -250,17 +244,12 @@ class gatheringFiles:
|
||||
|
||||
if "OpenCore" in product_name:
|
||||
oc_binary_data_zip_path = os.path.join(self.temporary_dir, "OcBinaryData.zip")
|
||||
print("")
|
||||
print("Please wait for download OcBinaryData...")
|
||||
print("from {}".format(self.ocbinarydata_url))
|
||||
print("")
|
||||
self.utils.log_message("[GATHERING FILES] Please wait for download OcBinaryData...", level="INFO", to_build_log=True)
|
||||
self.utils.log_message("[GATHERING FILES] Downloading from {}".format(self.ocbinarydata_url), level="INFO", to_build_log=True)
|
||||
self.fetcher.download_and_save_file(self.ocbinarydata_url, oc_binary_data_zip_path)
|
||||
|
||||
if not os.path.exists(oc_binary_data_zip_path):
|
||||
print("")
|
||||
print("Could not download OcBinaryData at this time.")
|
||||
print("Please try again later.\n")
|
||||
self.utils.request_input()
|
||||
self.utils.log_message("[GATHERING FILES] Could not download OcBinaryData at this time. Please try again later.", level="ERROR", to_build_log=True)
|
||||
shutil.rmtree(self.temporary_dir, ignore_errors=True)
|
||||
return False
|
||||
|
||||
@@ -278,14 +267,9 @@ class gatheringFiles:
|
||||
response = self.fetcher.fetch_and_parse_content(patches_url, "plist")
|
||||
|
||||
return response["Kernel"]["Patch"]
|
||||
except:
|
||||
print("")
|
||||
print("Unable to download {} at this time".format(patches_name))
|
||||
print("from " + patches_url)
|
||||
print("")
|
||||
print("Please try again later or apply them manually.")
|
||||
print("")
|
||||
self.utils.request_input()
|
||||
except:
|
||||
self.utils.log_message("[GATHERING FILES] Unable to download {} at this time".format(patches_name), level="WARNING", to_build_log=True)
|
||||
show_info("Download Failed", "Unable to download {} at this time. Please try again later or apply them manually.".format(patches_name))
|
||||
return []
|
||||
|
||||
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":
|
||||
return
|
||||
|
||||
self.utils.head("Gathering Hardware Sniffer")
|
||||
self.utils.log_message("[GATHERING FILES] Gathering Hardware Sniffer...", level="INFO")
|
||||
|
||||
PRODUCT_NAME = "Hardware-Sniffer-CLI.exe"
|
||||
REPO_OWNER = "lzhoang2801"
|
||||
@@ -333,11 +317,7 @@ class gatheringFiles:
|
||||
break
|
||||
|
||||
if not all([product_id, product_download_url, sha256_hash]):
|
||||
print("")
|
||||
print("Could not find release information for {}.".format(PRODUCT_NAME))
|
||||
print("Please try again later.")
|
||||
print("")
|
||||
self.utils.request_input()
|
||||
show_info("Release Information Not Found", "Could not find release information for {}. Please try again later.".format(PRODUCT_NAME))
|
||||
raise Exception("Could not find release information for {}.".format(PRODUCT_NAME))
|
||||
|
||||
download_history = self.utils.read_file(self.download_history_file)
|
||||
@@ -356,22 +336,14 @@ class gatheringFiles:
|
||||
file_is_valid = (sha256_hash == local_hash)
|
||||
|
||||
if is_latest_id and file_is_valid:
|
||||
print("")
|
||||
print("Latest version of {} already downloaded.".format(PRODUCT_NAME))
|
||||
self.utils.log_message("[GATHERING FILES] Latest version of {} already downloaded.".format(PRODUCT_NAME), level="INFO")
|
||||
return destination_path
|
||||
|
||||
print("")
|
||||
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("")
|
||||
self.utils.log_message("[GATHERING FILES] {} {}...".format("Updating" if product_history_index is not None else "Please wait for download", PRODUCT_NAME), level="INFO")
|
||||
|
||||
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"
|
||||
print("Go to {} to download {} manually.".format(manual_download_url, PRODUCT_NAME))
|
||||
print("")
|
||||
self.utils.request_input()
|
||||
manual_download_url = "https://github.com/{}/{}/releases/latest".format(REPO_OWNER, REPO_NAME)
|
||||
show_info("Download Failed", "Go to {} to download {} manually.".format(manual_download_url, 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)
|
||||
|
||||
@@ -4,9 +4,9 @@ import random
|
||||
import json
|
||||
|
||||
class Github:
|
||||
def __init__(self):
|
||||
self.utils = utils.Utils()
|
||||
self.fetcher = resource_fetcher.ResourceFetcher()
|
||||
def __init__(self, utils_instance=None, resource_fetcher_instance=None):
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
|
||||
|
||||
def extract_payload(self, response):
|
||||
for line in response.splitlines():
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
from Scripts.datasets import os_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
|
||||
|
||||
class HardwareCustomizer:
|
||||
def __init__(self):
|
||||
self.compatibility_checker = compatibility_checker.CompatibilityChecker()
|
||||
self.utils = utils.Utils()
|
||||
def __init__(self, utils_instance=None):
|
||||
self.utils = utils_instance if utils_instance else 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):
|
||||
self.hardware_report = hardware_report
|
||||
@@ -16,7 +42,7 @@ class HardwareCustomizer:
|
||||
self.selected_devices = {}
|
||||
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():
|
||||
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":
|
||||
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:
|
||||
answer = self.utils.request_input("Build EFI for UEFI? (Yes/no): ").strip().lower()
|
||||
if answer == "yes":
|
||||
self.customized_hardware[device_type]["Firmware Type"] = "UEFI"
|
||||
break
|
||||
elif answer == "no":
|
||||
self.customized_hardware[device_type]["Firmware Type"] = "Legacy"
|
||||
break
|
||||
else:
|
||||
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
|
||||
if devices.get("Firmware Type") != "UEFI":
|
||||
content = (
|
||||
"Would you like to build the EFI for UEFI?<br>"
|
||||
"If yes, please make sure to update your BIOS and enable UEFI Boot Mode in your BIOS settings.<br>"
|
||||
"You can still proceed with Legacy if you prefer."
|
||||
)
|
||||
if show_confirmation("BIOS Firmware Type is not UEFI", content):
|
||||
self.utils.log_message("[HARDWARE CUSTOMIZATION] BIOS Firmware Type is not UEFI, building EFI for UEFI", level="INFO")
|
||||
self.customized_hardware[device_type]["Firmware Type"] = "UEFI"
|
||||
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
|
||||
|
||||
for device_name in devices:
|
||||
@@ -72,21 +94,27 @@ class HardwareCustomizer:
|
||||
self._handle_device_selection(device_type if device_type != "Network" else "WiFi")
|
||||
|
||||
if self.selected_devices:
|
||||
self.utils.head("Device Selection Summary")
|
||||
print("")
|
||||
print("Selected devices:")
|
||||
print("")
|
||||
print("Type Device Device ID")
|
||||
print("------------------------------------------------------------------")
|
||||
content = "The following devices have been selected for your configuration:<br>"
|
||||
content += "<table width='100%' cellpadding='4'>"
|
||||
content += "<tr>"
|
||||
content += "<td><b>Category</b></td>"
|
||||
content += "<td><b>Device Name</b></td>"
|
||||
content += "<td><b>Device ID</b></td>"
|
||||
content += "</tr>"
|
||||
|
||||
for device_type, device_dict in self.selected_devices.items():
|
||||
for device_name, device_props in device_dict.items():
|
||||
device_id = device_props.get("Device ID", "Unknown")
|
||||
print("{:<13} {:<42} {}".format(device_type, device_name[:38], device_id))
|
||||
print("")
|
||||
print("All other devices of the same type have been disabled.")
|
||||
print("")
|
||||
self.utils.request_input()
|
||||
|
||||
content += "<tr>"
|
||||
content += "<td>{}</td>".format(device_type)
|
||||
content += "<td>{}</td>".format(device_name)
|
||||
content += "<td>{}</td>".format(device_id)
|
||||
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
|
||||
|
||||
def _get_device_combinations(self, device_indices):
|
||||
@@ -114,10 +142,12 @@ class HardwareCustomizer:
|
||||
devices = self._get_compatible_devices(device_type)
|
||||
device_groups = None
|
||||
|
||||
title = "Multiple {} Devices Detected".format(device_type)
|
||||
content = []
|
||||
|
||||
if len(devices) > 1:
|
||||
print("\n*** Multiple {} Devices Detected".format(device_type))
|
||||
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":
|
||||
_apu_index = None
|
||||
_navi_22_indices = set()
|
||||
@@ -148,7 +178,7 @@ class HardwareCustomizer:
|
||||
_other_indices.add(index)
|
||||
|
||||
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 = []
|
||||
if _apu_index:
|
||||
@@ -158,7 +188,7 @@ class HardwareCustomizer:
|
||||
if _navi_indices or _intel_gpu_indices or _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:
|
||||
for selected_device in selected_devices:
|
||||
if not device_type in self.selected_devices:
|
||||
@@ -185,14 +215,15 @@ class HardwareCustomizer:
|
||||
|
||||
return compatible_devices
|
||||
|
||||
def _select_device(self, device_type, devices, device_groups=None):
|
||||
print("")
|
||||
def _select_device(self, device_type, devices, device_groups=None, title=None, content=None):
|
||||
self.utils.log_message("[HARDWARE CUSTOMIZATION] Starting device selection for {}".format(device_type), level="INFO")
|
||||
if device_groups:
|
||||
print("Please select a {} combination configuration:".format(device_type))
|
||||
content.append("Please select a {} combination configuration:".format(device_type))
|
||||
else:
|
||||
print("Please select which {} device you want to use:".format(device_type))
|
||||
print("")
|
||||
|
||||
content.append("Please select which {} device you want to use:".format(device_type))
|
||||
|
||||
options = []
|
||||
|
||||
if device_groups:
|
||||
valid_combinations = []
|
||||
|
||||
@@ -230,67 +261,48 @@ class HardwareCustomizer:
|
||||
|
||||
valid_combinations.sort(key=lambda x: (len(x[0]), x[2][0]))
|
||||
|
||||
for idx, (group_devices, _, group_compatibility) in enumerate(valid_combinations, start=1):
|
||||
print("{}. {}".format(idx, " + ".join(group_devices)))
|
||||
for group_devices, group_indices, group_compatibility in valid_combinations:
|
||||
option = "<b>{}</b>".format(" + ".join(group_devices))
|
||||
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:
|
||||
device_props = devices[group_devices[0]]
|
||||
if 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(group_compatibility[0]):
|
||||
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.")
|
||||
option += "<br>OCLP Compatibility: {}".format(self.show_macos_compatibility((device_props.get("OCLP Compatibility")[0], os_data.get_lowest_darwin_version())))
|
||||
options.append(option)
|
||||
else:
|
||||
for index, device_name in enumerate(devices, start=1):
|
||||
device_props = devices[device_name]
|
||||
for device_name, device_props in devices.items():
|
||||
compatibility = device_props.get("Compatibility")
|
||||
|
||||
print("{}. {}".format(index, device_name))
|
||||
print(" Device ID: {}".format(device_props.get("Device ID", "Unknown")))
|
||||
print(" Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility(compatibility)))
|
||||
option = "<b>{}</b>".format(device_name)
|
||||
option += "<br>Device ID: {}".format(device_props.get("Device ID", "Unknown"))
|
||||
option += "<br>Compatibility: {}".format(self.show_macos_compatibility(compatibility))
|
||||
|
||||
if 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]):
|
||||
print(" OCLP Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version()))))
|
||||
print()
|
||||
option += "<br>OCLP Compatibility: {}".format(self.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version())))
|
||||
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:
|
||||
choice = self.utils.request_input(f"Select a {device_type} device (1-{len(devices)}): ")
|
||||
|
||||
try:
|
||||
choice_num = int(choice)
|
||||
if 1 <= choice_num <= len(devices):
|
||||
selected_device = list(devices)[choice_num - 1]
|
||||
|
||||
for device in devices:
|
||||
if device != selected_device:
|
||||
self._disable_device(device_type, device, devices[device])
|
||||
|
||||
return [selected_device]
|
||||
else:
|
||||
print("Invalid option. Please try again.")
|
||||
except ValueError:
|
||||
print("Please enter a valid number.")
|
||||
while True:
|
||||
choice_num = show_options_dialog(title, "<br>".join(content), options, default_index=len(options) - 1)
|
||||
|
||||
if choice_num is None:
|
||||
continue
|
||||
|
||||
if device_groups:
|
||||
selected_devices, _, _ = valid_combinations[choice_num]
|
||||
else:
|
||||
selected_devices = [list(devices)[choice_num]]
|
||||
|
||||
for device in devices:
|
||||
if device not in selected_devices:
|
||||
self._disable_device(device_type, device, devices[device])
|
||||
|
||||
self.utils.log_message("[HARDWARE CUSTOMIZATION] Selected devices: {}".format(", ".join(selected_devices)), level="INFO")
|
||||
return selected_devices
|
||||
|
||||
def _disable_device(self, device_type, device_name, device_props):
|
||||
if device_type == "WiFi":
|
||||
|
||||
@@ -4,8 +4,8 @@ import json
|
||||
from Scripts import utils
|
||||
|
||||
class IntegrityChecker:
|
||||
def __init__(self):
|
||||
self.utils = utils.Utils()
|
||||
def __init__(self, utils_instance=None):
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
|
||||
def get_sha256(self, file_path, block_size=65536):
|
||||
if not os.path.exists(file_path) or os.path.isdir(file_path):
|
||||
@@ -17,7 +17,7 @@ class IntegrityChecker:
|
||||
sha256.update(block)
|
||||
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):
|
||||
return None
|
||||
|
||||
@@ -26,8 +26,15 @@ class IntegrityChecker:
|
||||
|
||||
manifest_data = {}
|
||||
for root, _, files in os.walk(folder_path):
|
||||
if '.git' in root or "__pycache__" in root:
|
||||
continue
|
||||
|
||||
for name in files:
|
||||
if '.git' in name or ".pyc" in name:
|
||||
continue
|
||||
|
||||
file_path = os.path.join(root, name)
|
||||
|
||||
relative_path = os.path.relpath(file_path, folder_path).replace('\\', '/')
|
||||
|
||||
if relative_path == os.path.basename(manifest_path):
|
||||
@@ -35,7 +42,8 @@ class IntegrityChecker:
|
||||
|
||||
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
|
||||
|
||||
def verify_folder_integrity(self, folder_path, manifest_path=None):
|
||||
@@ -83,4 +91,4 @@ class IntegrityChecker:
|
||||
|
||||
is_valid = not any(issues.values())
|
||||
|
||||
return is_valid, issues
|
||||
return is_valid, issues
|
||||
@@ -6,6 +6,7 @@ from Scripts.datasets import codec_layouts
|
||||
from Scripts import utils
|
||||
import os
|
||||
import shutil
|
||||
import random
|
||||
|
||||
try:
|
||||
long
|
||||
@@ -14,9 +15,11 @@ except NameError:
|
||||
long = int
|
||||
unicode = str
|
||||
|
||||
from Scripts.custom_dialogs import show_options_dialog, show_info, show_confirmation, show_checklist_dialog
|
||||
|
||||
class KextMaestro:
|
||||
def __init__(self):
|
||||
self.utils = utils.Utils()
|
||||
def __init__(self, utils_instance=None):
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
self.matching_keys = [
|
||||
"IOPCIMatch",
|
||||
"IONameMatch",
|
||||
@@ -77,6 +80,52 @@ class KextMaestro:
|
||||
|
||||
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):
|
||||
kext = self.kexts[index]
|
||||
|
||||
@@ -96,9 +145,7 @@ class KextMaestro:
|
||||
other_kext.checked = False
|
||||
|
||||
def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi_patches):
|
||||
self.utils.head("Select Required Kernel Extensions")
|
||||
print("")
|
||||
print("Checking for required kernel extensions...")
|
||||
self.utils.log_message("[KEXT MAESTRO] Checking for required kernel extensions...", level="INFO")
|
||||
|
||||
for kext in self.kexts:
|
||||
kext.checked = kext.required
|
||||
@@ -122,24 +169,26 @@ class KextMaestro:
|
||||
for codec_properties in hardware_report.get("Sound", {}).values():
|
||||
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"):
|
||||
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.")
|
||||
print("To use AppleALC, you must rollback AppleHDA. Alternatively, you can use VoodooHDA.")
|
||||
print("")
|
||||
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")
|
||||
print("")
|
||||
while True:
|
||||
kext_option = self.utils.request_input("Select audio kext for your system: ").strip()
|
||||
if kext_option == "1":
|
||||
needs_oclp = True
|
||||
selected_kexts.append("AppleALC")
|
||||
break
|
||||
elif kext_option == "2":
|
||||
break
|
||||
else:
|
||||
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
|
||||
content = (
|
||||
"Since macOS Tahoe 26 DP2, Apple has removed AppleHDA and uses the Apple T2 chip for audio management.<br>"
|
||||
"Therefore, AppleALC is no longer functional until you rollback AppleHDA."
|
||||
)
|
||||
options = [
|
||||
"<b>AppleALC</b> - Requires rollback AppleHDA with <b>OpenCore Legacy Patcher</b>",
|
||||
"<b>VoodooHDA</b> - Lower audio quality than use AppleHDA, injection kext into <b>/Library/Extensions</b>"
|
||||
]
|
||||
result = show_options_dialog("Audio Kext Selection", content, options, default_index=0)
|
||||
if result == 0:
|
||||
needs_oclp = True
|
||||
selected_kexts.append("AppleALC")
|
||||
else:
|
||||
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 \
|
||||
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
|
||||
|
||||
if gpu_props.get("Codename") in {"Navi 21", "Navi 23"}:
|
||||
print("\n*** Found {} is AMD {} GPU.".format(gpu_name, gpu_props.get("Codename")))
|
||||
print("")
|
||||
print("\033[91mImportant: Black Screen Fix\033[0m")
|
||||
print("If you experience a black screen after verbose mode:")
|
||||
print(" 1. Use ProperTree to open config.plist")
|
||||
print(" 2. Navigate to NVRAM -> Add -> 7C436110-AB2A-4BBB-A880-FE41995C9F82 -> boot-args")
|
||||
print(" 3. Remove \"-v debug=0x100 keepsyms=1\" from boot-args")
|
||||
print("")
|
||||
content = (
|
||||
"<span style='color:red font-weight:bold'>Important: Black Screen Fix</span><br>"
|
||||
"If you experience a black screen after verbose mode:<br>"
|
||||
"1. Use ProperTree to open config.plist<br>"
|
||||
"2. Navigate to NVRAM -> Add -> 7C436110-AB2A-4BBB-A880-FE41995C9F82 -> boot-args<br>"
|
||||
"3. Remove \"-v debug=0x100 keepsyms=1\" from boot-args<br><br>"
|
||||
).format(gpu_name, gpu_props.get("Codename"))
|
||||
|
||||
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"):
|
||||
recommended_option = 1
|
||||
recommended_name = "NootRX"
|
||||
max_option = 3
|
||||
print("\033[1;93mNote:\033[0m Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD {} GPUs.".format(gpu_props.get("Codename")))
|
||||
print("To avoid this, you can use NootRX or choose not to install a GPU kext.")
|
||||
print("")
|
||||
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")
|
||||
content += (
|
||||
"Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD {} GPUs.<br>"
|
||||
"To avoid this, you can use NootRX or choose not to install a GPU kext."
|
||||
).format(gpu_props.get("Codename"))
|
||||
options.append("<b>Don't use any kext</b>")
|
||||
recommended_option = 0
|
||||
else:
|
||||
recommended_option = 2
|
||||
recommended_name = "WhateverGreen"
|
||||
max_option = 2
|
||||
print("\033[1;93mNote:\033[0m")
|
||||
print("- AMD {} GPUs have two available kext options:".format(gpu_props.get("Codename")))
|
||||
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("")
|
||||
content += (
|
||||
"AMD {} GPUs have two available kext options:<br>"
|
||||
"You can try different kexts after installation to find the best one for your system."
|
||||
).format(gpu_props.get("Codename"))
|
||||
recommended_option = 1
|
||||
|
||||
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")
|
||||
print("Automatically selecting WhateverGreen kext due to Intel GPU compatibility")
|
||||
print("")
|
||||
self.utils.request_input("Press Enter to continue...")
|
||||
show_info("NootRX Kext Warning", "NootRX kext is not compatible with Intel GPUs.<br>Automatically selecting WhateverGreen kext due to Intel GPU compatibility.")
|
||||
selected_kexts.append("WhateverGreen")
|
||||
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:
|
||||
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:
|
||||
if result == 0:
|
||||
selected_kexts.append("NootRX")
|
||||
elif selected_option == 2:
|
||||
elif result == 1:
|
||||
selected_kexts.append("WhateverGreen")
|
||||
|
||||
|
||||
continue
|
||||
|
||||
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")))
|
||||
print("")
|
||||
print("\033[1;93mNote:\033[0m Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD GPUs.")
|
||||
print("The current recommendation is to not use WhateverGreen.")
|
||||
print("However, you can still try adding it to see if it works on your system.")
|
||||
print("")
|
||||
self.utils.request_input("Press Enter to continue...")
|
||||
content = (
|
||||
"Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD GPUs.<br>"
|
||||
"The current recommendation is to not use WhateverGreen.<br>"
|
||||
"However, you can still try adding it to see if it works on your system."
|
||||
)
|
||||
show_info("AMD GPU Kext Warning", content)
|
||||
break
|
||||
|
||||
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"):
|
||||
selected_kexts.append("AirportBrcmFixup")
|
||||
elif device_id in pci_data.IntelWiFiIDs:
|
||||
print("\n*** Found {} is Intel WiFi device.".format(network_name))
|
||||
print("")
|
||||
print("\033[1;93mNote:\033[0m Intel WiFi devices have two available kext options:")
|
||||
print("")
|
||||
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")
|
||||
airport_itlwm_content = (
|
||||
"<b>AirportItlwm</b> - Uses native WiFi settings menu<br>"
|
||||
"• Provides Handoff, Universal Clipboard, Location Services, Instant Hotspot support<br>"
|
||||
"• Supports enterprise-level security<br>"
|
||||
)
|
||||
|
||||
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"):
|
||||
print(" • \033[91mOn macOS Sonoma 14\033[0m: 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("")
|
||||
airport_itlwm_content += "• <span style='color:red'>On macOS Sonoma 14</span>: iServices won't work unless using OCLP root patch"
|
||||
|
||||
recommended_option = 2 if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0") else 1
|
||||
recommended_name = "itlwm" if recommended_option == 2 else "AirportItlwm"
|
||||
itlwm_content = (
|
||||
"<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):
|
||||
print("\033[91mImportant:\033[0m For macOS Beta versions, only itlwm kext is supported")
|
||||
print("")
|
||||
self.utils.request_input("Press Enter to continue...")
|
||||
selected_option = recommended_option
|
||||
show_info("Intel WiFi Kext Selection", "For macOS Beta versions, only itlwm kext is supported.")
|
||||
selected_option = 1
|
||||
else:
|
||||
kext_option = self.utils.request_input("Select kext for your Intel WiFi device (default: {}): ".format(recommended_name)).strip() or str(recommended_option)
|
||||
|
||||
if kext_option.isdigit() and 0 < int(kext_option) < 3:
|
||||
selected_option = int(kext_option)
|
||||
else:
|
||||
print("\033[91mInvalid selection, using recommended option: {}\033[0m".format(recommended_option))
|
||||
selected_option = recommended_option
|
||||
|
||||
if selected_option == 2:
|
||||
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 selected_option == 1:
|
||||
selected_kexts.append("itlwm")
|
||||
else:
|
||||
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"):
|
||||
selected_kexts.append("IOSkywalkFamily")
|
||||
elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"):
|
||||
print("")
|
||||
print("\033[1;93mNote:\033[0m Since macOS Sonoma 14, iServices won't work with AirportItlwm without patches")
|
||||
print("")
|
||||
while True:
|
||||
option = self.utils.request_input("Apply OCLP root patch to fix iServices? (yes/No): ").strip().lower()
|
||||
if option == "yes":
|
||||
selected_kexts.append("IOSkywalkFamily")
|
||||
break
|
||||
elif option == "no":
|
||||
break
|
||||
else:
|
||||
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
|
||||
content = (
|
||||
"Since macOS Sonoma 14, iServices won't work with AirportItlwm without patches.<br><br>"
|
||||
"Apply OCLP root patch to fix iServices?"
|
||||
)
|
||||
if show_confirmation("OpenCore Legacy Patcher Required", content):
|
||||
selected_kexts.append("IOSkywalkFamily")
|
||||
elif device_id in pci_data.AtherosWiFiIDs[:8]:
|
||||
selected_kexts.append("corecaptureElCap")
|
||||
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:
|
||||
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):
|
||||
for kext in self.kexts:
|
||||
@@ -639,72 +661,65 @@ class KextMaestro:
|
||||
if not incompatible_kexts:
|
||||
return False
|
||||
|
||||
while True:
|
||||
self.utils.head("Kext Compatibility Check")
|
||||
print("\nIncompatible kexts for the current macOS version ({}):\n".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 ""))
|
||||
|
||||
print("\n\033[1;93mNote:\033[0m")
|
||||
print("- With Lilu plugins, using the \"-lilubetaall\" boot argument will force them to load.")
|
||||
print("- Forcing unsupported kexts can cause system instability. \033[0;31mProceed with caution.\033[0m")
|
||||
print("")
|
||||
|
||||
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"))
|
||||
|
||||
if option.lower() == "yes":
|
||||
return True
|
||||
elif option.lower() == "no":
|
||||
return False
|
||||
content = (
|
||||
"List of incompatible kexts for the current macOS version ({}):<br>"
|
||||
"<ul>"
|
||||
).format(target_darwin_version)
|
||||
|
||||
for index, (kext_name, is_lilu_dependent) in enumerate(incompatible_kexts):
|
||||
content += "<li><b>{}. {}</b>".format(index + 1, kext_name)
|
||||
if is_lilu_dependent:
|
||||
content += " - Lilu Plugin"
|
||||
content += "</li>"
|
||||
|
||||
content += (
|
||||
"</ul><br>"
|
||||
"<b>Note:</b><br>"
|
||||
"• With Lilu plugins, using the \"-lilubetaall\" boot argument will force them to load.<br>"
|
||||
"• Forcing unsupported kexts can cause system instability. <b><span style='color:red'>Proceed with caution.</span></b><br><br>"
|
||||
"Do you want to force load {} on the unsupported macOS version?"
|
||||
).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):
|
||||
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:
|
||||
contents = []
|
||||
contents.append("")
|
||||
contents.append("List of available kexts:")
|
||||
for index, kext in enumerate(self.kexts, start=1):
|
||||
if kext.category != current_category:
|
||||
current_category = kext.category
|
||||
category_header = "Category: {}".format(current_category if current_category else "Uncategorized")
|
||||
contents.append(f"\n{category_header}\n" + "=" * len(category_header))
|
||||
checkbox = "[*]" if kext.checked else "[ ]"
|
||||
|
||||
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)
|
||||
newly_checked = [i for i in selected_indices if i not in checked_indices]
|
||||
|
||||
allow_unsupported_kexts = self.verify_kext_compatibility(newly_checked, macos_version)
|
||||
|
||||
for i, kext in enumerate(self.kexts):
|
||||
if i not in selected_indices and kext.checked and not kext.required:
|
||||
self.uncheck_kext(i)
|
||||
|
||||
for i in selected_indices:
|
||||
self.check_kext(i, macos_version, allow_unsupported_kexts)
|
||||
15
Scripts/pages/__init__.py
Normal file
15
Scripts/pages/__init__.py
Normal 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
552
Scripts/pages/build_page.py
Normal 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)
|
||||
557
Scripts/pages/compatibility_page.py
Normal file
557
Scripts/pages/compatibility_page.py
Normal 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()
|
||||
293
Scripts/pages/configuration_page.py
Normal file
293
Scripts/pages/configuration_page.py
Normal 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
168
Scripts/pages/home_page.py
Normal 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
|
||||
485
Scripts/pages/select_hardware_report_page.py
Normal file
485
Scripts/pages/select_hardware_report_page.py
Normal 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")
|
||||
271
Scripts/pages/settings_page.py
Normal file
271
Scripts/pages/settings_page.py
Normal 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")
|
||||
@@ -4,10 +4,10 @@ import os
|
||||
import re
|
||||
|
||||
class ReportValidator:
|
||||
def __init__(self):
|
||||
def __init__(self, utils_instance=None):
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
self.u = utils.Utils()
|
||||
self.u = utils_instance if utils_instance else utils.Utils()
|
||||
|
||||
self.PATTERNS = {
|
||||
"not_empty": r".+",
|
||||
@@ -244,17 +244,17 @@ class ReportValidator:
|
||||
if expected_type:
|
||||
if not isinstance(data, 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
|
||||
|
||||
if isinstance(data, str):
|
||||
pattern = rule.get("pattern")
|
||||
if pattern is not None:
|
||||
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
|
||||
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
|
||||
|
||||
cleaned_data = data
|
||||
@@ -265,53 +265,30 @@ class ReportValidator:
|
||||
|
||||
for key, value in data.items():
|
||||
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:
|
||||
cleaned_data[key] = cleaned_val
|
||||
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:
|
||||
cleaned_data[key] = cleaned_val
|
||||
else:
|
||||
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():
|
||||
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):
|
||||
item_rule = rule.get("item_rule")
|
||||
if item_rule:
|
||||
cleaned_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:
|
||||
cleaned_data.append(cleaned_val)
|
||||
else:
|
||||
cleaned_data = list(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))
|
||||
return cleaned_data
|
||||
@@ -20,14 +20,14 @@ else:
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
class ResourceFetcher:
|
||||
def __init__(self, headers=None):
|
||||
def __init__(self, utils_instance=None, integrity_checker_instance=None, headers=None):
|
||||
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"
|
||||
}
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
self.buffer_size = 16 * 1024
|
||||
self.ssl_context = self.create_ssl_context()
|
||||
self.integrity_checker = integrity_checker.IntegrityChecker()
|
||||
self.utils = utils.Utils()
|
||||
self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker()
|
||||
|
||||
def create_ssl_context(self):
|
||||
try:
|
||||
@@ -36,9 +36,10 @@ class ResourceFetcher:
|
||||
import certifi
|
||||
cafile = certifi.where()
|
||||
ssl_context = ssl.create_default_context(cafile=cafile)
|
||||
self.utils.log_message("[RESOURCE FETCHER] Created SSL context", level="INFO")
|
||||
except Exception as e:
|
||||
print("Failed to create SSL context: {}".format(e))
|
||||
ssl_context = ssl._create_unverified_context()
|
||||
self.utils.log_message("[RESOURCE FETCHER] Created unverified SSL context", level="INFO")
|
||||
return ssl_context
|
||||
|
||||
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)
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
print("Request failed: {}".format(e))
|
||||
self.utils.log_message("[RESOURCE FETCHER] Request failed: {}".format(e), level="ERROR", to_build_log=True)
|
||||
|
||||
return None
|
||||
|
||||
@@ -62,12 +63,14 @@ class ResourceFetcher:
|
||||
attempt = 0
|
||||
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)
|
||||
|
||||
if not response:
|
||||
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
|
||||
|
||||
if response.getcode() == 200:
|
||||
@@ -76,7 +79,7 @@ class ResourceFetcher:
|
||||
attempt += 1
|
||||
|
||||
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
|
||||
|
||||
content = response.read()
|
||||
@@ -85,12 +88,12 @@ class ResourceFetcher:
|
||||
try:
|
||||
content = gzip.decompress(content)
|
||||
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":
|
||||
try:
|
||||
content = zlib.decompress(content)
|
||||
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:
|
||||
if content_type == "json":
|
||||
@@ -100,7 +103,7 @@ class ResourceFetcher:
|
||||
else:
|
||||
return content.decode("utf-8")
|
||||
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
|
||||
|
||||
@@ -150,20 +153,19 @@ class ResourceFetcher:
|
||||
else:
|
||||
progress = "{} {:.1f}MB downloaded".format(speed_str, bytes_downloaded/(1024*1024))
|
||||
|
||||
print(" " * 80, end="\r")
|
||||
print(progress, end="\r")
|
||||
|
||||
print()
|
||||
self.utils.log_message("[RESOURCE FETCHER] Download progress: {}".format(progress), level="INFO", to_build_log=True)
|
||||
|
||||
def download_and_save_file(self, resource_url, destination_path, sha256_hash=None):
|
||||
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:
|
||||
attempt += 1
|
||||
response = self._make_request(resource_url)
|
||||
|
||||
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
|
||||
|
||||
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 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)
|
||||
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
|
||||
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)
|
||||
continue
|
||||
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
|
||||
|
||||
if os.path.exists(destination_path):
|
||||
os.remove(destination_path)
|
||||
|
||||
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
|
||||
53
Scripts/settings.py
Normal file
53
Scripts/settings.py
Normal 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))
|
||||
@@ -1,9 +1,11 @@
|
||||
from Scripts.datasets.mac_model_data import mac_devices
|
||||
from Scripts.datasets import kext_data
|
||||
from Scripts.datasets import os_data
|
||||
from Scripts.custom_dialogs import show_smbios_selection_dialog
|
||||
from Scripts import gathering_files
|
||||
from Scripts import run
|
||||
from Scripts import utils
|
||||
from Scripts import settings
|
||||
import os
|
||||
import uuid
|
||||
import random
|
||||
@@ -12,10 +14,11 @@ import platform
|
||||
os_name = platform.system()
|
||||
|
||||
class SMBIOS:
|
||||
def __init__(self):
|
||||
self.g = gathering_files.gatheringFiles()
|
||||
self.run = run.Run().run
|
||||
self.utils = utils.Utils()
|
||||
def __init__(self, gathering_files_instance=None, run_instance=None, utils_instance=None, settings_instance=None):
|
||||
self.g = gathering_files_instance if gathering_files_instance else gathering_files.gatheringFiles()
|
||||
self.run = run_instance.run if run_instance else run.Run().run
|
||||
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__))
|
||||
|
||||
def check_macserial(self, retry_count=0):
|
||||
@@ -28,6 +31,7 @@ class SMBIOS:
|
||||
elif os_name == "Darwin":
|
||||
macserial_binary = ["macserial"]
|
||||
else:
|
||||
self.utils.log_message("[SMBIOS] Unknown OS for macserial", level="ERROR")
|
||||
raise Exception("Unknown OS for macserial")
|
||||
|
||||
for binary in macserial_binary:
|
||||
@@ -36,6 +40,7 @@ class SMBIOS:
|
||||
return macserial_path
|
||||
|
||||
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))
|
||||
|
||||
download_history = self.utils.read_file(self.g.download_history_file)
|
||||
@@ -68,13 +73,21 @@ class SMBIOS:
|
||||
else:
|
||||
serial = output[0].splitlines()[0].split(" | ")
|
||||
|
||||
return {
|
||||
smbios_info = {
|
||||
"MLB": "A" + "0"*15 + "Z" if not serial else serial[-1],
|
||||
"ROM": random_mac_address,
|
||||
"SystemProductName": smbios_model,
|
||||
"SystemSerialNumber": "A" + "0"*10 + "9" if not serial else serial[0],
|
||||
"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):
|
||||
for patch in acpi_patches:
|
||||
@@ -160,79 +173,45 @@ class SMBIOS:
|
||||
elif "Ice Lake" in codename:
|
||||
smbios_model = "MacBookAir9,1"
|
||||
|
||||
self.utils.log_message("[SMBIOS] Suggested SMBIOS model: {}".format(smbios_model), level="INFO")
|
||||
return smbios_model
|
||||
|
||||
def customize_smbios_model(self, hardware_report, selected_smbios_model, macos_version):
|
||||
current_category = None
|
||||
|
||||
def customize_smbios_model(self, hardware_report, selected_smbios_model, macos_version, parent=None):
|
||||
default_smbios_model = self.select_smbios_model(hardware_report, macos_version)
|
||||
show_all_models = False
|
||||
is_laptop = "Laptop" == hardware_report.get("Motherboard").get("Platform")
|
||||
|
||||
while True:
|
||||
incompatible_models_by_index = []
|
||||
contents = []
|
||||
contents.append("")
|
||||
if show_all_models:
|
||||
contents.append("List of available SMBIOS:")
|
||||
else:
|
||||
contents.append("List of compatible SMBIOS:")
|
||||
for index, device in enumerate(mac_devices, start=1):
|
||||
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)
|
||||
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 "[ ]"
|
||||
macos_name = os_data.get_macos_name_by_darwin(macos_version)
|
||||
|
||||
items = []
|
||||
for index, device in enumerate(mac_devices):
|
||||
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)
|
||||
|
||||
platform_match = True
|
||||
if is_laptop and not device.name.startswith("MacBook"):
|
||||
platform_match = False
|
||||
elif not is_laptop and device.name.startswith("MacBook"):
|
||||
platform_match = False
|
||||
|
||||
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))
|
||||
if device.name == selected_smbios_model:
|
||||
line = "\033[1;32m{}\033[0m".format(line)
|
||||
elif not isSupported:
|
||||
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 mac models that are not officially supported by {}.".format(os_data.get_macos_name_by_darwin(macos_version)))
|
||||
contents.append("")
|
||||
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)
|
||||
is_compatible = is_supported and platform_match
|
||||
|
||||
category = ""
|
||||
for char in device.name:
|
||||
if char.isdigit():
|
||||
break
|
||||
category += char
|
||||
|
||||
gpu_str = "" if not device.discrete_gpu else " - {}".format(device.discrete_gpu)
|
||||
label = "{} - {} ({}){}".format(device.name, device.cpu, device.cpu_generation, gpu_str)
|
||||
|
||||
self.utils.adjust_window_size(content)
|
||||
self.utils.head("Customize SMBIOS Model", 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 selected_smbios_model
|
||||
if option.lower() == "r" and selected_smbios_model != default_smbios_model:
|
||||
return default_smbios_model
|
||||
if option.lower() in ("a", "c"):
|
||||
show_all_models = not show_all_models
|
||||
continue
|
||||
|
||||
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
|
||||
items.append({
|
||||
'name': device.name,
|
||||
'label': label,
|
||||
'category': category,
|
||||
'is_supported': is_supported,
|
||||
'is_compatible': is_compatible
|
||||
})
|
||||
|
||||
content = "Lines in gray indicate mac models that are not officially supported by {}.".format(macos_name)
|
||||
|
||||
result = show_smbios_selection_dialog("Customize SMBIOS Model", content, items, selected_smbios_model, default_smbios_model)
|
||||
|
||||
return result if result else selected_smbios_model
|
||||
36
Scripts/state.py
Normal file
36
Scripts/state.py
Normal 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
68
Scripts/styles.py
Normal 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
183
Scripts/ui_utils.py
Normal 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
|
||||
122
Scripts/utils.py
122
Scripts/utils.py
@@ -1,18 +1,38 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import plistlib
|
||||
import shutil
|
||||
import re
|
||||
import binascii
|
||||
import subprocess
|
||||
import pathlib
|
||||
import zipfile
|
||||
import tempfile
|
||||
import traceback
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
class Utils:
|
||||
def __init__(self, script_name = "OpCore Simplify"):
|
||||
self.script_name = script_name
|
||||
def __init__(self):
|
||||
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):
|
||||
temporary_dir = tempfile.gettempdir()
|
||||
@@ -26,6 +46,7 @@ class Utils:
|
||||
try:
|
||||
shutil.rmtree(os.path.join(temporary_dir, file))
|
||||
except Exception as e:
|
||||
self.log_message("[UTILS] Failed to remove temp directory {}: {}".format(file, e), "Error")
|
||||
pass
|
||||
|
||||
def get_temporary_dir(self):
|
||||
@@ -127,23 +148,6 @@ class Utils:
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
major, minor, patch = map(int, darwin_version.split('.'))
|
||||
@@ -156,78 +160,4 @@ class Utils:
|
||||
else:
|
||||
subprocess.run(['xdg-open', folder_path])
|
||||
elif os.name == 'nt':
|
||||
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)
|
||||
os.startfile(folder_path)
|
||||
29
Scripts/value_formatters.py
Normal file
29
Scripts/value_formatters.py
Normal 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"
|
||||
310
Scripts/widgets/config_editor.py
Normal file
310
Scripts/widgets/config_editor.py
Normal 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)
|
||||
@@ -1,14 +1,15 @@
|
||||
from Scripts import run
|
||||
from Scripts import utils
|
||||
from Scripts.custom_dialogs import ask_network_count, show_info, show_confirmation
|
||||
import platform
|
||||
import json
|
||||
|
||||
os_name = platform.system()
|
||||
|
||||
class WifiProfileExtractor:
|
||||
def __init__(self):
|
||||
self.run = run.Run().run
|
||||
self.utils = utils.Utils()
|
||||
def __init__(self, run_instance=None, utils_instance=None):
|
||||
self.run = run_instance.run if run_instance else run.Run().run
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
|
||||
def get_authentication_type(self, authentication_type):
|
||||
authentication_type = authentication_type.lower()
|
||||
@@ -27,14 +28,16 @@ class WifiProfileExtractor:
|
||||
return None
|
||||
|
||||
def validate_wifi_password(self, authentication_type=None, password=None):
|
||||
print("Validating password with authentication type: {}".format(authentication_type))
|
||||
|
||||
if password is None:
|
||||
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Password is not found", level="INFO")
|
||||
return None
|
||||
|
||||
if authentication_type is None:
|
||||
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Authentication type is not found", level="INFO")
|
||||
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":
|
||||
return ""
|
||||
|
||||
@@ -103,30 +106,14 @@ class WifiProfileExtractor:
|
||||
return self.validate_wifi_password(authentication_type, password)
|
||||
|
||||
def ask_network_count(self, total_networks):
|
||||
self.utils.head("WiFi Network Retrieval")
|
||||
print("")
|
||||
print("Found {} WiFi networks on this device.".format(total_networks))
|
||||
print("")
|
||||
print("How many networks would you like to process?")
|
||||
print(" 1-{} - Specific number (default: 5)".format(total_networks))
|
||||
print(" A - All available networks")
|
||||
print("")
|
||||
if self.utils.gui_handler:
|
||||
result = ask_network_count(total_networks)
|
||||
if result == 'a':
|
||||
return total_networks
|
||||
return int(result)
|
||||
|
||||
num_choice = self.utils.request_input("Enter your choice: ").strip().lower() or "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
|
||||
|
||||
return 5
|
||||
|
||||
def process_networks(self, ssid_list, max_networks, get_password_func):
|
||||
networks = []
|
||||
processed_count = 0
|
||||
@@ -137,39 +124,35 @@ class WifiProfileExtractor:
|
||||
ssid = ssid_list[processed_count]
|
||||
|
||||
try:
|
||||
print("")
|
||||
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.")
|
||||
|
||||
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)
|
||||
password = get_password_func(ssid)
|
||||
if password is not None:
|
||||
if (ssid, password) not in networks:
|
||||
consecutive_failures = 0
|
||||
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:
|
||||
break
|
||||
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
|
||||
print("Could not retrieve password for this network.")
|
||||
|
||||
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
|
||||
|
||||
consecutive_failures = 0
|
||||
except Exception as e:
|
||||
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:
|
||||
continue_input = self.utils.request_input("\nUnable to retrieve passwords. Continue trying? (Yes/no): ").strip().lower() or "yes"
|
||||
|
||||
if continue_input != "yes":
|
||||
result = show_confirmation("WiFi Profile Extractor", "Unable to retrieve passwords. Continue trying?")
|
||||
|
||||
if not result:
|
||||
break
|
||||
|
||||
consecutive_failures = 0
|
||||
@@ -177,12 +160,11 @@ class WifiProfileExtractor:
|
||||
processed_count += 1
|
||||
|
||||
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
|
||||
|
||||
@@ -201,10 +183,12 @@ class WifiProfileExtractor:
|
||||
|
||||
max_networks = self.ask_network_count(len(ssid_list))
|
||||
|
||||
self.utils.head("Administrator Authentication Required")
|
||||
print("")
|
||||
print("To retrieve WiFi passwords from the Keychain, macOS will prompt")
|
||||
print("you for administrator credentials for each WiFi network.")
|
||||
if self.utils.gui_handler:
|
||||
content = (
|
||||
"To retrieve WiFi passwords from the Keychain, macOS will prompt<br>"
|
||||
"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)
|
||||
|
||||
@@ -232,9 +216,7 @@ class WifiProfileExtractor:
|
||||
|
||||
max_networks = len(ssid_list)
|
||||
|
||||
self.utils.head("WiFi Profile Extractor")
|
||||
print("")
|
||||
print("Retrieving passwords for {} network(s)...".format(len(ssid_list)))
|
||||
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving passwords for {} network(s)".format(len(ssid_list)), level="INFO", to_build_log=True)
|
||||
|
||||
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_windows)
|
||||
|
||||
@@ -253,9 +235,7 @@ class WifiProfileExtractor:
|
||||
|
||||
max_networks = len(ssid_list)
|
||||
|
||||
self.utils.head("WiFi Profile Extractor")
|
||||
print("")
|
||||
print("Retrieving passwords for {} network(s)...".format(len(ssid_list)))
|
||||
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving passwords for {} network(s)".format(len(ssid_list)), level="INFO", to_build_log=True)
|
||||
|
||||
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_linux)
|
||||
|
||||
@@ -286,31 +266,21 @@ class WifiProfileExtractor:
|
||||
return interfaces
|
||||
|
||||
def get_profiles(self):
|
||||
os_name = platform.system()
|
||||
|
||||
self.utils.head("WiFi Profile Extractor")
|
||||
print("")
|
||||
print("\033[1;93mNote:\033[0m")
|
||||
print("- When using itlwm kext, WiFi appears as Ethernet in macOS")
|
||||
print("- You'll need Heliport app to manage WiFi connections in macOS")
|
||||
print("- This step will enable auto WiFi connections at boot time")
|
||||
print(" and is useful for users installing macOS via Recovery OS")
|
||||
print("")
|
||||
content = (
|
||||
"<b>Note:</b><br>"
|
||||
"<ul>"
|
||||
"<li>When using itlwm kext, WiFi appears as Ethernet in macOS</li>"
|
||||
"<li>You'll need Heliport app to manage WiFi connections in macOS</li>"
|
||||
"<li>This step will enable auto WiFi connections at boot time<br>"
|
||||
"and is useful for users installing macOS via Recovery OS</li>"
|
||||
"</ul><br>"
|
||||
"Would you like to scan for WiFi profiles?"
|
||||
)
|
||||
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 = []
|
||||
self.utils.head("Detecting WiFi Profiles")
|
||||
print("")
|
||||
print("Scanning for WiFi profiles...")
|
||||
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Detecting WiFi Profiles", level="INFO", to_build_log=True)
|
||||
|
||||
if os_name == "Windows":
|
||||
profiles = self.get_preferred_networks_windows()
|
||||
@@ -321,31 +291,17 @@ class WifiProfileExtractor:
|
||||
|
||||
if 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)
|
||||
if interface_profiles:
|
||||
profiles = interface_profiles
|
||||
break
|
||||
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:
|
||||
self.utils.head("WiFi Profile Extractor")
|
||||
print("")
|
||||
print("No WiFi profiles with saved passwords were found.")
|
||||
self.utils.request_input()
|
||||
self.utils.log_message("[WIFI PROFILE EXTRACTOR] No WiFi profiles with saved passwords were found.", level="INFO", to_build_log=True)
|
||||
|
||||
self.utils.head("WiFi Profile Extractor")
|
||||
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()
|
||||
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Successfully applied {} WiFi profiles".format(len(profiles)), level="INFO", to_build_log=True)
|
||||
|
||||
return profiles
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
certifi
|
||||
PyQt6
|
||||
pyqt6-sip
|
||||
PyQt6-Fluent-Widgets
|
||||
367
updater.py
367
updater.py
@@ -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 github
|
||||
from Scripts import run
|
||||
from Scripts import utils
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
from Scripts import integrity_checker
|
||||
from Scripts.custom_dialogs import show_update_dialog, show_info, show_confirmation
|
||||
|
||||
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:
|
||||
def __init__(self):
|
||||
self.github = github.Github()
|
||||
self.fetcher = resource_fetcher.ResourceFetcher()
|
||||
self.run = run.Run().run
|
||||
self.utils = utils.Utils()
|
||||
self.sha_version = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sha_version.txt")
|
||||
def __init__(self, utils_instance=None, github_instance=None, resource_fetcher_instance=None, run_instance=None, integrity_checker_instance=None):
|
||||
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||
self.github = github_instance if github_instance else github.Github(utils_instance=self.utils)
|
||||
self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher(utils_instance=self.utils)
|
||||
self.run = run_instance.run if run_instance else run.Run().run
|
||||
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.temporary_dir = tempfile.mkdtemp()
|
||||
self.current_step = 0
|
||||
self.root_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
def get_current_sha_version(self):
|
||||
print("Checking current version...")
|
||||
try:
|
||||
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)))
|
||||
def get_remote_manifest(self, dialog=None):
|
||||
if dialog:
|
||||
dialog.update_progress(10, "Fetching remote manifest...")
|
||||
|
||||
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.current_step += 1
|
||||
print("")
|
||||
print("Step {}: Creating temporary directory...".format(self.current_step))
|
||||
self.utils.extract_zip_file(temp_manifest_zip_path, self.temporary_dir)
|
||||
|
||||
remote_manifest_path = os.path.join(self.temporary_dir, "manifest.json")
|
||||
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:
|
||||
self.utils.create_folder(self.temporary_dir)
|
||||
print(" Temporary directory created.")
|
||||
|
||||
self.current_step += 1
|
||||
print("Step {}: Downloading update package...".format(self.current_step))
|
||||
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 dialog:
|
||||
dialog.update_progress(65, "Downloading update package...")
|
||||
|
||||
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
|
||||
print(" Update package downloaded ({:.1f} KB)".format(os.path.getsize(file_path)/1024))
|
||||
|
||||
self.current_step += 1
|
||||
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")
|
||||
file_path = os.path.join(self.temporary_dir, "update.zip")
|
||||
success = self.fetcher.download_and_save_file(self.download_repo_url, file_path)
|
||||
|
||||
if not success or not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
|
||||
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:
|
||||
print(" Error during download/extraction: {}".format(str(e)))
|
||||
self.utils.log_message("[UPDATER] Error during download/extraction: {}".format(str(e)), level="ERROR")
|
||||
return False
|
||||
|
||||
def update_files(self):
|
||||
self.current_step += 1
|
||||
print("Step {}: Updating files...".format(self.current_step))
|
||||
|
||||
def update_files(self, files_to_update, dialog=None):
|
||||
if not files_to_update:
|
||||
return True
|
||||
|
||||
try:
|
||||
target_dir = os.path.join(self.temporary_dir, "OpCore-Simplify-main")
|
||||
|
||||
if not os.path.exists(target_dir):
|
||||
target_dir = os.path.join(self.temporary_dir, "main", "OpCore-Simplify-main")
|
||||
|
||||
if not os.path.exists(target_dir):
|
||||
print(" Could not locate extracted files directory")
|
||||
self.utils.log_message("[UPDATER] Target directory not found: {}".format(target_dir), level="ERROR")
|
||||
return False
|
||||
|
||||
file_paths = self.utils.find_matching_paths(target_dir, type_filter="file")
|
||||
|
||||
total_files = len(file_paths)
|
||||
print(" Found {} files to update".format(total_files))
|
||||
all_files = files_to_update["modified"] + files_to_update["missing"]
|
||||
total_files = len(all_files)
|
||||
|
||||
if dialog:
|
||||
dialog.update_progress(85, "Updating {} files...".format(total_files))
|
||||
|
||||
updated_count = 0
|
||||
for index, (path, type) in enumerate(file_paths, start=1):
|
||||
source = os.path.join(target_dir, path)
|
||||
destination = source.replace(target_dir, os.path.dirname(os.path.realpath(__file__)))
|
||||
for index, relative_path in enumerate(all_files, start=1):
|
||||
source = os.path.join(target_dir, relative_path)
|
||||
|
||||
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))
|
||||
|
||||
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:
|
||||
shutil.move(source, destination)
|
||||
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({
|
||||
"args": ["chmod", "+x", destination]
|
||||
})
|
||||
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("")
|
||||
print(" Successfully updated {}/{} files".format(updated_count, total_files))
|
||||
if dialog:
|
||||
dialog.update_progress(95, "Successfully updated {}/{} files".format(updated_count, total_files))
|
||||
|
||||
self.current_step += 1
|
||||
print("Step {}: Cleaning up temporary files...".format(self.current_step))
|
||||
shutil.rmtree(self.temporary_dir)
|
||||
print(" Cleanup complete")
|
||||
if os.path.exists(self.temporary_dir):
|
||||
shutil.rmtree(self.temporary_dir)
|
||||
|
||||
if dialog:
|
||||
dialog.update_progress(100, "Update completed!")
|
||||
|
||||
return True
|
||||
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
|
||||
|
||||
def save_latest_sha_version(self, latest_sha):
|
||||
try:
|
||||
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("")
|
||||
|
||||
def run_update(self):
|
||||
checker_thread = UpdateCheckerThread(self)
|
||||
|
||||
current_sha_version = self.get_current_sha_version()
|
||||
latest_sha_version = self.get_latest_sha_version()
|
||||
|
||||
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("")
|
||||
def on_update_available(files_to_update):
|
||||
checker_thread.quit()
|
||||
checker_thread.wait()
|
||||
|
||||
while True:
|
||||
user_input = self.utils.request_input("Do you want to skip the update process? (yes/No): ").strip().lower()
|
||||
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 show_confirmation("An update is available!", "Would you like to update now?", yes_text="Update", no_text="Later"):
|
||||
return False
|
||||
|
||||
if not self.download_update():
|
||||
print("")
|
||||
print(" Update failed: Could not download or extract update package")
|
||||
|
||||
dialog = show_update_dialog("Updating", "Starting update process...")
|
||||
dialog.show()
|
||||
|
||||
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):
|
||||
self.current_step += 1
|
||||
print("Step {}: Cleaning up temporary files...".format(self.current_step))
|
||||
shutil.rmtree(self.temporary_dir)
|
||||
print(" Cleanup complete")
|
||||
|
||||
return False
|
||||
|
||||
if not self.update_files():
|
||||
print("")
|
||||
print(" Update failed: Could not update files")
|
||||
return False
|
||||
|
||||
if not self.save_latest_sha_version(latest_sha_version):
|
||||
print("")
|
||||
print(" Update completed but version information could not be saved")
|
||||
|
||||
print("")
|
||||
print("Update completed successfully!")
|
||||
print("")
|
||||
print("The program needs to restart to complete the update process.")
|
||||
return True
|
||||
else:
|
||||
print("You are already using the latest version")
|
||||
return False
|
||||
try:
|
||||
shutil.rmtree(self.temporary_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def on_check_failed(error_message):
|
||||
checker_thread.quit()
|
||||
checker_thread.wait()
|
||||
show_info("Update Check Failed", error_message)
|
||||
|
||||
def on_no_update():
|
||||
checker_thread.quit()
|
||||
checker_thread.wait()
|
||||
|
||||
checker_thread.update_available.connect(on_update_available)
|
||||
checker_thread.check_failed.connect(on_check_failed)
|
||||
checker_thread.no_update.connect(on_no_update)
|
||||
|
||||
checker_thread.start()
|
||||
Reference in New Issue
Block a user