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

|

|
||||||
|
|
||||||
3. **Selecting hardware report**:
|
3. **Selecting hardware report**:
|
||||||
- On Windows, there will be an option for `E. Export hardware report`. It's recommended to use this for the best results with your hardware configuration and BIOS at the time of building.
|
|
||||||
- Alternatively, use [**Hardware Sniffer**](https://github.com/lzhoang2801/Hardware-Sniffer) to create a `Report.json` and ACPI dump for configuration manully.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|
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.
|
- By default, the latest compatible macOS version will be selected for your hardware.
|
||||||
- OpCore Simplify will automatically apply essential ACPI patches and kexts.
|
- OpCore Simplify will automatically apply essential ACPI patches and kexts.
|
||||||
- You can manually review and customize these settings as needed.
|
- You can manually review and customize these settings as needed.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
5. **Building OpenCore EFI**:
|
6. **Building OpenCore EFI**:
|
||||||
- Once you've customized all options, select **Build OpenCore EFI** to generate your EFI.
|
- Once you've customized all options, select **Build OpenCore EFI** to generate your EFI.
|
||||||
- The tool will automatically download the necessary bootloader and kexts, which may take a few minutes.
|
- The tool will automatically download the necessary bootloader and kexts, which may take a few minutes.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

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

|
|
||||||
|
|
||||||
7. **Create USB and Install macOS**:
|
7. **Create USB and Install macOS**:
|
||||||
- Use [**UnPlugged**](https://github.com/corpnewt/UnPlugged) on Windows to create a USB macOS installer, or follow [this guide](https://dortania.github.io/OpenCore-Install-Guide/installer-guide/mac-install.html) for macOS.
|
- Use [**UnPlugged**](https://github.com/corpnewt/UnPlugged) on Windows to create a USB macOS installer, or follow [this guide](https://dortania.github.io/OpenCore-Install-Guide/installer-guide/mac-install.html) for macOS.
|
||||||
@@ -158,6 +132,7 @@ Distributed under the BSD 3-Clause License. See `LICENSE` for more information.
|
|||||||
|
|
||||||
- [OpenCorePkg](https://github.com/acidanthera/OpenCorePkg) and [kexts](https://github.com/lzhoang2801/OpCore-Simplify/blob/main/Scripts/datasets/kext_data.py) – The backbone of this project.
|
- [OpenCorePkg](https://github.com/acidanthera/OpenCorePkg) and [kexts](https://github.com/lzhoang2801/OpCore-Simplify/blob/main/Scripts/datasets/kext_data.py) – The backbone of this project.
|
||||||
- [SSDTTime](https://github.com/corpnewt/SSDTTime) – SSDT patching utilities.
|
- [SSDTTime](https://github.com/corpnewt/SSDTTime) – SSDT patching utilities.
|
||||||
|
- [@rubentalstra](https://github.com/rubentalstra): Idea and code prototype [Implement GUI #471](https://github.com/lzhoang2801/OpCore-Simplify/pull/471)
|
||||||
|
|
||||||
## 📞 **Contact**
|
## 📞 **Contact**
|
||||||
|
|
||||||
@@ -168,4 +143,4 @@ Distributed under the BSD 3-Clause License. See `LICENSE` for more information.
|
|||||||
|
|
||||||
## 🌟 **Star History**
|
## 🌟 **Star History**
|
||||||
|
|
||||||
[](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 dsdt
|
||||||
from Scripts import run
|
from Scripts import run
|
||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
|
from Scripts.custom_dialogs import show_checklist_dialog
|
||||||
import os
|
import os
|
||||||
import binascii
|
import binascii
|
||||||
import re
|
import re
|
||||||
@@ -17,11 +18,11 @@ import sys
|
|||||||
import plistlib
|
import plistlib
|
||||||
|
|
||||||
class ACPIGuru:
|
class ACPIGuru:
|
||||||
def __init__(self):
|
def __init__(self, dsdt_instance=None, smbios_instance=None, run_instance=None, utils_instance=None):
|
||||||
self.acpi = dsdt.DSDT()
|
self.acpi = dsdt_instance if dsdt_instance else dsdt.DSDT()
|
||||||
self.smbios = smbios.SMBIOS()
|
self.smbios = smbios_instance if smbios_instance else smbios.SMBIOS()
|
||||||
self.run = run.Run().run
|
self.run = run_instance.run if run_instance else run.Run().run
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
self.patches = acpi_patch_data.patches
|
self.patches = acpi_patch_data.patches
|
||||||
self.hardware_report = None
|
self.hardware_report = None
|
||||||
self.disabled_devices = None
|
self.disabled_devices = None
|
||||||
@@ -118,9 +119,7 @@ class ACPIGuru:
|
|||||||
def read_acpi_tables(self, path):
|
def read_acpi_tables(self, path):
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
self.utils.head("Loading ACPI Table(s)")
|
self.utils.log_message("[ACPI GURU] Loading ACPI Table(s) from {}".format(path), level="INFO")
|
||||||
print("by CorpNewt")
|
|
||||||
print("")
|
|
||||||
tables = []
|
tables = []
|
||||||
trouble_dsdt = None
|
trouble_dsdt = None
|
||||||
fixed = False
|
fixed = False
|
||||||
@@ -129,10 +128,10 @@ class ACPIGuru:
|
|||||||
# Clear any existing tables so we load anew
|
# Clear any existing tables so we load anew
|
||||||
self.acpi.acpi_tables = {}
|
self.acpi.acpi_tables = {}
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
print("Gathering valid tables from {}...\n".format(os.path.basename(path)))
|
self.utils.log_message("[ACPI GURU] Gathering valid tables from {}".format(os.path.basename(path)), level="INFO")
|
||||||
for t in self.sorted_nicely(os.listdir(path)):
|
for t in self.sorted_nicely(os.listdir(path)):
|
||||||
if not "Patched" in t and self.acpi.table_is_valid(path,t):
|
if not "Patched" in t and self.acpi.table_is_valid(path,t):
|
||||||
print(" - {}".format(t))
|
self.utils.log_message("[ACPI GURU] Found valid table: {}".format(t), level="INFO")
|
||||||
tables.append(t)
|
tables.append(t)
|
||||||
if not tables:
|
if not tables:
|
||||||
# Check if there's an ACPI directory within the passed
|
# Check if there's an ACPI directory within the passed
|
||||||
@@ -140,50 +139,44 @@ class ACPIGuru:
|
|||||||
if os.path.isdir(os.path.join(path,"ACPI")):
|
if os.path.isdir(os.path.join(path,"ACPI")):
|
||||||
# Rerun this function with that updated path
|
# Rerun this function with that updated path
|
||||||
return self.read_acpi_tables(os.path.join(path,"ACPI"))
|
return self.read_acpi_tables(os.path.join(path,"ACPI"))
|
||||||
print(" - No valid .aml files were found!")
|
self.utils.log_message("[ACPI GURU] No valid .aml files were found!", level="ERROR")
|
||||||
print("")
|
|
||||||
#self.u.grab("Press [enter] to return...")
|
#self.u.grab("Press [enter] to return...")
|
||||||
self.utils.request_input()
|
|
||||||
# Restore any prior tables
|
# Restore any prior tables
|
||||||
self.acpi.acpi_tables = prior_tables
|
self.acpi.acpi_tables = prior_tables
|
||||||
return
|
return
|
||||||
print("")
|
self.utils.log_message("[ACPI GURU] Found at least one valid table", level="INFO")
|
||||||
# We got at least one file - let's look for the DSDT specifically
|
# We got at least one file - let's look for the DSDT specifically
|
||||||
# and try to load that as-is. If it doesn't load, we'll have to
|
# and try to load that as-is. If it doesn't load, we'll have to
|
||||||
# manage everything with temp folders
|
# manage everything with temp folders
|
||||||
dsdt_list = [x for x in tables if self.acpi._table_signature(path,x) == "DSDT"]
|
dsdt_list = [x for x in tables if self.acpi._table_signature(path,x) == "DSDT"]
|
||||||
if len(dsdt_list) > 1:
|
if len(dsdt_list) > 1:
|
||||||
print("Multiple files with DSDT signature passed:")
|
self.utils.log_message("[ACPI GURU] Multiple files with DSDT signature passed:", level="ERROR")
|
||||||
for d in self.sorted_nicely(dsdt_list):
|
for d in self.sorted_nicely(dsdt_list):
|
||||||
print(" - {}".format(d))
|
self.utils.log_message("[ACPI GURU] Found DSDT file: {}".format(d), level="INFO")
|
||||||
print("\nOnly one is allowed at a time. Please remove one of the above and try again.")
|
self.utils.log_message("[ACPI GURU] Only one is allowed at a time. Please remove one of the above and try again.", level="ERROR")
|
||||||
print("")
|
|
||||||
#self.u.grab("Press [enter] to return...")
|
#self.u.grab("Press [enter] to return...")
|
||||||
self.utils.request_input()
|
|
||||||
# Restore any prior tables
|
# Restore any prior tables
|
||||||
self.acpi.acpi_tables = prior_tables
|
self.acpi.acpi_tables = prior_tables
|
||||||
return
|
return
|
||||||
# Get the DSDT, if any
|
# Get the DSDT, if any
|
||||||
dsdt = dsdt_list[0] if len(dsdt_list) else None
|
dsdt = dsdt_list[0] if len(dsdt_list) else None
|
||||||
if dsdt: # Try to load it and see if it causes problems
|
if dsdt: # Try to load it and see if it causes problems
|
||||||
print("Disassembling {} to verify if pre-patches are needed...".format(dsdt))
|
self.utils.log_message("[ACPI GURU] Disassembling {} to verify if pre-patches are needed...".format(dsdt), level="INFO")
|
||||||
if not self.acpi.load(os.path.join(path,dsdt))[0]:
|
if not self.acpi.load(os.path.join(path,dsdt))[0]:
|
||||||
trouble_dsdt = dsdt
|
trouble_dsdt = dsdt
|
||||||
else:
|
else:
|
||||||
print("\nDisassembled successfully!\n")
|
self.utils.log_message("[ACPI GURU] Disassembled successfully!", level="INFO")
|
||||||
elif not "Patched" in path and os.path.isfile(path):
|
elif not "Patched" in path and os.path.isfile(path):
|
||||||
print("Loading {}...".format(os.path.basename(path)))
|
self.utils.log_message("[ACPI GURU] Loading {}...".format(os.path.basename(path)), level="INFO")
|
||||||
if self.acpi.load(path)[0]:
|
if self.acpi.load(path)[0]:
|
||||||
print("\nDone.")
|
self.utils.log_message("[ACPI GURU] Done.", level="INFO")
|
||||||
# If it loads fine - just return the path
|
# If it loads fine - just return the path
|
||||||
# to the parent directory
|
# to the parent directory
|
||||||
return os.path.dirname(path)
|
return os.path.dirname(path)
|
||||||
if not self.acpi._table_signature(path) == "DSDT":
|
if not self.acpi._table_signature(path) == "DSDT":
|
||||||
# Not a DSDT, we aren't applying pre-patches
|
# Not a DSDT, we aren't applying pre-patches
|
||||||
print("\n{} could not be disassembled!".format(os.path.basename(path)))
|
self.utils.log_message("[ACPI GURU] {} could not be disassembled!".format(os.path.basename(path)), level="ERROR")
|
||||||
print("")
|
|
||||||
#self.u.grab("Press [enter] to return...")
|
#self.u.grab("Press [enter] to return...")
|
||||||
self.utils.request_input()
|
|
||||||
# Restore any prior tables
|
# Restore any prior tables
|
||||||
self.acpi.acpi_tables = prior_tables
|
self.acpi.acpi_tables = prior_tables
|
||||||
return
|
return
|
||||||
@@ -194,10 +187,8 @@ class ACPIGuru:
|
|||||||
tables.append(os.path.basename(path))
|
tables.append(os.path.basename(path))
|
||||||
path = os.path.dirname(path)
|
path = os.path.dirname(path)
|
||||||
else:
|
else:
|
||||||
print("Passed file/folder does not exist!")
|
self.utils.log_message("[ACPI GURU] Passed file/folder does not exist!", level="ERROR")
|
||||||
print("")
|
|
||||||
#self.u.grab("Press [enter] to return...")
|
#self.u.grab("Press [enter] to return...")
|
||||||
self.utils.request_input()
|
|
||||||
# Restore any prior tables
|
# Restore any prior tables
|
||||||
self.acpi.acpi_tables = prior_tables
|
self.acpi.acpi_tables = prior_tables
|
||||||
return
|
return
|
||||||
@@ -214,22 +205,22 @@ class ACPIGuru:
|
|||||||
# Get a reference to the new trouble file
|
# Get a reference to the new trouble file
|
||||||
trouble_path = os.path.join(temp,trouble_dsdt)
|
trouble_path = os.path.join(temp,trouble_dsdt)
|
||||||
# Now we try patching it
|
# Now we try patching it
|
||||||
print("Checking available pre-patches...")
|
self.utils.log_message("[ACPI GURU] Checking available pre-patches...", level="INFO")
|
||||||
print("Loading {} into memory...".format(trouble_dsdt))
|
self.utils.log_message("[ACPI GURU] Loading {} into memory...".format(trouble_dsdt), level="INFO")
|
||||||
with open(trouble_path,"rb") as f:
|
with open(trouble_path,"rb") as f:
|
||||||
d = f.read()
|
d = f.read()
|
||||||
res = self.acpi.check_output(path)
|
res = self.acpi.check_output(path)
|
||||||
target_name = self.get_unique_name(trouble_dsdt,res,name_append="-Patched")
|
target_name = self.get_unique_name(trouble_dsdt,res,name_append="-Patched")
|
||||||
self.dsdt_patches = []
|
self.dsdt_patches = []
|
||||||
print("Iterating patches...\n")
|
self.utils.log_message("[ACPI GURU] Iterating patches...", level="INFO")
|
||||||
for p in self.pre_patches:
|
for p in self.pre_patches:
|
||||||
if not all(x in p for x in ("PrePatch","Comment","Find","Replace")): continue
|
if not all(x in p for x in ("PrePatch","Comment","Find","Replace")): continue
|
||||||
print(" - {}".format(p["PrePatch"]))
|
self.utils.log_message("[ACPI GURU] Found pre-patch: {}".format(p["PrePatch"]), level="INFO")
|
||||||
find = binascii.unhexlify(p["Find"])
|
find = binascii.unhexlify(p["Find"])
|
||||||
if d.count(find) == 1:
|
if d.count(find) == 1:
|
||||||
self.dsdt_patches.append(p) # Retain the patch
|
self.dsdt_patches.append(p) # Retain the patch
|
||||||
repl = binascii.unhexlify(p["Replace"])
|
repl = binascii.unhexlify(p["Replace"])
|
||||||
print(" --> Located - applying...")
|
self.utils.log_message("[ACPI GURU] Located pre-patch - applying...", level="INFO")
|
||||||
d = d.replace(find,repl) # Replace it in memory
|
d = d.replace(find,repl) # Replace it in memory
|
||||||
with open(trouble_path,"wb") as f:
|
with open(trouble_path,"wb") as f:
|
||||||
f.write(d) # Write the updated file
|
f.write(d) # Write the updated file
|
||||||
@@ -237,7 +228,7 @@ class ACPIGuru:
|
|||||||
if self.acpi.load(trouble_path)[0]:
|
if self.acpi.load(trouble_path)[0]:
|
||||||
fixed = True
|
fixed = True
|
||||||
# We got it to load - let's write the patches
|
# We got it to load - let's write the patches
|
||||||
print("\nDisassembled successfully!\n")
|
self.utils.log_message("[ACPI GURU] Disassembled successfully!", level="INFO")
|
||||||
#self.make_plist(None, None, patches)
|
#self.make_plist(None, None, patches)
|
||||||
# Save to the local file
|
# Save to the local file
|
||||||
#with open(os.path.join(res,target_name),"wb") as f:
|
#with open(os.path.join(res,target_name),"wb") as f:
|
||||||
@@ -246,10 +237,8 @@ class ACPIGuru:
|
|||||||
#self.patch_warn()
|
#self.patch_warn()
|
||||||
break
|
break
|
||||||
if not fixed:
|
if not fixed:
|
||||||
print("\n{} could not be disassembled!".format(trouble_dsdt))
|
self.utils.log_message("[ACPI GURU] {} could not be disassembled!".format(trouble_dsdt), level="ERROR")
|
||||||
print("")
|
|
||||||
#self.u.grab("Press [enter] to return...")
|
#self.u.grab("Press [enter] to return...")
|
||||||
self.utils.request_input()
|
|
||||||
if temp:
|
if temp:
|
||||||
shutil.rmtree(temp,ignore_errors=True)
|
shutil.rmtree(temp,ignore_errors=True)
|
||||||
# Restore any prior tables
|
# Restore any prior tables
|
||||||
@@ -257,26 +246,26 @@ class ACPIGuru:
|
|||||||
return
|
return
|
||||||
# Let's load the rest of the tables
|
# Let's load the rest of the tables
|
||||||
if len(tables) > 1:
|
if len(tables) > 1:
|
||||||
print("Loading valid tables in {}...".format(path))
|
self.utils.log_message("[ACPI GURU] Loading valid tables in {}...".format(path), level="INFO")
|
||||||
loaded_tables,failed = self.acpi.load(temp or path)
|
loaded_tables,failed = self.acpi.load(temp or path)
|
||||||
if not loaded_tables or failed:
|
if not loaded_tables or failed:
|
||||||
print("\nFailed to load tables in {}{}\n".format(
|
self.utils.log_message("[ACPI GURU] Failed to load tables in {}{}\n".format(
|
||||||
os.path.dirname(path) if os.path.isfile(path) else path,
|
os.path.dirname(path) if os.path.isfile(path) else path,
|
||||||
":" if failed else ""
|
":" if failed else ""
|
||||||
))
|
))
|
||||||
for t in self.sorted_nicely(failed):
|
for t in self.sorted_nicely(failed):
|
||||||
print(" - {}".format(t))
|
self.utils.log_message("[ACPI GURU] Failed to load table: {}".format(t), level="ERROR")
|
||||||
# Restore any prior tables
|
# Restore any prior tables
|
||||||
if not loaded_tables:
|
if not loaded_tables:
|
||||||
self.acpi.acpi_tables = prior_tables
|
self.acpi.acpi_tables = prior_tables
|
||||||
else:
|
else:
|
||||||
if len(tables) > 1:
|
#if len(tables) > 1:
|
||||||
print("") # Newline for readability
|
# print("") # Newline for readability
|
||||||
print("Done.")
|
self.utils.log_message("[ACPI GURU] Done.", level="INFO")
|
||||||
# If we had to patch the DSDT, or if not all tables loaded,
|
# If we had to patch the DSDT, or if not all tables loaded,
|
||||||
# make sure we get interaction from the user to continue
|
# make sure we get interaction from the user to continue
|
||||||
if trouble_dsdt or not loaded_tables or failed:
|
if trouble_dsdt or not loaded_tables or failed:
|
||||||
print("")
|
pass
|
||||||
#self.u.grab("Press [enter] to return...")
|
#self.u.grab("Press [enter] to return...")
|
||||||
#self.utils.request_input()
|
#self.utils.request_input()
|
||||||
if temp:
|
if temp:
|
||||||
@@ -293,7 +282,7 @@ class ACPIGuru:
|
|||||||
# Got it already
|
# Got it already
|
||||||
return True
|
return True
|
||||||
# Need to prompt
|
# Need to prompt
|
||||||
self.select_acpi_tables()
|
#self.select_acpi_tables()
|
||||||
self.dsdt = self.acpi.get_dsdt_or_only()
|
self.dsdt = self.acpi.get_dsdt_or_only()
|
||||||
if self._ensure_dsdt(allow_any=allow_any):
|
if self._ensure_dsdt(allow_any=allow_any):
|
||||||
return True
|
return True
|
||||||
@@ -3214,20 +3203,6 @@ DefinitionBlock ("", "SSDT", 2, "ZPSS", "WMIS", 0x00000000)
|
|||||||
"Delete": deletes
|
"Delete": deletes
|
||||||
}
|
}
|
||||||
|
|
||||||
def select_acpi_tables(self):
|
|
||||||
while True:
|
|
||||||
self.utils.head("Select ACPI Tables")
|
|
||||||
print("")
|
|
||||||
print("Q. Quit")
|
|
||||||
print(" ")
|
|
||||||
menu = self.utils.request_input("Please drag and drop ACPI Tables folder here: ")
|
|
||||||
if menu.lower() == "q":
|
|
||||||
self.utils.exit_program()
|
|
||||||
path = self.utils.normalize_path(menu)
|
|
||||||
if not path:
|
|
||||||
continue
|
|
||||||
return self.read_acpi_tables(path)
|
|
||||||
|
|
||||||
def get_patch_index(self, name):
|
def get_patch_index(self, name):
|
||||||
for index, patch in enumerate(self.patches):
|
for index, patch in enumerate(self.patches):
|
||||||
if patch.name == name:
|
if patch.name == name:
|
||||||
@@ -3235,6 +3210,7 @@ DefinitionBlock ("", "SSDT", 2, "ZPSS", "WMIS", 0x00000000)
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def select_acpi_patches(self, hardware_report, disabled_devices):
|
def select_acpi_patches(self, hardware_report, disabled_devices):
|
||||||
|
self.utils.log_message("[ACPI GURU] Selecting ACPI patches...", level="INFO")
|
||||||
selected_patches = []
|
selected_patches = []
|
||||||
|
|
||||||
if "Laptop" in hardware_report.get("Motherboard").get("Platform") and \
|
if "Laptop" in hardware_report.get("Motherboard").get("Platform") and \
|
||||||
@@ -3315,42 +3291,22 @@ DefinitionBlock ("", "SSDT", 2, "ZPSS", "WMIS", 0x00000000)
|
|||||||
if device_info.get("Bus Type") == "ACPI" and device_info.get("Device") in pci_data.YogaHIDs:
|
if device_info.get("Bus Type") == "ACPI" and device_info.get("Device") in pci_data.YogaHIDs:
|
||||||
selected_patches.append("WMIS")
|
selected_patches.append("WMIS")
|
||||||
|
|
||||||
|
self.utils.log_message("[ACPI GURU] Selected patches: {}".format(", ".join(selected_patches)), level="INFO")
|
||||||
for patch in self.patches:
|
for patch in self.patches:
|
||||||
patch.checked = patch.name in selected_patches
|
patch.checked = patch.name in selected_patches
|
||||||
|
|
||||||
def customize_patch_selection(self):
|
def customize_patch_selection(self):
|
||||||
while True:
|
items = []
|
||||||
contents = []
|
checked_indices = []
|
||||||
contents.append("")
|
|
||||||
contents.append("List of available patches:")
|
for i, patch in enumerate(self.patches):
|
||||||
contents.append("")
|
label = f"{patch.name} - {patch.description}"
|
||||||
for index, kext in enumerate(self.patches, start=1):
|
items.append(label)
|
||||||
checkbox = "[*]" if kext.checked else "[ ]"
|
if patch.checked:
|
||||||
|
checked_indices.append(i)
|
||||||
|
|
||||||
line = "{} {:2}. {:15} - {:60}".format(checkbox, index, kext.name, kext.description)
|
result = show_checklist_dialog("Configure ACPI Patches", "Select ACPI patches you want to apply:", items, checked_indices)
|
||||||
if kext.checked:
|
|
||||||
line = "\033[1;32m{}\033[0m".format(line)
|
if result is not None:
|
||||||
contents.append(line)
|
for i, patch in enumerate(self.patches):
|
||||||
contents.append("")
|
patch.checked = i in result
|
||||||
contents.append("\033[1;93mNote:\033[0m You can select multiple kexts by entering their indices separated by commas (e.g., '1, 2, 3').")
|
|
||||||
contents.append("")
|
|
||||||
contents.append("B. Back")
|
|
||||||
contents.append("Q. Quit")
|
|
||||||
contents.append("")
|
|
||||||
content = "\n".join(contents)
|
|
||||||
|
|
||||||
self.utils.adjust_window_size(content)
|
|
||||||
self.utils.head("Customize ACPI Patch Selections", resize=False)
|
|
||||||
print(content)
|
|
||||||
option = self.utils.request_input("Select your option: ")
|
|
||||||
if option.lower() == "q":
|
|
||||||
self.utils.exit_program()
|
|
||||||
if option.lower() == "b":
|
|
||||||
return
|
|
||||||
|
|
||||||
indices = [int(i.strip()) -1 for i in option.split(",") if i.strip().isdigit()]
|
|
||||||
|
|
||||||
for index in indices:
|
|
||||||
if index >= 0 and index < len(self.patches):
|
|
||||||
patch = self.patches[index]
|
|
||||||
patch.checked = not patch.checked
|
|
||||||
140
Scripts/backend.py
Normal file
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 pci_data
|
||||||
from Scripts.datasets import codec_layouts
|
from Scripts.datasets import codec_layouts
|
||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
import time
|
from Scripts import settings
|
||||||
|
|
||||||
class CompatibilityChecker:
|
class CompatibilityChecker:
|
||||||
def __init__(self):
|
def __init__(self, utils_instance=None, settings_instance=None):
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
|
self.settings = settings_instance if settings_instance else settings.Settings()
|
||||||
|
self.error_codes = []
|
||||||
|
|
||||||
def show_macos_compatibility(self, device_compatibility):
|
|
||||||
if not device_compatibility:
|
|
||||||
return "\033[90mUnchecked\033[0m"
|
|
||||||
|
|
||||||
if not device_compatibility[0]:
|
|
||||||
return "\033[0;31mUnsupported\033[0m"
|
|
||||||
|
|
||||||
max_compatibility = self.utils.parse_darwin_version(device_compatibility[0])[0]
|
|
||||||
min_compatibility = self.utils.parse_darwin_version(device_compatibility[-1])[0]
|
|
||||||
max_version = self.utils.parse_darwin_version(os_data.get_latest_darwin_version())[0]
|
|
||||||
min_version = self.utils.parse_darwin_version(os_data.get_lowest_darwin_version())[0]
|
|
||||||
|
|
||||||
if max_compatibility == min_version:
|
|
||||||
return "\033[1;36mMaximum support up to {}\033[0m".format(
|
|
||||||
os_data.get_macos_name_by_darwin(device_compatibility[-1])
|
|
||||||
)
|
|
||||||
|
|
||||||
if min_version < min_compatibility or max_compatibility < max_version:
|
|
||||||
return "\033[1;32m{} to {}\033[0m".format(
|
|
||||||
os_data.get_macos_name_by_darwin(device_compatibility[-1]),
|
|
||||||
os_data.get_macos_name_by_darwin(device_compatibility[0])
|
|
||||||
)
|
|
||||||
|
|
||||||
return "\033[1;36mUp to {}\033[0m".format(
|
|
||||||
os_data.get_macos_name_by_darwin(device_compatibility[0])
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_low_end_intel_cpu(self, processor_name):
|
def is_low_end_intel_cpu(self, processor_name):
|
||||||
return any(cpu_branding in processor_name for cpu_branding in ("Celeron", "Pentium"))
|
return any(cpu_branding in processor_name for cpu_branding in ("Celeron", "Pentium"))
|
||||||
|
|
||||||
@@ -53,29 +28,14 @@ class CompatibilityChecker:
|
|||||||
|
|
||||||
self.hardware_report["CPU"]["Compatibility"] = (max_version, min_version)
|
self.hardware_report["CPU"]["Compatibility"] = (max_version, min_version)
|
||||||
|
|
||||||
print("{}- {}: {}".format(" "*3, self.hardware_report.get("CPU").get("Processor Name"), self.show_macos_compatibility(self.hardware_report["CPU"].get("Compatibility"))))
|
|
||||||
|
|
||||||
if max_version == min_version and max_version == None:
|
if max_version == min_version and max_version == None:
|
||||||
print("")
|
self.error_codes.append("ERROR_MISSING_SSE4")
|
||||||
print("Missing required SSE4.x instruction set.")
|
return
|
||||||
print("Your CPU is not supported by macOS versions newer than Sierra (10.12).")
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
self.utils.exit_program()
|
|
||||||
|
|
||||||
self.max_native_macos_version = max_version
|
self.max_native_macos_version = max_version
|
||||||
self.min_native_macos_version = min_version
|
self.min_native_macos_version = min_version
|
||||||
|
|
||||||
def check_gpu_compatibility(self):
|
def check_gpu_compatibility(self):
|
||||||
if not self.hardware_report.get("GPU"):
|
|
||||||
print("")
|
|
||||||
print("No GPU found!")
|
|
||||||
print("Please make sure to export the hardware report with the GPU information")
|
|
||||||
print("and try again.")
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
self.utils.exit_program()
|
|
||||||
|
|
||||||
for gpu_name, gpu_props in self.hardware_report["GPU"].items():
|
for gpu_name, gpu_props in self.hardware_report["GPU"].items():
|
||||||
gpu_manufacturer = gpu_props.get("Manufacturer")
|
gpu_manufacturer = gpu_props.get("Manufacturer")
|
||||||
gpu_codename = gpu_props.get("Codename")
|
gpu_codename = gpu_props.get("Codename")
|
||||||
@@ -155,21 +115,6 @@ class CompatibilityChecker:
|
|||||||
if self.utils.parse_darwin_version(max_version) < self.utils.parse_darwin_version(ocl_patched_max_version):
|
if self.utils.parse_darwin_version(max_version) < self.utils.parse_darwin_version(ocl_patched_max_version):
|
||||||
gpu_props["OCLP Compatibility"] = (ocl_patched_max_version, ocl_patched_min_version if self.utils.parse_darwin_version(ocl_patched_min_version) > self.utils.parse_darwin_version("{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0)) else "{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0))
|
gpu_props["OCLP Compatibility"] = (ocl_patched_max_version, ocl_patched_min_version if self.utils.parse_darwin_version(ocl_patched_min_version) > self.utils.parse_darwin_version("{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0)) else "{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0))
|
||||||
|
|
||||||
print("{}- {}: {}".format(" "*3, gpu_name, self.show_macos_compatibility(gpu_props.get("Compatibility"))))
|
|
||||||
|
|
||||||
if "OCLP Compatibility" in gpu_props:
|
|
||||||
print("{}- OCLP Compatibility: {}".format(" "*6, self.show_macos_compatibility(gpu_props.get("OCLP Compatibility"))))
|
|
||||||
|
|
||||||
connected_monitors = []
|
|
||||||
for monitor_name, monitor_info in self.hardware_report.get("Monitor", {}).items():
|
|
||||||
if monitor_info.get("Connected GPU") == gpu_name:
|
|
||||||
connected_monitors.append("{} ({})".format(monitor_name, monitor_info.get("Connector Type")))
|
|
||||||
if "Intel" in gpu_manufacturer and device_id.startswith(("01", "04", "0A", "0C", "0D")):
|
|
||||||
if monitor_info.get("Connector Type") == "VGA":
|
|
||||||
connected_monitors[-1] = "\033[0;31m{}{}\033[0m".format(connected_monitors[-1][:-1], ", unsupported)")
|
|
||||||
if connected_monitors:
|
|
||||||
print("{}- Connected Monitor{}: {}".format(" "*6, "s" if len(connected_monitors) > 1 else "", ", ".join(connected_monitors)))
|
|
||||||
|
|
||||||
max_supported_gpu_version = min_supported_gpu_version = None
|
max_supported_gpu_version = min_supported_gpu_version = None
|
||||||
|
|
||||||
for gpu_name, gpu_props in self.hardware_report.get("GPU").items():
|
for gpu_name, gpu_props in self.hardware_report.get("GPU").items():
|
||||||
@@ -189,18 +134,14 @@ class CompatibilityChecker:
|
|||||||
self.ocl_patched_macos_version = (gpu_props.get("OCLP Compatibility")[0], self.ocl_patched_macos_version[-1] if self.ocl_patched_macos_version and self.utils.parse_darwin_version(self.ocl_patched_macos_version[-1]) < self.utils.parse_darwin_version(gpu_props.get("OCLP Compatibility")[-1]) else gpu_props.get("OCLP Compatibility")[-1])
|
self.ocl_patched_macos_version = (gpu_props.get("OCLP Compatibility")[0], self.ocl_patched_macos_version[-1] if self.ocl_patched_macos_version and self.utils.parse_darwin_version(self.ocl_patched_macos_version[-1]) < self.utils.parse_darwin_version(gpu_props.get("OCLP Compatibility")[-1]) else gpu_props.get("OCLP Compatibility")[-1])
|
||||||
|
|
||||||
if max_supported_gpu_version == min_supported_gpu_version and max_supported_gpu_version == None:
|
if max_supported_gpu_version == min_supported_gpu_version and max_supported_gpu_version == None:
|
||||||
print("")
|
self.error_codes.append("ERROR_NO_COMPATIBLE_GPU")
|
||||||
print("You cannot install macOS without a supported GPU.")
|
return
|
||||||
print("Please do NOT spam my inbox or issue tracker about this issue anymore!")
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
self.utils.exit_program()
|
|
||||||
|
|
||||||
self.max_native_macos_version = max_supported_gpu_version if self.utils.parse_darwin_version(max_supported_gpu_version) < self.utils.parse_darwin_version(self.max_native_macos_version) else self.max_native_macos_version
|
self.max_native_macos_version = max_supported_gpu_version if self.utils.parse_darwin_version(max_supported_gpu_version) < self.utils.parse_darwin_version(self.max_native_macos_version) else self.max_native_macos_version
|
||||||
self.min_native_macos_version = min_supported_gpu_version if self.utils.parse_darwin_version(min_supported_gpu_version) > self.utils.parse_darwin_version(self.min_native_macos_version) else self.min_native_macos_version
|
self.min_native_macos_version = min_supported_gpu_version if self.utils.parse_darwin_version(min_supported_gpu_version) > self.utils.parse_darwin_version(self.min_native_macos_version) else self.min_native_macos_version
|
||||||
|
|
||||||
def check_sound_compatibility(self):
|
def check_sound_compatibility(self):
|
||||||
for audio_device, audio_props in self.hardware_report.get("Sound", {}).items():
|
for _, audio_props in self.hardware_report.get("Sound", {}).items():
|
||||||
codec_id = audio_props.get("Device ID")
|
codec_id = audio_props.get("Device ID")
|
||||||
|
|
||||||
max_version = min_version = None
|
max_version = min_version = None
|
||||||
@@ -213,19 +154,9 @@ class CompatibilityChecker:
|
|||||||
|
|
||||||
audio_props["Compatibility"] = (max_version, min_version)
|
audio_props["Compatibility"] = (max_version, min_version)
|
||||||
|
|
||||||
print("{}- {}: {}".format(" "*3, audio_device, self.show_macos_compatibility(audio_props.get("Compatibility"))))
|
|
||||||
|
|
||||||
audio_endpoints = audio_props.get("Audio Endpoints")
|
|
||||||
if audio_endpoints:
|
|
||||||
print("{}- Audio Endpoint{}: {}".format(" "*6, "s" if len(audio_endpoints) > 1 else "", ", ".join(audio_endpoints)))
|
|
||||||
|
|
||||||
def check_biometric_compatibility(self):
|
def check_biometric_compatibility(self):
|
||||||
print(" \033[1;93mNote:\033[0m Biometric authentication in macOS requires Apple T2 Chip,")
|
for _, biometric_props in self.hardware_report.get("Biometric", {}).items():
|
||||||
print(" which is not available for Hackintosh systems.")
|
|
||||||
print("")
|
|
||||||
for biometric_device, biometric_props in self.hardware_report.get("Biometric", {}).items():
|
|
||||||
biometric_props["Compatibility"] = (None, None)
|
biometric_props["Compatibility"] = (None, None)
|
||||||
print("{}- {}: {}".format(" "*3, biometric_device, self.show_macos_compatibility(biometric_props.get("Compatibility"))))
|
|
||||||
|
|
||||||
def check_network_compatibility(self):
|
def check_network_compatibility(self):
|
||||||
for device_name, device_props in self.hardware_report.get("Network", {}).items():
|
for device_name, device_props in self.hardware_report.get("Network", {}).items():
|
||||||
@@ -265,31 +196,7 @@ class CompatibilityChecker:
|
|||||||
if bus_type.startswith("PCI") and not device_props.get("Compatibility"):
|
if bus_type.startswith("PCI") and not device_props.get("Compatibility"):
|
||||||
device_props["Compatibility"] = (None, None)
|
device_props["Compatibility"] = (None, None)
|
||||||
|
|
||||||
print("{}- {}: {}".format(" "*3, device_name, self.show_macos_compatibility(device_props.get("Compatibility"))))
|
|
||||||
|
|
||||||
if device_id in pci_data.WirelessCardIDs:
|
|
||||||
if device_id in pci_data.BroadcomWiFiIDs:
|
|
||||||
print("{}- Continuity Support: \033[1;32mFull\033[0m (AirDrop, Handoff, Universal Clipboard, Instant Hotspot,...)".format(" "*6))
|
|
||||||
elif device_id in pci_data.IntelWiFiIDs:
|
|
||||||
print("{}- Continuity Support: \033[1;33mPartial\033[0m (Handoff and Universal Clipboard with AirportItlwm)".format(" "*6))
|
|
||||||
print("{}\033[1;93mNote:\033[0m AirDrop, Universal Clipboard, Instant Hotspot,... not available".format(" "*6))
|
|
||||||
elif device_id in pci_data.AtherosWiFiIDs:
|
|
||||||
print("{}- Continuity Support: \033[1;31mLimited\033[0m (No Continuity features available)".format(" "*6))
|
|
||||||
print("{}\033[1;93mNote:\033[0m Atheros cards are not recommended for macOS".format(" "*6))
|
|
||||||
|
|
||||||
if "OCLP Compatibility" in device_props:
|
|
||||||
print("{}- OCLP Compatibility: {}".format(" "*6, self.show_macos_compatibility(device_props.get("OCLP Compatibility"))))
|
|
||||||
|
|
||||||
def check_storage_compatibility(self):
|
def check_storage_compatibility(self):
|
||||||
if not self.hardware_report.get("Storage Controllers"):
|
|
||||||
print("")
|
|
||||||
print("No storage controller found!")
|
|
||||||
print("Please make sure to export the hardware report with the storage controller information")
|
|
||||||
print("and try again.")
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
self.utils.exit_program()
|
|
||||||
|
|
||||||
for controller_name, controller_props in self.hardware_report["Storage Controllers"].items():
|
for controller_name, controller_props in self.hardware_report["Storage Controllers"].items():
|
||||||
if controller_props.get("Bus Type") != "PCI":
|
if controller_props.get("Bus Type") != "PCI":
|
||||||
continue
|
continue
|
||||||
@@ -301,28 +208,17 @@ class CompatibilityChecker:
|
|||||||
min_version = os_data.get_lowest_darwin_version()
|
min_version = os_data.get_lowest_darwin_version()
|
||||||
|
|
||||||
if device_id in pci_data.IntelVMDIDs:
|
if device_id in pci_data.IntelVMDIDs:
|
||||||
print("")
|
self.error_codes.append("ERROR_INTEL_VMD")
|
||||||
print("Intel VMD controllers are not supported in macOS.")
|
return
|
||||||
print("Please disable Intel VMD in the BIOS settings and try again with new hardware report.")
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
self.utils.exit_program()
|
|
||||||
|
|
||||||
if next((device for device in pci_data.UnsupportedNVMeSSDIDs if device_id == device[0] and subsystem_id in device[1]), None):
|
if next((device for device in pci_data.UnsupportedNVMeSSDIDs if device_id == device[0] and subsystem_id in device[1]), None):
|
||||||
max_version = min_version = None
|
max_version = min_version = None
|
||||||
|
|
||||||
controller_props["Compatibility"] = (max_version, min_version)
|
controller_props["Compatibility"] = (max_version, min_version)
|
||||||
|
|
||||||
print("{}- {}: {}".format(" "*3, controller_name, self.show_macos_compatibility(controller_props.get("Compatibility"))))
|
|
||||||
|
|
||||||
if all(controller_props.get("Compatibility") == (None, None) for controller_name, controller_props in self.hardware_report["Storage Controllers"].items()):
|
if all(controller_props.get("Compatibility") == (None, None) for controller_name, controller_props in self.hardware_report["Storage Controllers"].items()):
|
||||||
print("")
|
self.error_codes.append("ERROR_NO_COMPATIBLE_STORAGE")
|
||||||
print("No compatible storage controller for macOS was found!")
|
return
|
||||||
print("Consider purchasing a compatible SSD NVMe for your system.")
|
|
||||||
print("Western Digital NVMe SSDs are generally recommended for good macOS compatibility.")
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
self.utils.exit_program()
|
|
||||||
|
|
||||||
def check_bluetooth_compatibility(self):
|
def check_bluetooth_compatibility(self):
|
||||||
for bluetooth_name, bluetooth_props in self.hardware_report.get("Bluetooth", {}).items():
|
for bluetooth_name, bluetooth_props in self.hardware_report.get("Bluetooth", {}).items():
|
||||||
@@ -339,8 +235,6 @@ class CompatibilityChecker:
|
|||||||
max_version = min_version = None
|
max_version = min_version = None
|
||||||
|
|
||||||
bluetooth_props["Compatibility"] = (max_version, min_version)
|
bluetooth_props["Compatibility"] = (max_version, min_version)
|
||||||
|
|
||||||
print("{}- {}: {}".format(" "*3, bluetooth_name, self.show_macos_compatibility(bluetooth_props.get("Compatibility"))))
|
|
||||||
|
|
||||||
def check_sd_controller_compatibility(self):
|
def check_sd_controller_compatibility(self):
|
||||||
for controller_name, controller_props in self.hardware_report.get("SD Controller", {}).items():
|
for controller_name, controller_props in self.hardware_report.get("SD Controller", {}).items():
|
||||||
@@ -357,16 +251,12 @@ class CompatibilityChecker:
|
|||||||
|
|
||||||
controller_props["Compatibility"] = (max_version, min_version)
|
controller_props["Compatibility"] = (max_version, min_version)
|
||||||
|
|
||||||
print("{}- {}: {}".format(" "*3, controller_name, self.show_macos_compatibility(controller_props.get("Compatibility"))))
|
|
||||||
|
|
||||||
def check_compatibility(self, hardware_report):
|
def check_compatibility(self, hardware_report):
|
||||||
self.hardware_report = hardware_report
|
self.hardware_report = hardware_report
|
||||||
self.ocl_patched_macos_version = None
|
self.ocl_patched_macos_version = None
|
||||||
|
self.error_codes = []
|
||||||
|
|
||||||
self.utils.head("Compatibility Checker")
|
self.utils.log_message("[COMPATIBILITY CHECKER] Starting compatibility check...", level="INFO")
|
||||||
print("")
|
|
||||||
print("Checking compatibility with macOS for the following devices:")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
steps = [
|
steps = [
|
||||||
('CPU', self.check_cpu_compatibility),
|
('CPU', self.check_cpu_compatibility),
|
||||||
@@ -379,15 +269,13 @@ class CompatibilityChecker:
|
|||||||
('SD Controller', self.check_sd_controller_compatibility)
|
('SD Controller', self.check_sd_controller_compatibility)
|
||||||
]
|
]
|
||||||
|
|
||||||
index = 0
|
|
||||||
for device_type, function in steps:
|
for device_type, function in steps:
|
||||||
if self.hardware_report.get(device_type):
|
if self.hardware_report.get(device_type):
|
||||||
index += 1
|
|
||||||
print("{}. {}:".format(index, device_type))
|
|
||||||
time.sleep(0.25)
|
|
||||||
function()
|
function()
|
||||||
|
|
||||||
print("")
|
if self.error_codes:
|
||||||
self.utils.request_input()
|
self.utils.log_message("[COMPATIBILITY CHECKER] Compatibility check that found errors: {}".format(", ".join(self.error_codes)), level="INFO")
|
||||||
|
return hardware_report, (None, None), None, self.error_codes
|
||||||
|
|
||||||
return hardware_report, (self.min_native_macos_version, self.max_native_macos_version), self.ocl_patched_macos_version
|
self.utils.log_message("[COMPATIBILITY CHECKER] Compatibility check completed successfully", level="INFO")
|
||||||
|
return hardware_report, (self.min_native_macos_version, self.max_native_macos_version), self.ocl_patched_macos_version, self.error_codes
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import copy
|
||||||
|
import os
|
||||||
from Scripts.datasets import chipset_data
|
from Scripts.datasets import chipset_data
|
||||||
from Scripts.datasets import cpu_data
|
from Scripts.datasets import cpu_data
|
||||||
from Scripts.datasets import mac_model_data
|
from Scripts.datasets import mac_model_data
|
||||||
@@ -8,13 +10,14 @@ from Scripts.datasets import codec_layouts
|
|||||||
from Scripts import gathering_files
|
from Scripts import gathering_files
|
||||||
from Scripts import smbios
|
from Scripts import smbios
|
||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
|
from Scripts.custom_dialogs import show_options_dialog
|
||||||
import random
|
import random
|
||||||
|
|
||||||
class ConfigProdigy:
|
class ConfigProdigy:
|
||||||
def __init__(self):
|
def __init__(self, gathering_files_instance=None, smbios_instance=None, utils_instance=None):
|
||||||
self.g = gathering_files.gatheringFiles()
|
self.g = gathering_files_instance if gathering_files_instance else gathering_files.gatheringFiles()
|
||||||
self.smbios = smbios.SMBIOS()
|
self.smbios = smbios_instance if smbios_instance else smbios.SMBIOS()
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
self.cpuids = {
|
self.cpuids = {
|
||||||
"Ivy Bridge": "A9060300",
|
"Ivy Bridge": "A9060300",
|
||||||
"Haswell": "C3060300",
|
"Haswell": "C3060300",
|
||||||
@@ -237,76 +240,7 @@ class ConfigProdigy:
|
|||||||
|
|
||||||
return dict(sorted(igpu_properties.items(), key=lambda item: item[0]))
|
return dict(sorted(igpu_properties.items(), key=lambda item: item[0]))
|
||||||
|
|
||||||
def select_audio_codec_layout(self, hardware_report, config=None, controller_required=False):
|
def deviceproperties(self, hardware_report, disabled_devices, macos_version, kexts, audio_layout_id=None, audio_controller_properties=None):
|
||||||
try:
|
|
||||||
for device_properties in config["DeviceProperties"]["Add"].values():
|
|
||||||
if device_properties.get("layout-id"):
|
|
||||||
return None, None
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
codec_id = None
|
|
||||||
audio_controller_properties = None
|
|
||||||
|
|
||||||
for codec_properties in hardware_report.get("Sound", {}).values():
|
|
||||||
if codec_properties.get("Device ID") in codec_layouts.data:
|
|
||||||
codec_id = codec_properties.get("Device ID")
|
|
||||||
|
|
||||||
if codec_properties.get("Controller Device ID"):
|
|
||||||
for device_name, device_properties in hardware_report.get("System Devices").items():
|
|
||||||
if device_properties.get("Device ID") == codec_properties.get("Controller Device ID"):
|
|
||||||
audio_controller_properties = device_properties
|
|
||||||
break
|
|
||||||
break
|
|
||||||
|
|
||||||
if not codec_id:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
if controller_required and not audio_controller_properties:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
available_layouts = codec_layouts.data.get(codec_id)
|
|
||||||
|
|
||||||
recommended_authors = ("Mirone", "InsanelyDeepak", "Toleda", "DalianSky")
|
|
||||||
recommended_layouts = [layout for layout in available_layouts if self.utils.contains_any(recommended_authors, layout.comment)]
|
|
||||||
|
|
||||||
default_layout = random.choice(recommended_layouts or available_layouts)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
contents = []
|
|
||||||
contents.append("")
|
|
||||||
contents.append("List of Codec Layouts:")
|
|
||||||
contents.append("")
|
|
||||||
contents.append("ID Comment")
|
|
||||||
contents.append("------------------------------------------------------------------")
|
|
||||||
for layout in available_layouts:
|
|
||||||
line = "{:<4} {}".format(layout.id, layout.comment[:60])
|
|
||||||
if layout == default_layout:
|
|
||||||
contents.append("\033[1;32m{}\033[0m".format(line))
|
|
||||||
else:
|
|
||||||
contents.append(line)
|
|
||||||
contents.append("")
|
|
||||||
contents.append("\033[1;93mNote:\033[0m")
|
|
||||||
contents.append("- The default layout may not be optimal.")
|
|
||||||
contents.append("- Test different layouts to find what works best for your system.")
|
|
||||||
contents.append("")
|
|
||||||
content = "\n".join(contents)
|
|
||||||
|
|
||||||
self.utils.adjust_window_size(content)
|
|
||||||
self.utils.head("Choosing Codec Layout ID", resize=False)
|
|
||||||
print(content)
|
|
||||||
selected_layout_id = self.utils.request_input(f"Enter the ID of the codec layout you want to use (default: {default_layout.id}): ") or default_layout.id
|
|
||||||
|
|
||||||
try:
|
|
||||||
selected_layout_id = int(selected_layout_id)
|
|
||||||
|
|
||||||
for layout in available_layouts:
|
|
||||||
if layout.id == selected_layout_id:
|
|
||||||
return selected_layout_id, audio_controller_properties
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
def deviceproperties(self, hardware_report, disabled_devices, macos_version, kexts):
|
|
||||||
deviceproperties_add = {}
|
deviceproperties_add = {}
|
||||||
|
|
||||||
def add_device_property(pci_path, properties):
|
def add_device_property(pci_path, properties):
|
||||||
@@ -349,11 +283,8 @@ class ConfigProdigy:
|
|||||||
"model": gpu_name
|
"model": gpu_name
|
||||||
})
|
})
|
||||||
|
|
||||||
if kexts[kext_data.kext_index_by_name.get("AppleALC")].checked:
|
if audio_layout_id is not None and audio_controller_properties is not None:
|
||||||
selected_layout_id, audio_controller_properties = self.select_audio_codec_layout(hardware_report, controller_required=True)
|
add_device_property(audio_controller_properties.get("PCI Path"), {"layout-id": audio_layout_id})
|
||||||
|
|
||||||
if selected_layout_id and audio_controller_properties:
|
|
||||||
add_device_property(audio_controller_properties.get("PCI Path"), {"layout-id": selected_layout_id})
|
|
||||||
|
|
||||||
for network_name, network_props in hardware_report.get("Network", {}).items():
|
for network_name, network_props in hardware_report.get("Network", {}).items():
|
||||||
device_id = network_props.get("Device ID")
|
device_id = network_props.get("Device ID")
|
||||||
@@ -502,7 +433,7 @@ class ConfigProdigy:
|
|||||||
|
|
||||||
return kernel_patch
|
return kernel_patch
|
||||||
|
|
||||||
def boot_args(self, hardware_report, macos_version, needs_oclp, kexts, config):
|
def boot_args(self, hardware_report, macos_version, needs_oclp, kexts, config, audio_layout_id=None, audio_controller_properties=None):
|
||||||
boot_args = [
|
boot_args = [
|
||||||
"-v",
|
"-v",
|
||||||
"debug=0x100",
|
"debug=0x100",
|
||||||
@@ -566,10 +497,8 @@ class ConfigProdigy:
|
|||||||
elif discrete_gpu.get("Manufacturer") == "NVIDIA" and not "Kepler" in discrete_gpu.get("Codename"):
|
elif discrete_gpu.get("Manufacturer") == "NVIDIA" and not "Kepler" in discrete_gpu.get("Codename"):
|
||||||
boot_args.extend(("nvda_drv_vrl=1", "ngfxcompat=1", "ngfxgl=1"))
|
boot_args.extend(("nvda_drv_vrl=1", "ngfxcompat=1", "ngfxgl=1"))
|
||||||
elif kext.name == "AppleALC":
|
elif kext.name == "AppleALC":
|
||||||
selected_layout_id, _ = self.select_audio_codec_layout(hardware_report, config)
|
if audio_layout_id is not None and audio_controller_properties is None:
|
||||||
|
boot_args.append("alcid={}".format(audio_layout_id))
|
||||||
if selected_layout_id:
|
|
||||||
boot_args.append("alcid={}".format(selected_layout_id))
|
|
||||||
elif kext.name == "VoodooI2C":
|
elif kext.name == "VoodooI2C":
|
||||||
boot_args.append("-vi2c-force-polling")
|
boot_args.append("-vi2c-force-polling")
|
||||||
elif kext.name == "CpuTopologyRebuild":
|
elif kext.name == "CpuTopologyRebuild":
|
||||||
@@ -611,7 +540,7 @@ class ConfigProdigy:
|
|||||||
|
|
||||||
return uefi_drivers
|
return uefi_drivers
|
||||||
|
|
||||||
def genarate(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp, kexts, config):
|
def genarate(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp, kexts, config, audio_layout_id=None, audio_controller_properties=None):
|
||||||
del config["#WARNING - 1"]
|
del config["#WARNING - 1"]
|
||||||
del config["#WARNING - 2"]
|
del config["#WARNING - 2"]
|
||||||
del config["#WARNING - 3"]
|
del config["#WARNING - 3"]
|
||||||
@@ -636,7 +565,7 @@ class ConfigProdigy:
|
|||||||
config["Booter"]["Quirks"]["SetupVirtualMap"] = hardware_report.get("BIOS").get("Firmware Type") == "UEFI" and not hardware_report.get("Motherboard").get("Chipset") in chipset_data.AMDChipsets[11:17] + chipset_data.IntelChipsets[90:100]
|
config["Booter"]["Quirks"]["SetupVirtualMap"] = hardware_report.get("BIOS").get("Firmware Type") == "UEFI" and not hardware_report.get("Motherboard").get("Chipset") in chipset_data.AMDChipsets[11:17] + chipset_data.IntelChipsets[90:100]
|
||||||
config["Booter"]["Quirks"]["SyncRuntimePermissions"] = "AMD" in hardware_report.get("CPU").get("Manufacturer") or hardware_report.get("Motherboard").get("Chipset") in chipset_data.IntelChipsets[90:100] + chipset_data.IntelChipsets[104:]
|
config["Booter"]["Quirks"]["SyncRuntimePermissions"] = "AMD" in hardware_report.get("CPU").get("Manufacturer") or hardware_report.get("Motherboard").get("Chipset") in chipset_data.IntelChipsets[90:100] + chipset_data.IntelChipsets[104:]
|
||||||
|
|
||||||
config["DeviceProperties"]["Add"] = self.deviceproperties(hardware_report, disabled_devices, macos_version, kexts)
|
config["DeviceProperties"]["Add"] = self.deviceproperties(hardware_report, disabled_devices, macos_version, kexts, audio_layout_id, audio_controller_properties)
|
||||||
|
|
||||||
config["Kernel"]["Block"] = self.block_kext_bundle(kexts)
|
config["Kernel"]["Block"] = self.block_kext_bundle(kexts)
|
||||||
spoof_cpuid = self.spoof_cpuid(
|
spoof_cpuid = self.spoof_cpuid(
|
||||||
@@ -685,7 +614,7 @@ class ConfigProdigy:
|
|||||||
config["Misc"]["Tools"] = []
|
config["Misc"]["Tools"] = []
|
||||||
|
|
||||||
del config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["#INFO (prev-lang:kbd)"]
|
del config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["#INFO (prev-lang:kbd)"]
|
||||||
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["boot-args"] = self.boot_args(hardware_report, macos_version, needs_oclp, kexts, config)
|
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["boot-args"] = self.boot_args(hardware_report, macos_version, needs_oclp, kexts, config, audio_layout_id, audio_controller_properties)
|
||||||
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["csr-active-config"] = self.utils.hex_to_bytes(self.csr_active_config(macos_version))
|
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["csr-active-config"] = self.utils.hex_to_bytes(self.csr_active_config(macos_version))
|
||||||
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["prev-lang:kbd"] = self.utils.hex_to_bytes("")
|
config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["prev-lang:kbd"] = self.utils.hex_to_bytes("")
|
||||||
|
|
||||||
|
|||||||
461
Scripts/custom_dialogs.py
Normal file
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:
|
class macOSVersionInfo:
|
||||||
def __init__(self, name, macos_version, release_status = "final"):
|
def __init__(self, name, macos_version, release_status = "final"):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -17,7 +23,7 @@ macos_versions = [
|
|||||||
macOSVersionInfo("Tahoe", "26")
|
macOSVersionInfo("Tahoe", "26")
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_latest_darwin_version(include_beta=True):
|
def get_latest_darwin_version(include_beta=INCLUDE_BETA):
|
||||||
for macos_version in macos_versions[::-1]:
|
for macos_version in macos_versions[::-1]:
|
||||||
if include_beta:
|
if include_beta:
|
||||||
return "{}.{}.{}".format(macos_version.darwin_version, 99, 99)
|
return "{}.{}.{}".format(macos_version.darwin_version, 99, 99)
|
||||||
@@ -32,4 +38,4 @@ def get_macos_name_by_darwin(darwin_version):
|
|||||||
for data in macos_versions:
|
for data in macos_versions:
|
||||||
if data.darwin_version == int(darwin_version[:2]):
|
if data.darwin_version == int(darwin_version[:2]):
|
||||||
return "macOS {} {}{}".format(data.name, data.macos_version, "" if data.release_status == "final" else " (Beta)")
|
return "macOS {} {}{}".format(data.name, data.macos_version, "" if data.release_status == "final" else " (Beta)")
|
||||||
return None
|
return None
|
||||||
@@ -7,13 +7,11 @@ from Scripts import run
|
|||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
|
|
||||||
class DSDT:
|
class DSDT:
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, utils_instance=None, github_instance=None, resource_fetcher_instance=None, run_instance=None):
|
||||||
#self.dl = downloader.Downloader()
|
self.u = utils_instance if utils_instance else utils.Utils()
|
||||||
self.github = github.Github()
|
self.github = github_instance if github_instance else github.Github()
|
||||||
self.fetcher = resource_fetcher.ResourceFetcher()
|
self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
|
||||||
self.r = run.Run()
|
self.r = run_instance if run_instance else run.Run()
|
||||||
#self.u = utils.Utils("SSDT Time")
|
|
||||||
self.u = utils.Utils()
|
|
||||||
self.iasl_url_macOS = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-stable"
|
self.iasl_url_macOS = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-stable"
|
||||||
self.iasl_url_macOS_legacy = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-legacy"
|
self.iasl_url_macOS_legacy = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-legacy"
|
||||||
self.iasl_url_linux = "https://raw.githubusercontent.com/corpnewt/linux_iasl/main/iasl.zip"
|
self.iasl_url_linux = "https://raw.githubusercontent.com/corpnewt/linux_iasl/main/iasl.zip"
|
||||||
@@ -315,10 +313,7 @@ class DSDT:
|
|||||||
return self.check_iasl(legacy=legacy,try_downloading=False)
|
return self.check_iasl(legacy=legacy,try_downloading=False)
|
||||||
|
|
||||||
def _download_and_extract(self, temp, url):
|
def _download_and_extract(self, temp, url):
|
||||||
self.u.head("Gathering Files")
|
self.u.log_message("[DSDT] Downloading iasl...", level="INFO")
|
||||||
print("")
|
|
||||||
print("Please wait for download iasl...")
|
|
||||||
print("")
|
|
||||||
ztemp = tempfile.mkdtemp(dir=temp)
|
ztemp = tempfile.mkdtemp(dir=temp)
|
||||||
zfile = os.path.basename(url)
|
zfile = os.path.basename(url)
|
||||||
#print("Downloading {}".format(os.path.basename(url)))
|
#print("Downloading {}".format(os.path.basename(url)))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from Scripts.custom_dialogs import show_info
|
||||||
from Scripts import github
|
from Scripts import github
|
||||||
from Scripts import kext_maestro
|
from Scripts import kext_maestro
|
||||||
from Scripts import integrity_checker
|
from Scripts import integrity_checker
|
||||||
@@ -11,12 +12,12 @@ import platform
|
|||||||
os_name = platform.system()
|
os_name = platform.system()
|
||||||
|
|
||||||
class gatheringFiles:
|
class gatheringFiles:
|
||||||
def __init__(self):
|
def __init__(self, utils_instance=None, github_instance=None, kext_maestro_instance=None, integrity_checker_instance=None, resource_fetcher_instance=None):
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
self.github = github.Github()
|
self.github = github_instance if github_instance else github.Github()
|
||||||
self.kext = kext_maestro.KextMaestro()
|
self.kext = kext_maestro_instance if kext_maestro_instance else kext_maestro.KextMaestro()
|
||||||
self.fetcher = resource_fetcher.ResourceFetcher()
|
self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
|
||||||
self.integrity_checker = integrity_checker.IntegrityChecker()
|
self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker()
|
||||||
self.dortania_builds_url = "https://raw.githubusercontent.com/dortania/build-repo/builds/latest.json"
|
self.dortania_builds_url = "https://raw.githubusercontent.com/dortania/build-repo/builds/latest.json"
|
||||||
self.ocbinarydata_url = "https://github.com/acidanthera/OcBinaryData/archive/refs/heads/master.zip"
|
self.ocbinarydata_url = "https://github.com/acidanthera/OcBinaryData/archive/refs/heads/master.zip"
|
||||||
self.amd_vanilla_patches_url = "https://raw.githubusercontent.com/AMD-OSX/AMD_Vanilla/beta/patches.plist"
|
self.amd_vanilla_patches_url = "https://raw.githubusercontent.com/AMD-OSX/AMD_Vanilla/beta/patches.plist"
|
||||||
@@ -85,6 +86,7 @@ class gatheringFiles:
|
|||||||
|
|
||||||
def move_bootloader_kexts_to_product_directory(self, product_name):
|
def move_bootloader_kexts_to_product_directory(self, product_name):
|
||||||
if not os.path.exists(self.temporary_dir):
|
if not os.path.exists(self.temporary_dir):
|
||||||
|
self.utils.log_message("[GATHERING FILES] The directory {} does not exist.".format(self.temporary_dir), level="ERROR", to_build_log=True)
|
||||||
raise FileNotFoundError("The directory {} does not exist.".format(self.temporary_dir))
|
raise FileNotFoundError("The directory {} does not exist.".format(self.temporary_dir))
|
||||||
|
|
||||||
temp_product_dir = os.path.join(self.temporary_dir, product_name)
|
temp_product_dir = os.path.join(self.temporary_dir, product_name)
|
||||||
@@ -139,9 +141,7 @@ class gatheringFiles:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def gather_bootloader_kexts(self, kexts, macos_version):
|
def gather_bootloader_kexts(self, kexts, macos_version):
|
||||||
self.utils.head("Gathering Files")
|
self.utils.log_message("[GATHERING FILES] Please wait for download OpenCorePkg, kexts and macserial...", level="INFO", to_build_log=True)
|
||||||
print("")
|
|
||||||
print("Please wait for download OpenCorePkg, kexts and macserial...")
|
|
||||||
|
|
||||||
download_history = self.utils.read_file(self.download_history_file)
|
download_history = self.utils.read_file(self.download_history_file)
|
||||||
if not isinstance(download_history, list):
|
if not isinstance(download_history, list):
|
||||||
@@ -187,8 +187,7 @@ class gatheringFiles:
|
|||||||
product_download_index = self.get_product_index(download_database, product.github_repo.get("repo"))
|
product_download_index = self.get_product_index(download_database, product.github_repo.get("repo"))
|
||||||
|
|
||||||
if product_download_index is None:
|
if product_download_index is None:
|
||||||
print("\n")
|
self.utils.log_message("[GATHERING FILES] Could not find download URL for {}.".format(product_name), level="WARNING", to_build_log=True)
|
||||||
print("Could not find download URL for {}.".format(product_name))
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
product_info = download_database[product_download_index]
|
product_info = download_database[product_download_index]
|
||||||
@@ -210,20 +209,14 @@ class gatheringFiles:
|
|||||||
folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path)
|
folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path)
|
||||||
|
|
||||||
if is_latest_id and folder_is_valid:
|
if is_latest_id and folder_is_valid:
|
||||||
print(f"\nLatest version of {product_name} already downloaded.")
|
self.utils.log_message("[GATHERING FILES] Latest version of {} already downloaded.".format(product_name), level="INFO", to_build_log=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print("")
|
self.utils.log_message("[GATHERING FILES] Updating {}...".format(product_name), level="INFO", to_build_log=True)
|
||||||
print("Updating" if product_history_index is not None else "Please wait for download", end=" ")
|
|
||||||
print("{}...".format(product_name))
|
|
||||||
if product_download_url:
|
if product_download_url:
|
||||||
print("from {}".format(product_download_url))
|
self.utils.log_message("[GATHERING FILES] Downloading from {}".format(product_download_url), level="INFO", to_build_log=True)
|
||||||
print("")
|
|
||||||
else:
|
else:
|
||||||
print("")
|
self.utils.log_message("[GATHERING FILES] Could not find download URL for {}.".format(product_name), level="ERROR", to_build_log=True)
|
||||||
print("Could not find download URL for {}.".format(product_name))
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
shutil.rmtree(self.temporary_dir, ignore_errors=True)
|
shutil.rmtree(self.temporary_dir, ignore_errors=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -231,9 +224,10 @@ class gatheringFiles:
|
|||||||
if not self.fetcher.download_and_save_file(product_download_url, zip_path, sha256_hash):
|
if not self.fetcher.download_and_save_file(product_download_url, zip_path, sha256_hash):
|
||||||
folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path)
|
folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path)
|
||||||
if product_history_index is not None and folder_is_valid:
|
if product_history_index is not None and folder_is_valid:
|
||||||
print("Using previously downloaded version of {}.".format(product_name))
|
self.utils.log_message("[GATHERING FILES] Using previously downloaded version of {}.".format(product_name), level="INFO", to_build_log=True)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
self.utils.log_message("[GATHERING FILES] Could not download {} at this time. Please try again later.".format(product_name), level="ERROR", to_build_log=True)
|
||||||
raise Exception("Could not download {} at this time. Please try again later.".format(product_name))
|
raise Exception("Could not download {} at this time. Please try again later.".format(product_name))
|
||||||
|
|
||||||
self.utils.extract_zip_file(zip_path)
|
self.utils.extract_zip_file(zip_path)
|
||||||
@@ -250,17 +244,12 @@ class gatheringFiles:
|
|||||||
|
|
||||||
if "OpenCore" in product_name:
|
if "OpenCore" in product_name:
|
||||||
oc_binary_data_zip_path = os.path.join(self.temporary_dir, "OcBinaryData.zip")
|
oc_binary_data_zip_path = os.path.join(self.temporary_dir, "OcBinaryData.zip")
|
||||||
print("")
|
self.utils.log_message("[GATHERING FILES] Please wait for download OcBinaryData...", level="INFO", to_build_log=True)
|
||||||
print("Please wait for download OcBinaryData...")
|
self.utils.log_message("[GATHERING FILES] Downloading from {}".format(self.ocbinarydata_url), level="INFO", to_build_log=True)
|
||||||
print("from {}".format(self.ocbinarydata_url))
|
|
||||||
print("")
|
|
||||||
self.fetcher.download_and_save_file(self.ocbinarydata_url, oc_binary_data_zip_path)
|
self.fetcher.download_and_save_file(self.ocbinarydata_url, oc_binary_data_zip_path)
|
||||||
|
|
||||||
if not os.path.exists(oc_binary_data_zip_path):
|
if not os.path.exists(oc_binary_data_zip_path):
|
||||||
print("")
|
self.utils.log_message("[GATHERING FILES] Could not download OcBinaryData at this time. Please try again later.", level="ERROR", to_build_log=True)
|
||||||
print("Could not download OcBinaryData at this time.")
|
|
||||||
print("Please try again later.\n")
|
|
||||||
self.utils.request_input()
|
|
||||||
shutil.rmtree(self.temporary_dir, ignore_errors=True)
|
shutil.rmtree(self.temporary_dir, ignore_errors=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -278,14 +267,9 @@ class gatheringFiles:
|
|||||||
response = self.fetcher.fetch_and_parse_content(patches_url, "plist")
|
response = self.fetcher.fetch_and_parse_content(patches_url, "plist")
|
||||||
|
|
||||||
return response["Kernel"]["Patch"]
|
return response["Kernel"]["Patch"]
|
||||||
except:
|
except:
|
||||||
print("")
|
self.utils.log_message("[GATHERING FILES] Unable to download {} at this time".format(patches_name), level="WARNING", to_build_log=True)
|
||||||
print("Unable to download {} at this time".format(patches_name))
|
show_info("Download Failed", "Unable to download {} at this time. Please try again later or apply them manually.".format(patches_name))
|
||||||
print("from " + patches_url)
|
|
||||||
print("")
|
|
||||||
print("Please try again later or apply them manually.")
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _update_download_history(self, download_history, product_name, product_id, product_url, sha256_hash):
|
def _update_download_history(self, download_history, product_name, product_id, product_url, sha256_hash):
|
||||||
@@ -310,7 +294,7 @@ class gatheringFiles:
|
|||||||
if os_name != "Windows":
|
if os_name != "Windows":
|
||||||
return
|
return
|
||||||
|
|
||||||
self.utils.head("Gathering Hardware Sniffer")
|
self.utils.log_message("[GATHERING FILES] Gathering Hardware Sniffer...", level="INFO")
|
||||||
|
|
||||||
PRODUCT_NAME = "Hardware-Sniffer-CLI.exe"
|
PRODUCT_NAME = "Hardware-Sniffer-CLI.exe"
|
||||||
REPO_OWNER = "lzhoang2801"
|
REPO_OWNER = "lzhoang2801"
|
||||||
@@ -333,11 +317,7 @@ class gatheringFiles:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not all([product_id, product_download_url, sha256_hash]):
|
if not all([product_id, product_download_url, sha256_hash]):
|
||||||
print("")
|
show_info("Release Information Not Found", "Could not find release information for {}. Please try again later.".format(PRODUCT_NAME))
|
||||||
print("Could not find release information for {}.".format(PRODUCT_NAME))
|
|
||||||
print("Please try again later.")
|
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
raise Exception("Could not find release information for {}.".format(PRODUCT_NAME))
|
raise Exception("Could not find release information for {}.".format(PRODUCT_NAME))
|
||||||
|
|
||||||
download_history = self.utils.read_file(self.download_history_file)
|
download_history = self.utils.read_file(self.download_history_file)
|
||||||
@@ -356,22 +336,14 @@ class gatheringFiles:
|
|||||||
file_is_valid = (sha256_hash == local_hash)
|
file_is_valid = (sha256_hash == local_hash)
|
||||||
|
|
||||||
if is_latest_id and file_is_valid:
|
if is_latest_id and file_is_valid:
|
||||||
print("")
|
self.utils.log_message("[GATHERING FILES] Latest version of {} already downloaded.".format(PRODUCT_NAME), level="INFO")
|
||||||
print("Latest version of {} already downloaded.".format(PRODUCT_NAME))
|
|
||||||
return destination_path
|
return destination_path
|
||||||
|
|
||||||
print("")
|
self.utils.log_message("[GATHERING FILES] {} {}...".format("Updating" if product_history_index is not None else "Please wait for download", PRODUCT_NAME), level="INFO")
|
||||||
print("Updating" if product_history_index is not None else "Please wait for download", end=" ")
|
|
||||||
print("{}...".format(PRODUCT_NAME))
|
|
||||||
print("")
|
|
||||||
print("from {}".format(product_download_url))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
if not self.fetcher.download_and_save_file(product_download_url, destination_path, sha256_hash):
|
if not self.fetcher.download_and_save_file(product_download_url, destination_path, sha256_hash):
|
||||||
manual_download_url = f"https://github.com/{REPO_OWNER}/{REPO_NAME}/releases/latest"
|
manual_download_url = "https://github.com/{}/{}/releases/latest".format(REPO_OWNER, REPO_NAME)
|
||||||
print("Go to {} to download {} manually.".format(manual_download_url, PRODUCT_NAME))
|
show_info("Download Failed", "Go to {} to download {} manually.".format(manual_download_url, PRODUCT_NAME))
|
||||||
print("")
|
|
||||||
self.utils.request_input()
|
|
||||||
raise Exception("Failed to download {}.".format(PRODUCT_NAME))
|
raise Exception("Failed to download {}.".format(PRODUCT_NAME))
|
||||||
|
|
||||||
self._update_download_history(download_history, PRODUCT_NAME, product_id, product_download_url, sha256_hash)
|
self._update_download_history(download_history, PRODUCT_NAME, product_id, product_download_url, sha256_hash)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import random
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
class Github:
|
class Github:
|
||||||
def __init__(self):
|
def __init__(self, utils_instance=None, resource_fetcher_instance=None):
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
self.fetcher = resource_fetcher.ResourceFetcher()
|
self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher()
|
||||||
|
|
||||||
def extract_payload(self, response):
|
def extract_payload(self, response):
|
||||||
for line in response.splitlines():
|
for line in response.splitlines():
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
from Scripts.datasets import os_data
|
from Scripts.datasets import os_data
|
||||||
from Scripts.datasets import pci_data
|
from Scripts.datasets import pci_data
|
||||||
from Scripts import compatibility_checker
|
from Scripts.custom_dialogs import show_confirmation, show_info, show_options_dialog
|
||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
|
|
||||||
class HardwareCustomizer:
|
class HardwareCustomizer:
|
||||||
def __init__(self):
|
def __init__(self, utils_instance=None):
|
||||||
self.compatibility_checker = compatibility_checker.CompatibilityChecker()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
self.utils = utils.Utils()
|
|
||||||
|
def show_macos_compatibility(self, device_compatibility):
|
||||||
|
if not device_compatibility:
|
||||||
|
return "<span style='color:gray'>Unchecked</span>"
|
||||||
|
|
||||||
|
if not device_compatibility[0]:
|
||||||
|
return "<span style='color:red'>Unsupported</span>"
|
||||||
|
|
||||||
|
max_compatibility = self.utils.parse_darwin_version(device_compatibility[0])[0]
|
||||||
|
min_compatibility = self.utils.parse_darwin_version(device_compatibility[-1])[0]
|
||||||
|
max_version = self.utils.parse_darwin_version(os_data.get_latest_darwin_version())[0]
|
||||||
|
min_version = self.utils.parse_darwin_version(os_data.get_lowest_darwin_version())[0]
|
||||||
|
|
||||||
|
if max_compatibility == min_version:
|
||||||
|
return "<span style='color:blue'>Maximum support up to {}</span>".format(
|
||||||
|
os_data.get_macos_name_by_darwin(device_compatibility[-1])
|
||||||
|
)
|
||||||
|
|
||||||
|
if min_version < min_compatibility or max_compatibility < max_version:
|
||||||
|
return "<span style='color:green'>{} to {}</span>".format(
|
||||||
|
os_data.get_macos_name_by_darwin(device_compatibility[-1]),
|
||||||
|
os_data.get_macos_name_by_darwin(device_compatibility[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
return "<span style='color:blue'>Up to {}</span>".format(
|
||||||
|
os_data.get_macos_name_by_darwin(device_compatibility[0])
|
||||||
|
)
|
||||||
|
|
||||||
def hardware_customization(self, hardware_report, macos_version):
|
def hardware_customization(self, hardware_report, macos_version):
|
||||||
self.hardware_report = hardware_report
|
self.hardware_report = hardware_report
|
||||||
@@ -16,7 +42,7 @@ class HardwareCustomizer:
|
|||||||
self.selected_devices = {}
|
self.selected_devices = {}
|
||||||
needs_oclp = False
|
needs_oclp = False
|
||||||
|
|
||||||
self.utils.head("Hardware Customization")
|
self.utils.log_message("[HARDWARE CUSTOMIZATION] Starting hardware customization", level="INFO")
|
||||||
|
|
||||||
for device_type, devices in self.hardware_report.items():
|
for device_type, devices in self.hardware_report.items():
|
||||||
if not device_type in ("BIOS", "GPU", "Sound", "Biometric", "Network", "Storage Controllers", "Bluetooth", "SD Controller"):
|
if not device_type in ("BIOS", "GPU", "Sound", "Biometric", "Network", "Storage Controllers", "Bluetooth", "SD Controller"):
|
||||||
@@ -27,24 +53,20 @@ class HardwareCustomizer:
|
|||||||
|
|
||||||
if device_type == "BIOS":
|
if device_type == "BIOS":
|
||||||
self.customized_hardware[device_type] = devices.copy()
|
self.customized_hardware[device_type] = devices.copy()
|
||||||
if devices.get("Firmware Type") != "UEFI":
|
|
||||||
print("\n*** BIOS Firmware Type is not UEFI")
|
|
||||||
print("")
|
|
||||||
print("Do you want to build the EFI for UEFI?")
|
|
||||||
print("If yes, please make sure to update your BIOS and enable UEFI Boot Mode in your BIOS settings.")
|
|
||||||
print("You can still proceed with Legacy if you prefer.")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
while True:
|
if devices.get("Firmware Type") != "UEFI":
|
||||||
answer = self.utils.request_input("Build EFI for UEFI? (Yes/no): ").strip().lower()
|
content = (
|
||||||
if answer == "yes":
|
"Would you like to build the EFI for UEFI?<br>"
|
||||||
self.customized_hardware[device_type]["Firmware Type"] = "UEFI"
|
"If yes, please make sure to update your BIOS and enable UEFI Boot Mode in your BIOS settings.<br>"
|
||||||
break
|
"You can still proceed with Legacy if you prefer."
|
||||||
elif answer == "no":
|
)
|
||||||
self.customized_hardware[device_type]["Firmware Type"] = "Legacy"
|
if show_confirmation("BIOS Firmware Type is not UEFI", content):
|
||||||
break
|
self.utils.log_message("[HARDWARE CUSTOMIZATION] BIOS Firmware Type is not UEFI, building EFI for UEFI", level="INFO")
|
||||||
else:
|
self.customized_hardware[device_type]["Firmware Type"] = "UEFI"
|
||||||
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
|
else:
|
||||||
|
self.utils.log_message("[HARDWARE CUSTOMIZATION] BIOS Firmware Type is not UEFI, building EFI for Legacy", level="INFO")
|
||||||
|
self.customized_hardware[device_type]["Firmware Type"] = "Legacy"
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for device_name in devices:
|
for device_name in devices:
|
||||||
@@ -72,21 +94,27 @@ class HardwareCustomizer:
|
|||||||
self._handle_device_selection(device_type if device_type != "Network" else "WiFi")
|
self._handle_device_selection(device_type if device_type != "Network" else "WiFi")
|
||||||
|
|
||||||
if self.selected_devices:
|
if self.selected_devices:
|
||||||
self.utils.head("Device Selection Summary")
|
content = "The following devices have been selected for your configuration:<br>"
|
||||||
print("")
|
content += "<table width='100%' cellpadding='4'>"
|
||||||
print("Selected devices:")
|
content += "<tr>"
|
||||||
print("")
|
content += "<td><b>Category</b></td>"
|
||||||
print("Type Device Device ID")
|
content += "<td><b>Device Name</b></td>"
|
||||||
print("------------------------------------------------------------------")
|
content += "<td><b>Device ID</b></td>"
|
||||||
|
content += "</tr>"
|
||||||
|
|
||||||
for device_type, device_dict in self.selected_devices.items():
|
for device_type, device_dict in self.selected_devices.items():
|
||||||
for device_name, device_props in device_dict.items():
|
for device_name, device_props in device_dict.items():
|
||||||
device_id = device_props.get("Device ID", "Unknown")
|
device_id = device_props.get("Device ID", "Unknown")
|
||||||
print("{:<13} {:<42} {}".format(device_type, device_name[:38], device_id))
|
content += "<tr>"
|
||||||
print("")
|
content += "<td>{}</td>".format(device_type)
|
||||||
print("All other devices of the same type have been disabled.")
|
content += "<td>{}</td>".format(device_name)
|
||||||
print("")
|
content += "<td>{}</td>".format(device_id)
|
||||||
self.utils.request_input()
|
content += "</tr>"
|
||||||
|
|
||||||
|
content += "</table>"
|
||||||
|
content += "<p><i>Note: Unselected devices in these categories have been disabled.</i></p>"
|
||||||
|
show_info("Hardware Configuration Summary", content)
|
||||||
|
|
||||||
return self.customized_hardware, self.disabled_devices, needs_oclp
|
return self.customized_hardware, self.disabled_devices, needs_oclp
|
||||||
|
|
||||||
def _get_device_combinations(self, device_indices):
|
def _get_device_combinations(self, device_indices):
|
||||||
@@ -114,10 +142,12 @@ class HardwareCustomizer:
|
|||||||
devices = self._get_compatible_devices(device_type)
|
devices = self._get_compatible_devices(device_type)
|
||||||
device_groups = None
|
device_groups = None
|
||||||
|
|
||||||
|
title = "Multiple {} Devices Detected".format(device_type)
|
||||||
|
content = []
|
||||||
|
|
||||||
if len(devices) > 1:
|
if len(devices) > 1:
|
||||||
print("\n*** Multiple {} Devices Detected".format(device_type))
|
|
||||||
if device_type == "WiFi" or device_type == "Bluetooth":
|
if device_type == "WiFi" or device_type == "Bluetooth":
|
||||||
print(f"macOS works best with only one {device_type} device enabled.")
|
content.append("macOS works best with only one {} device enabled.<br>".format(device_type))
|
||||||
elif device_type == "GPU":
|
elif device_type == "GPU":
|
||||||
_apu_index = None
|
_apu_index = None
|
||||||
_navi_22_indices = set()
|
_navi_22_indices = set()
|
||||||
@@ -148,7 +178,7 @@ class HardwareCustomizer:
|
|||||||
_other_indices.add(index)
|
_other_indices.add(index)
|
||||||
|
|
||||||
if _apu_index or _navi_22_indices:
|
if _apu_index or _navi_22_indices:
|
||||||
print("Multiple active GPUs can cause kext conflicts in macOS.")
|
content.append("Multiple active GPUs can cause kext conflicts in macOS.")
|
||||||
|
|
||||||
device_groups = []
|
device_groups = []
|
||||||
if _apu_index:
|
if _apu_index:
|
||||||
@@ -158,7 +188,7 @@ class HardwareCustomizer:
|
|||||||
if _navi_indices or _intel_gpu_indices or _other_indices:
|
if _navi_indices or _intel_gpu_indices or _other_indices:
|
||||||
device_groups.append(_navi_indices | _intel_gpu_indices | _other_indices)
|
device_groups.append(_navi_indices | _intel_gpu_indices | _other_indices)
|
||||||
|
|
||||||
selected_devices = self._select_device(device_type, devices, device_groups)
|
selected_devices = self._select_device(device_type, devices, device_groups, title, content)
|
||||||
if selected_devices:
|
if selected_devices:
|
||||||
for selected_device in selected_devices:
|
for selected_device in selected_devices:
|
||||||
if not device_type in self.selected_devices:
|
if not device_type in self.selected_devices:
|
||||||
@@ -185,14 +215,15 @@ class HardwareCustomizer:
|
|||||||
|
|
||||||
return compatible_devices
|
return compatible_devices
|
||||||
|
|
||||||
def _select_device(self, device_type, devices, device_groups=None):
|
def _select_device(self, device_type, devices, device_groups=None, title=None, content=None):
|
||||||
print("")
|
self.utils.log_message("[HARDWARE CUSTOMIZATION] Starting device selection for {}".format(device_type), level="INFO")
|
||||||
if device_groups:
|
if device_groups:
|
||||||
print("Please select a {} combination configuration:".format(device_type))
|
content.append("Please select a {} combination configuration:".format(device_type))
|
||||||
else:
|
else:
|
||||||
print("Please select which {} device you want to use:".format(device_type))
|
content.append("Please select which {} device you want to use:".format(device_type))
|
||||||
print("")
|
|
||||||
|
options = []
|
||||||
|
|
||||||
if device_groups:
|
if device_groups:
|
||||||
valid_combinations = []
|
valid_combinations = []
|
||||||
|
|
||||||
@@ -230,67 +261,48 @@ class HardwareCustomizer:
|
|||||||
|
|
||||||
valid_combinations.sort(key=lambda x: (len(x[0]), x[2][0]))
|
valid_combinations.sort(key=lambda x: (len(x[0]), x[2][0]))
|
||||||
|
|
||||||
for idx, (group_devices, _, group_compatibility) in enumerate(valid_combinations, start=1):
|
for group_devices, group_indices, group_compatibility in valid_combinations:
|
||||||
print("{}. {}".format(idx, " + ".join(group_devices)))
|
option = "<b>{}</b>".format(" + ".join(group_devices))
|
||||||
if group_compatibility:
|
if group_compatibility:
|
||||||
print(" Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility(group_compatibility)))
|
option += "<br>Compatibility: {}".format(self.show_macos_compatibility(group_compatibility))
|
||||||
if len(group_devices) == 1:
|
if len(group_devices) == 1:
|
||||||
device_props = devices[group_devices[0]]
|
device_props = devices[group_devices[0]]
|
||||||
if device_props.get("OCLP Compatibility"):
|
if device_props.get("OCLP Compatibility"):
|
||||||
oclp_compatibility = device_props.get("OCLP Compatibility")
|
option += "<br>OCLP Compatibility: {}".format(self.show_macos_compatibility((device_props.get("OCLP Compatibility")[0], os_data.get_lowest_darwin_version())))
|
||||||
if self.utils.parse_darwin_version(oclp_compatibility[0]) > self.utils.parse_darwin_version(group_compatibility[0]):
|
options.append(option)
|
||||||
print(" OCLP Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version()))))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
choice = self.utils.request_input(f"Select a {device_type} combination (1-{len(valid_combinations)}): ")
|
|
||||||
|
|
||||||
try:
|
|
||||||
choice_num = int(choice)
|
|
||||||
if 1 <= choice_num <= len(valid_combinations):
|
|
||||||
selected_devices, _, _ = valid_combinations[choice_num - 1]
|
|
||||||
|
|
||||||
for device in devices:
|
|
||||||
if device not in selected_devices:
|
|
||||||
self._disable_device(device_type, device, devices[device])
|
|
||||||
|
|
||||||
return selected_devices
|
|
||||||
else:
|
|
||||||
print("Invalid option. Please try again.")
|
|
||||||
except ValueError:
|
|
||||||
print("Please enter a valid number.")
|
|
||||||
else:
|
else:
|
||||||
for index, device_name in enumerate(devices, start=1):
|
for device_name, device_props in devices.items():
|
||||||
device_props = devices[device_name]
|
|
||||||
compatibility = device_props.get("Compatibility")
|
compatibility = device_props.get("Compatibility")
|
||||||
|
|
||||||
print("{}. {}".format(index, device_name))
|
option = "<b>{}</b>".format(device_name)
|
||||||
print(" Device ID: {}".format(device_props.get("Device ID", "Unknown")))
|
option += "<br>Device ID: {}".format(device_props.get("Device ID", "Unknown"))
|
||||||
print(" Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility(compatibility)))
|
option += "<br>Compatibility: {}".format(self.show_macos_compatibility(compatibility))
|
||||||
|
|
||||||
if device_props.get("OCLP Compatibility"):
|
if device_props.get("OCLP Compatibility"):
|
||||||
oclp_compatibility = device_props.get("OCLP Compatibility")
|
oclp_compatibility = device_props.get("OCLP Compatibility")
|
||||||
if self.utils.parse_darwin_version(oclp_compatibility[0]) > self.utils.parse_darwin_version(compatibility[0]):
|
if self.utils.parse_darwin_version(oclp_compatibility[0]) > self.utils.parse_darwin_version(compatibility[0]):
|
||||||
print(" OCLP Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version()))))
|
option += "<br>OCLP Compatibility: {}".format(self.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version())))
|
||||||
print()
|
options.append(option)
|
||||||
|
|
||||||
|
self.utils.log_message("[HARDWARE CUSTOMIZATION] Options: {}".format(", ".join(option.split("<br>")[0].replace("<b>", "").replace("</b>", "").strip() for option in options)), level="INFO")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
choice = self.utils.request_input(f"Select a {device_type} device (1-{len(devices)}): ")
|
choice_num = show_options_dialog(title, "<br>".join(content), options, default_index=len(options) - 1)
|
||||||
|
|
||||||
try:
|
if choice_num is None:
|
||||||
choice_num = int(choice)
|
continue
|
||||||
if 1 <= choice_num <= len(devices):
|
|
||||||
selected_device = list(devices)[choice_num - 1]
|
if device_groups:
|
||||||
|
selected_devices, _, _ = valid_combinations[choice_num]
|
||||||
for device in devices:
|
else:
|
||||||
if device != selected_device:
|
selected_devices = [list(devices)[choice_num]]
|
||||||
self._disable_device(device_type, device, devices[device])
|
|
||||||
|
for device in devices:
|
||||||
return [selected_device]
|
if device not in selected_devices:
|
||||||
else:
|
self._disable_device(device_type, device, devices[device])
|
||||||
print("Invalid option. Please try again.")
|
|
||||||
except ValueError:
|
self.utils.log_message("[HARDWARE CUSTOMIZATION] Selected devices: {}".format(", ".join(selected_devices)), level="INFO")
|
||||||
print("Please enter a valid number.")
|
return selected_devices
|
||||||
|
|
||||||
def _disable_device(self, device_type, device_name, device_props):
|
def _disable_device(self, device_type, device_name, device_props):
|
||||||
if device_type == "WiFi":
|
if device_type == "WiFi":
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import json
|
|||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
|
|
||||||
class IntegrityChecker:
|
class IntegrityChecker:
|
||||||
def __init__(self):
|
def __init__(self, utils_instance=None):
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
|
|
||||||
def get_sha256(self, file_path, block_size=65536):
|
def get_sha256(self, file_path, block_size=65536):
|
||||||
if not os.path.exists(file_path) or os.path.isdir(file_path):
|
if not os.path.exists(file_path) or os.path.isdir(file_path):
|
||||||
@@ -17,7 +17,7 @@ class IntegrityChecker:
|
|||||||
sha256.update(block)
|
sha256.update(block)
|
||||||
return sha256.hexdigest()
|
return sha256.hexdigest()
|
||||||
|
|
||||||
def generate_folder_manifest(self, folder_path, manifest_path=None):
|
def generate_folder_manifest(self, folder_path, manifest_path=None, save_manifest=True):
|
||||||
if not os.path.isdir(folder_path):
|
if not os.path.isdir(folder_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -26,8 +26,15 @@ class IntegrityChecker:
|
|||||||
|
|
||||||
manifest_data = {}
|
manifest_data = {}
|
||||||
for root, _, files in os.walk(folder_path):
|
for root, _, files in os.walk(folder_path):
|
||||||
|
if '.git' in root or "__pycache__" in root:
|
||||||
|
continue
|
||||||
|
|
||||||
for name in files:
|
for name in files:
|
||||||
|
if '.git' in name or ".pyc" in name:
|
||||||
|
continue
|
||||||
|
|
||||||
file_path = os.path.join(root, name)
|
file_path = os.path.join(root, name)
|
||||||
|
|
||||||
relative_path = os.path.relpath(file_path, folder_path).replace('\\', '/')
|
relative_path = os.path.relpath(file_path, folder_path).replace('\\', '/')
|
||||||
|
|
||||||
if relative_path == os.path.basename(manifest_path):
|
if relative_path == os.path.basename(manifest_path):
|
||||||
@@ -35,7 +42,8 @@ class IntegrityChecker:
|
|||||||
|
|
||||||
manifest_data[relative_path] = self.get_sha256(file_path)
|
manifest_data[relative_path] = self.get_sha256(file_path)
|
||||||
|
|
||||||
self.utils.write_file(manifest_path, manifest_data)
|
if save_manifest:
|
||||||
|
self.utils.write_file(manifest_path, manifest_data)
|
||||||
return manifest_data
|
return manifest_data
|
||||||
|
|
||||||
def verify_folder_integrity(self, folder_path, manifest_path=None):
|
def verify_folder_integrity(self, folder_path, manifest_path=None):
|
||||||
@@ -83,4 +91,4 @@ class IntegrityChecker:
|
|||||||
|
|
||||||
is_valid = not any(issues.values())
|
is_valid = not any(issues.values())
|
||||||
|
|
||||||
return is_valid, issues
|
return is_valid, issues
|
||||||
@@ -6,6 +6,7 @@ from Scripts.datasets import codec_layouts
|
|||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import random
|
||||||
|
|
||||||
try:
|
try:
|
||||||
long
|
long
|
||||||
@@ -14,9 +15,11 @@ except NameError:
|
|||||||
long = int
|
long = int
|
||||||
unicode = str
|
unicode = str
|
||||||
|
|
||||||
|
from Scripts.custom_dialogs import show_options_dialog, show_info, show_confirmation, show_checklist_dialog
|
||||||
|
|
||||||
class KextMaestro:
|
class KextMaestro:
|
||||||
def __init__(self):
|
def __init__(self, utils_instance=None):
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
self.matching_keys = [
|
self.matching_keys = [
|
||||||
"IOPCIMatch",
|
"IOPCIMatch",
|
||||||
"IONameMatch",
|
"IONameMatch",
|
||||||
@@ -77,6 +80,52 @@ class KextMaestro:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _select_audio_codec_layout(self, hardware_report, default_layout_id=None):
|
||||||
|
codec_id = None
|
||||||
|
audio_controller_properties = None
|
||||||
|
|
||||||
|
for codec_properties in hardware_report.get("Sound", {}).values():
|
||||||
|
if codec_properties.get("Device ID") in codec_layouts.data:
|
||||||
|
codec_id = codec_properties.get("Device ID")
|
||||||
|
|
||||||
|
if codec_properties.get("Controller Device ID"):
|
||||||
|
for device_name, device_properties in hardware_report.get("System Devices", {}).items():
|
||||||
|
if device_properties.get("Device ID") == codec_properties.get("Controller Device ID"):
|
||||||
|
audio_controller_properties = device_properties
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
|
available_layouts = codec_layouts.data.get(codec_id)
|
||||||
|
|
||||||
|
if not available_layouts:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
options = []
|
||||||
|
default_index = 0
|
||||||
|
|
||||||
|
if default_layout_id is None:
|
||||||
|
recommended_authors = ("Mirone", "InsanelyDeepak", "Toleda", "DalianSky")
|
||||||
|
recommended_layouts = [layout for layout in available_layouts if self.utils.contains_any(recommended_authors, layout.comment)]
|
||||||
|
default_layout_id = random.choice(recommended_layouts or available_layouts).id
|
||||||
|
|
||||||
|
for i, layout in enumerate(available_layouts):
|
||||||
|
options.append("{} - {}".format(layout.id, layout.comment))
|
||||||
|
if layout.id == default_layout_id:
|
||||||
|
default_index = i
|
||||||
|
|
||||||
|
while True:
|
||||||
|
content = "For best audio quality, please try multiple layouts to determine which works best with your hardware in post-install."
|
||||||
|
|
||||||
|
selected_index = show_options_dialog(
|
||||||
|
title="Choosing Codec Layout ID",
|
||||||
|
content=content,
|
||||||
|
options=options,
|
||||||
|
default_index=default_index
|
||||||
|
)
|
||||||
|
|
||||||
|
if selected_index is not None:
|
||||||
|
return available_layouts[selected_index].id, audio_controller_properties
|
||||||
|
|
||||||
def check_kext(self, index, target_darwin_version, allow_unsupported_kexts=False):
|
def check_kext(self, index, target_darwin_version, allow_unsupported_kexts=False):
|
||||||
kext = self.kexts[index]
|
kext = self.kexts[index]
|
||||||
|
|
||||||
@@ -96,9 +145,7 @@ class KextMaestro:
|
|||||||
other_kext.checked = False
|
other_kext.checked = False
|
||||||
|
|
||||||
def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi_patches):
|
def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi_patches):
|
||||||
self.utils.head("Select Required Kernel Extensions")
|
self.utils.log_message("[KEXT MAESTRO] Checking for required kernel extensions...", level="INFO")
|
||||||
print("")
|
|
||||||
print("Checking for required kernel extensions...")
|
|
||||||
|
|
||||||
for kext in self.kexts:
|
for kext in self.kexts:
|
||||||
kext.checked = kext.required
|
kext.checked = kext.required
|
||||||
@@ -122,24 +169,26 @@ class KextMaestro:
|
|||||||
for codec_properties in hardware_report.get("Sound", {}).values():
|
for codec_properties in hardware_report.get("Sound", {}).values():
|
||||||
if codec_properties.get("Device ID") in codec_layouts.data:
|
if codec_properties.get("Device ID") in codec_layouts.data:
|
||||||
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
|
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
|
||||||
print("\n\033[1;93mNote:\033[0m Since macOS Tahoe 26 DP2, Apple has removed AppleHDA kext and uses the Apple T2 chip for audio management.")
|
content = (
|
||||||
print("To use AppleALC, you must rollback AppleHDA. Alternatively, you can use VoodooHDA.")
|
"Since macOS Tahoe 26 DP2, Apple has removed AppleHDA and uses the Apple T2 chip for audio management.<br>"
|
||||||
print("")
|
"Therefore, AppleALC is no longer functional until you rollback AppleHDA."
|
||||||
print("1. \033[1mAppleALC\033[0m - Requires AppleHDA rollback with \033[1;93mOpenCore Legacy Patcher\033[0m")
|
)
|
||||||
print("2. \033[1mVoodooHDA\033[0m - Lower audio quality, manual injection to /Library/Extensions")
|
options = [
|
||||||
print("")
|
"<b>AppleALC</b> - Requires rollback AppleHDA with <b>OpenCore Legacy Patcher</b>",
|
||||||
while True:
|
"<b>VoodooHDA</b> - Lower audio quality than use AppleHDA, injection kext into <b>/Library/Extensions</b>"
|
||||||
kext_option = self.utils.request_input("Select audio kext for your system: ").strip()
|
]
|
||||||
if kext_option == "1":
|
result = show_options_dialog("Audio Kext Selection", content, options, default_index=0)
|
||||||
needs_oclp = True
|
if result == 0:
|
||||||
selected_kexts.append("AppleALC")
|
needs_oclp = True
|
||||||
break
|
selected_kexts.append("AppleALC")
|
||||||
elif kext_option == "2":
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
|
|
||||||
else:
|
else:
|
||||||
selected_kexts.append("AppleALC")
|
selected_kexts.append("AppleALC")
|
||||||
|
|
||||||
|
if "AppleALC" in selected_kexts:
|
||||||
|
audio_layout_id, audio_controller_properties = self._select_audio_codec_layout(hardware_report)
|
||||||
|
else:
|
||||||
|
audio_layout_id = None
|
||||||
|
audio_controller_properties = None
|
||||||
|
|
||||||
if "AMD" in hardware_report.get("CPU").get("Manufacturer") and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("21.4.0") or \
|
if "AMD" in hardware_report.get("CPU").get("Manufacturer") and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("21.4.0") or \
|
||||||
int(hardware_report.get("CPU").get("CPU Count")) > 1 and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("19.0.0"):
|
int(hardware_report.get("CPU").get("CPU Count")) > 1 and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("19.0.0"):
|
||||||
@@ -166,66 +215,54 @@ class KextMaestro:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if gpu_props.get("Codename") in {"Navi 21", "Navi 23"}:
|
if gpu_props.get("Codename") in {"Navi 21", "Navi 23"}:
|
||||||
print("\n*** Found {} is AMD {} GPU.".format(gpu_name, gpu_props.get("Codename")))
|
content = (
|
||||||
print("")
|
"<span style='color:red font-weight:bold'>Important: Black Screen Fix</span><br>"
|
||||||
print("\033[91mImportant: Black Screen Fix\033[0m")
|
"If you experience a black screen after verbose mode:<br>"
|
||||||
print("If you experience a black screen after verbose mode:")
|
"1. Use ProperTree to open config.plist<br>"
|
||||||
print(" 1. Use ProperTree to open config.plist")
|
"2. Navigate to NVRAM -> Add -> 7C436110-AB2A-4BBB-A880-FE41995C9F82 -> boot-args<br>"
|
||||||
print(" 2. Navigate to NVRAM -> Add -> 7C436110-AB2A-4BBB-A880-FE41995C9F82 -> boot-args")
|
"3. Remove \"-v debug=0x100 keepsyms=1\" from boot-args<br><br>"
|
||||||
print(" 3. Remove \"-v debug=0x100 keepsyms=1\" from boot-args")
|
).format(gpu_name, gpu_props.get("Codename"))
|
||||||
print("")
|
|
||||||
|
options = [
|
||||||
|
"<b>NootRX</b> - Uses latest GPU firmware",
|
||||||
|
"<b>WhateverGreen</b> - Uses original Apple firmware",
|
||||||
|
]
|
||||||
|
|
||||||
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
|
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
|
||||||
recommended_option = 1
|
content += (
|
||||||
recommended_name = "NootRX"
|
"Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD {} GPUs.<br>"
|
||||||
max_option = 3
|
"To avoid this, you can use NootRX or choose not to install a GPU kext."
|
||||||
print("\033[1;93mNote:\033[0m Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD {} GPUs.".format(gpu_props.get("Codename")))
|
).format(gpu_props.get("Codename"))
|
||||||
print("To avoid this, you can use NootRX or choose not to install a GPU kext.")
|
options.append("<b>Don't use any kext</b>")
|
||||||
print("")
|
recommended_option = 0
|
||||||
print("1. \033[1mNootRX\033[0m - Uses latest GPU firmware")
|
|
||||||
print("2. \033[1mWhateverGreen\033[0m - Uses original Apple firmware")
|
|
||||||
print("3. \033[1mDon't use any kext\033[0m")
|
|
||||||
else:
|
else:
|
||||||
recommended_option = 2
|
content += (
|
||||||
recommended_name = "WhateverGreen"
|
"AMD {} GPUs have two available kext options:<br>"
|
||||||
max_option = 2
|
"You can try different kexts after installation to find the best one for your system."
|
||||||
print("\033[1;93mNote:\033[0m")
|
).format(gpu_props.get("Codename"))
|
||||||
print("- AMD {} GPUs have two available kext options:".format(gpu_props.get("Codename")))
|
recommended_option = 1
|
||||||
print("- You can try different kexts after installation to find the best one for your system")
|
|
||||||
print("")
|
|
||||||
print("1. \033[1mNootRX\033[0m - Uses latest GPU firmware")
|
|
||||||
print("2. \033[1mWhateverGreen\033[0m - Uses original Apple firmware")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
if any(other_gpu_props.get("Manufacturer") == "Intel" for other_gpu_props in hardware_report.get("GPU", {}).values()):
|
if any(other_gpu_props.get("Manufacturer") == "Intel" for other_gpu_props in hardware_report.get("GPU", {}).values()):
|
||||||
print("\033[91mImportant:\033[0m NootRX kext is not compatible with Intel GPUs")
|
show_info("NootRX Kext Warning", "NootRX kext is not compatible with Intel GPUs.<br>Automatically selecting WhateverGreen kext due to Intel GPU compatibility.")
|
||||||
print("Automatically selecting WhateverGreen kext due to Intel GPU compatibility")
|
selected_kexts.append("WhateverGreen")
|
||||||
print("")
|
|
||||||
self.utils.request_input("Press Enter to continue...")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
kext_option = self.utils.request_input("Select kext for your AMD {} GPU (default: {}): ".format(gpu_props.get("Codename"), recommended_name)).strip() or str(recommended_option)
|
result = show_options_dialog("AMD GPU Kext Selection", content, options, default_index=recommended_option)
|
||||||
|
|
||||||
if kext_option.isdigit() and 0 < int(kext_option) < max_option + 1:
|
if result == 0:
|
||||||
selected_option = int(kext_option)
|
|
||||||
else:
|
|
||||||
print("\033[93mInvalid selection, using recommended option: {}\033[0m".format(recommended_option))
|
|
||||||
selected_option = recommended_option
|
|
||||||
|
|
||||||
if selected_option == 1:
|
|
||||||
selected_kexts.append("NootRX")
|
selected_kexts.append("NootRX")
|
||||||
elif selected_option == 2:
|
elif result == 1:
|
||||||
selected_kexts.append("WhateverGreen")
|
selected_kexts.append("WhateverGreen")
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
|
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"):
|
||||||
print("\n*** Found {} is AMD {} GPU.".format(gpu_name, gpu_props.get("Codename")))
|
content = (
|
||||||
print("")
|
"Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD GPUs.<br>"
|
||||||
print("\033[1;93mNote:\033[0m Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD GPUs.")
|
"The current recommendation is to not use WhateverGreen.<br>"
|
||||||
print("The current recommendation is to not use WhateverGreen.")
|
"However, you can still try adding it to see if it works on your system."
|
||||||
print("However, you can still try adding it to see if it works on your system.")
|
)
|
||||||
print("")
|
show_info("AMD GPU Kext Warning", content)
|
||||||
self.utils.request_input("Press Enter to continue...")
|
|
||||||
break
|
break
|
||||||
|
|
||||||
selected_kexts.append("WhateverGreen")
|
selected_kexts.append("WhateverGreen")
|
||||||
@@ -252,44 +289,35 @@ class KextMaestro:
|
|||||||
elif device_id in pci_data.BroadcomWiFiIDs[16:18] and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("20.0.0"):
|
elif device_id in pci_data.BroadcomWiFiIDs[16:18] and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("20.0.0"):
|
||||||
selected_kexts.append("AirportBrcmFixup")
|
selected_kexts.append("AirportBrcmFixup")
|
||||||
elif device_id in pci_data.IntelWiFiIDs:
|
elif device_id in pci_data.IntelWiFiIDs:
|
||||||
print("\n*** Found {} is Intel WiFi device.".format(network_name))
|
airport_itlwm_content = (
|
||||||
print("")
|
"<b>AirportItlwm</b> - Uses native WiFi settings menu<br>"
|
||||||
print("\033[1;93mNote:\033[0m Intel WiFi devices have two available kext options:")
|
"• Provides Handoff, Universal Clipboard, Location Services, Instant Hotspot support<br>"
|
||||||
print("")
|
"• Supports enterprise-level security<br>"
|
||||||
print("1. \033[1mAirportItlwm\033[0m - Uses native WiFi settings menu")
|
)
|
||||||
print(" • Provides Handoff, Universal Clipboard, Location Services, Instant Hotspot support")
|
|
||||||
print(" • Supports enterprise-level security")
|
|
||||||
|
|
||||||
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"):
|
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"):
|
||||||
print(" • \033[91mSince macOS Sequoia 15\033[0m: Can work with OCLP root patch but may cause issues")
|
airport_itlwm_content += "• <span style='color:red'>Since macOS Sequoia 15</span>: Can work with OCLP root patch but may cause issues"
|
||||||
elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"):
|
elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"):
|
||||||
print(" • \033[91mOn macOS Sonoma 14\033[0m: iServices won't work unless using OCLP root patch")
|
airport_itlwm_content += "• <span style='color:red'>On macOS Sonoma 14</span>: iServices won't work unless using OCLP root patch"
|
||||||
|
|
||||||
print("")
|
|
||||||
print("2. \033[1mitlwm\033[0m - More stable overall")
|
|
||||||
print(" • Works with HeliPort app instead of native WiFi settings menu")
|
|
||||||
print(" • No Apple Continuity features and enterprise-level security")
|
|
||||||
print(" • Can connect to Hidden Networks")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
recommended_option = 2 if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0") else 1
|
itlwm_content = (
|
||||||
recommended_name = "itlwm" if recommended_option == 2 else "AirportItlwm"
|
"<b>itlwm</b> - More stable overall<br>"
|
||||||
|
"• Works with <b>HeliPort</b> app instead of native WiFi settings menu<br>"
|
||||||
|
"• No Apple Continuity features and enterprise-level security<br>"
|
||||||
|
"• Can connect to Hidden Networks"
|
||||||
|
)
|
||||||
|
|
||||||
|
recommended_option = 1 if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0") else 0
|
||||||
|
options = [airport_itlwm_content, itlwm_content]
|
||||||
|
|
||||||
if "Beta" in os_data.get_macos_name_by_darwin(macos_version):
|
if "Beta" in os_data.get_macos_name_by_darwin(macos_version):
|
||||||
print("\033[91mImportant:\033[0m For macOS Beta versions, only itlwm kext is supported")
|
show_info("Intel WiFi Kext Selection", "For macOS Beta versions, only itlwm kext is supported.")
|
||||||
print("")
|
selected_option = 1
|
||||||
self.utils.request_input("Press Enter to continue...")
|
|
||||||
selected_option = recommended_option
|
|
||||||
else:
|
else:
|
||||||
kext_option = self.utils.request_input("Select kext for your Intel WiFi device (default: {}): ".format(recommended_name)).strip() or str(recommended_option)
|
result = show_options_dialog("Intel WiFi Kext Selection", "Intel WiFi devices have two available kext options:", options, default_index=recommended_option)
|
||||||
|
selected_option = result if result is not None else recommended_option
|
||||||
if kext_option.isdigit() and 0 < int(kext_option) < 3:
|
|
||||||
selected_option = int(kext_option)
|
if selected_option == 1:
|
||||||
else:
|
|
||||||
print("\033[91mInvalid selection, using recommended option: {}\033[0m".format(recommended_option))
|
|
||||||
selected_option = recommended_option
|
|
||||||
|
|
||||||
if selected_option == 2:
|
|
||||||
selected_kexts.append("itlwm")
|
selected_kexts.append("itlwm")
|
||||||
else:
|
else:
|
||||||
selected_kexts.append("AirportItlwm")
|
selected_kexts.append("AirportItlwm")
|
||||||
@@ -297,18 +325,12 @@ class KextMaestro:
|
|||||||
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"):
|
if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"):
|
||||||
selected_kexts.append("IOSkywalkFamily")
|
selected_kexts.append("IOSkywalkFamily")
|
||||||
elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"):
|
elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"):
|
||||||
print("")
|
content = (
|
||||||
print("\033[1;93mNote:\033[0m Since macOS Sonoma 14, iServices won't work with AirportItlwm without patches")
|
"Since macOS Sonoma 14, iServices won't work with AirportItlwm without patches.<br><br>"
|
||||||
print("")
|
"Apply OCLP root patch to fix iServices?"
|
||||||
while True:
|
)
|
||||||
option = self.utils.request_input("Apply OCLP root patch to fix iServices? (yes/No): ").strip().lower()
|
if show_confirmation("OpenCore Legacy Patcher Required", content):
|
||||||
if option == "yes":
|
selected_kexts.append("IOSkywalkFamily")
|
||||||
selected_kexts.append("IOSkywalkFamily")
|
|
||||||
break
|
|
||||||
elif option == "no":
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
|
|
||||||
elif device_id in pci_data.AtherosWiFiIDs[:8]:
|
elif device_id in pci_data.AtherosWiFiIDs[:8]:
|
||||||
selected_kexts.append("corecaptureElCap")
|
selected_kexts.append("corecaptureElCap")
|
||||||
if self.utils.parse_darwin_version(macos_version) > self.utils.parse_darwin_version("20.99.99"):
|
if self.utils.parse_darwin_version(macos_version) > self.utils.parse_darwin_version("20.99.99"):
|
||||||
@@ -424,7 +446,7 @@ class KextMaestro:
|
|||||||
for name in selected_kexts:
|
for name in selected_kexts:
|
||||||
self.check_kext(kext_data.kext_index_by_name.get(name), macos_version, allow_unsupported_kexts)
|
self.check_kext(kext_data.kext_index_by_name.get(name), macos_version, allow_unsupported_kexts)
|
||||||
|
|
||||||
return needs_oclp
|
return needs_oclp, audio_layout_id, audio_controller_properties
|
||||||
|
|
||||||
def install_kexts_to_efi(self, macos_version, kexts_directory):
|
def install_kexts_to_efi(self, macos_version, kexts_directory):
|
||||||
for kext in self.kexts:
|
for kext in self.kexts:
|
||||||
@@ -639,72 +661,65 @@ class KextMaestro:
|
|||||||
if not incompatible_kexts:
|
if not incompatible_kexts:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
while True:
|
content = (
|
||||||
self.utils.head("Kext Compatibility Check")
|
"List of incompatible kexts for the current macOS version ({}):<br>"
|
||||||
print("\nIncompatible kexts for the current macOS version ({}):\n".format(target_darwin_version))
|
"<ul>"
|
||||||
|
).format(target_darwin_version)
|
||||||
for index, (kext_name, is_lilu_dependent) in enumerate(incompatible_kexts, start=1):
|
|
||||||
print("{:2}. {:25}{}".format(index, kext_name, " - Lilu Plugin" if is_lilu_dependent else ""))
|
for index, (kext_name, is_lilu_dependent) in enumerate(incompatible_kexts):
|
||||||
|
content += "<li><b>{}. {}</b>".format(index + 1, kext_name)
|
||||||
print("\n\033[1;93mNote:\033[0m")
|
if is_lilu_dependent:
|
||||||
print("- With Lilu plugins, using the \"-lilubetaall\" boot argument will force them to load.")
|
content += " - Lilu Plugin"
|
||||||
print("- Forcing unsupported kexts can cause system instability. \033[0;31mProceed with caution.\033[0m")
|
content += "</li>"
|
||||||
print("")
|
|
||||||
|
content += (
|
||||||
option = self.utils.request_input("Do you want to force load {} on the unsupported macOS version? (yes/No): ".format("these kexts" if len(incompatible_kexts) > 1 else "this kext"))
|
"</ul><br>"
|
||||||
|
"<b>Note:</b><br>"
|
||||||
if option.lower() == "yes":
|
"• With Lilu plugins, using the \"-lilubetaall\" boot argument will force them to load.<br>"
|
||||||
return True
|
"• Forcing unsupported kexts can cause system instability. <b><span style='color:red'>Proceed with caution.</span></b><br><br>"
|
||||||
elif option.lower() == "no":
|
"Do you want to force load {} on the unsupported macOS version?"
|
||||||
return False
|
).format("these kexts" if len(incompatible_kexts) > 1 else "this kext")
|
||||||
|
|
||||||
|
return show_confirmation("Incompatible Kexts", content, yes_text="Yes", no_text="No")
|
||||||
|
|
||||||
def kext_configuration_menu(self, macos_version):
|
def kext_configuration_menu(self, macos_version):
|
||||||
current_category = None
|
content = (
|
||||||
|
"Select kernel extensions (kexts) for your system.<br>"
|
||||||
|
"Grayed-out items are not supported by the current macOS version ({}).<br><br>"
|
||||||
|
"<b>Note:</b><br>"
|
||||||
|
"• When a plugin of a kext is selected, the entire kext will be automatically selected."
|
||||||
|
).format(macos_version)
|
||||||
|
|
||||||
|
checklist_items = []
|
||||||
|
|
||||||
|
for kext in self.kexts:
|
||||||
|
is_supported = self.utils.parse_darwin_version(kext.min_darwin_version) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(kext.max_darwin_version)
|
||||||
|
|
||||||
|
display_text = "{} - {}".format(kext.name, kext.description)
|
||||||
|
if not is_supported:
|
||||||
|
display_text += " (Unsupported)"
|
||||||
|
|
||||||
|
checklist_items.append({
|
||||||
|
"label": display_text,
|
||||||
|
"category": kext.category if kext.category else "Uncategorized",
|
||||||
|
"supported": is_supported
|
||||||
|
})
|
||||||
|
|
||||||
|
checked_indices = [i for i, kext in enumerate(self.kexts) if kext.checked]
|
||||||
|
|
||||||
|
selected_indices = show_checklist_dialog("Configure Kernel Extensions", content, checklist_items, checked_indices)
|
||||||
|
|
||||||
|
self.utils.log_message("[KEXT MAESTRO] Selected kexts: {}".format(selected_indices), level="INFO")
|
||||||
|
if selected_indices is None:
|
||||||
|
return
|
||||||
|
|
||||||
while True:
|
newly_checked = [i for i in selected_indices if i not in checked_indices]
|
||||||
contents = []
|
|
||||||
contents.append("")
|
allow_unsupported_kexts = self.verify_kext_compatibility(newly_checked, macos_version)
|
||||||
contents.append("List of available kexts:")
|
|
||||||
for index, kext in enumerate(self.kexts, start=1):
|
for i, kext in enumerate(self.kexts):
|
||||||
if kext.category != current_category:
|
if i not in selected_indices and kext.checked and not kext.required:
|
||||||
current_category = kext.category
|
self.uncheck_kext(i)
|
||||||
category_header = "Category: {}".format(current_category if current_category else "Uncategorized")
|
|
||||||
contents.append(f"\n{category_header}\n" + "=" * len(category_header))
|
for i in selected_indices:
|
||||||
checkbox = "[*]" if kext.checked else "[ ]"
|
self.check_kext(i, macos_version, allow_unsupported_kexts)
|
||||||
|
|
||||||
line = "{} {:2}. {:35} - {:60}".format(checkbox, index, kext.name, kext.description)
|
|
||||||
if kext.checked:
|
|
||||||
line = "\033[1;32m{}\033[0m".format(line)
|
|
||||||
elif not self.utils.parse_darwin_version(kext.min_darwin_version) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(kext.max_darwin_version):
|
|
||||||
line = "\033[90m{}\033[0m".format(line)
|
|
||||||
contents.append(line)
|
|
||||||
contents.append("")
|
|
||||||
contents.append("\033[1;93mNote:\033[0m")
|
|
||||||
contents.append("- Lines in gray indicate kexts that are not supported by the current macOS version ({}).".format(macos_version))
|
|
||||||
contents.append("- When a plugin of a kext is selected, the entire kext will be automatically selected.")
|
|
||||||
contents.append("- You can select multiple kexts by entering their indices separated by commas (e.g., '1, 2, 3').")
|
|
||||||
contents.append("")
|
|
||||||
contents.append("B. Back")
|
|
||||||
contents.append("Q. Quit")
|
|
||||||
contents.append("")
|
|
||||||
content = "\n".join(contents)
|
|
||||||
|
|
||||||
self.utils.adjust_window_size(content)
|
|
||||||
self.utils.head("Configure Kernel Extensions", resize=False)
|
|
||||||
print(content)
|
|
||||||
option = self.utils.request_input("Select your option: ")
|
|
||||||
if option.lower() == "b":
|
|
||||||
return
|
|
||||||
if option.lower() == "q":
|
|
||||||
self.utils.exit_program()
|
|
||||||
indices = [int(i.strip()) -1 for i in option.split(",") if i.strip().isdigit()]
|
|
||||||
|
|
||||||
allow_unsupported_kexts = self.verify_kext_compatibility(indices, macos_version)
|
|
||||||
|
|
||||||
for index in indices:
|
|
||||||
if index >= 0 and index < len(self.kexts):
|
|
||||||
kext = self.kexts[index]
|
|
||||||
if kext.checked and not kext.required:
|
|
||||||
self.uncheck_kext(index)
|
|
||||||
else:
|
|
||||||
self.check_kext(index, macos_version, allow_unsupported_kexts)
|
|
||||||
15
Scripts/pages/__init__.py
Normal file
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
|
import re
|
||||||
|
|
||||||
class ReportValidator:
|
class ReportValidator:
|
||||||
def __init__(self):
|
def __init__(self, utils_instance=None):
|
||||||
self.errors = []
|
self.errors = []
|
||||||
self.warnings = []
|
self.warnings = []
|
||||||
self.u = utils.Utils()
|
self.u = utils_instance if utils_instance else utils.Utils()
|
||||||
|
|
||||||
self.PATTERNS = {
|
self.PATTERNS = {
|
||||||
"not_empty": r".+",
|
"not_empty": r".+",
|
||||||
@@ -244,17 +244,17 @@ class ReportValidator:
|
|||||||
if expected_type:
|
if expected_type:
|
||||||
if not isinstance(data, expected_type):
|
if not isinstance(data, expected_type):
|
||||||
type_name = expected_type.__name__ if hasattr(expected_type, "__name__") else str(expected_type)
|
type_name = expected_type.__name__ if hasattr(expected_type, "__name__") else str(expected_type)
|
||||||
self.errors.append(f"{path}: Expected type {type_name}, got {type(data).__name__}")
|
self.errors.append("{}: Expected type {}, got {}".format(path, type_name, type(data).__name__))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
pattern = rule.get("pattern")
|
pattern = rule.get("pattern")
|
||||||
if pattern is not None:
|
if pattern is not None:
|
||||||
if not re.match(pattern, data):
|
if not re.match(pattern, data):
|
||||||
self.errors.append(f"{path}: Value '{data}' does not match pattern '{pattern}'")
|
self.errors.append("{}: Value '{}' does not match pattern '{}'".format(path, data, pattern))
|
||||||
return None
|
return None
|
||||||
elif not re.match(self.PATTERNS["not_empty"], data):
|
elif not re.match(self.PATTERNS["not_empty"], data):
|
||||||
self.errors.append(f"{path}: Value '{data}' does not match pattern '{self.PATTERNS['not_empty']}'")
|
self.errors.append("{}: Value '{}' does not match pattern '{}'".format(path, data, self.PATTERNS["not_empty"]))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
cleaned_data = data
|
cleaned_data = data
|
||||||
@@ -265,53 +265,30 @@ class ReportValidator:
|
|||||||
|
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if key in schema_keys:
|
if key in schema_keys:
|
||||||
cleaned_val = self._validate_node(value, schema_keys[key], f"{path}.{key}")
|
cleaned_val = self._validate_node(value, schema_keys[key], "{}.".format(path, key))
|
||||||
if cleaned_val is not None:
|
if cleaned_val is not None:
|
||||||
cleaned_data[key] = cleaned_val
|
cleaned_data[key] = cleaned_val
|
||||||
elif "values_rule" in rule:
|
elif "values_rule" in rule:
|
||||||
cleaned_val = self._validate_node(value, rule["values_rule"], f"{path}.{key}")
|
cleaned_val = self._validate_node(value, rule["values_rule"], "{}.".format(path, key))
|
||||||
if cleaned_val is not None:
|
if cleaned_val is not None:
|
||||||
cleaned_data[key] = cleaned_val
|
cleaned_data[key] = cleaned_val
|
||||||
else:
|
else:
|
||||||
if schema_keys:
|
if schema_keys:
|
||||||
self.warnings.append(f"{path}: Unknown key '{key}'")
|
self.warnings.append("{}: Unknown key '{}'".format(path, key))
|
||||||
|
|
||||||
for key, key_rule in schema_keys.items():
|
for key, key_rule in schema_keys.items():
|
||||||
if key_rule.get("required", True) and key not in cleaned_data:
|
if key_rule.get("required", True) and key not in cleaned_data:
|
||||||
self.errors.append(f"{path}: Missing required key '{key}'")
|
self.errors.append("{}: Missing required key '{}'".format(path, key))
|
||||||
|
|
||||||
elif isinstance(data, list):
|
elif isinstance(data, list):
|
||||||
item_rule = rule.get("item_rule")
|
item_rule = rule.get("item_rule")
|
||||||
if item_rule:
|
if item_rule:
|
||||||
cleaned_data = []
|
cleaned_data = []
|
||||||
for i, item in enumerate(data):
|
for i, item in enumerate(data):
|
||||||
cleaned_val = self._validate_node(item, item_rule, f"{path}[{i}]")
|
cleaned_val = self._validate_node(item, item_rule, "{}[{}]".format(path, i))
|
||||||
if cleaned_val is not None:
|
if cleaned_val is not None:
|
||||||
cleaned_data.append(cleaned_val)
|
cleaned_data.append(cleaned_val)
|
||||||
else:
|
else:
|
||||||
cleaned_data = list(data)
|
cleaned_data = list(data)
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
def show_validation_report(self, report_path, is_valid, errors, warnings):
|
|
||||||
self.u.head("Validation Report")
|
|
||||||
print("")
|
|
||||||
print("Validation report for: {}".format(report_path))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
if is_valid:
|
|
||||||
print("Hardware report is valid!")
|
|
||||||
else:
|
|
||||||
print("Hardware report is not valid! Please check the errors and warnings below.")
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
print("")
|
|
||||||
print("\033[31mErrors ({}):\033[0m".format(len(errors)))
|
|
||||||
for i, error in enumerate(errors, 1):
|
|
||||||
print(" {}. {}".format(i, error))
|
|
||||||
|
|
||||||
if warnings:
|
|
||||||
print("")
|
|
||||||
print("\033[33mWarnings ({}):\033[0m".format(len(warnings)))
|
|
||||||
for i, warning in enumerate(warnings, 1):
|
|
||||||
print(" {}. {}".format(i, warning))
|
|
||||||
@@ -20,14 +20,14 @@ else:
|
|||||||
MAX_ATTEMPTS = 3
|
MAX_ATTEMPTS = 3
|
||||||
|
|
||||||
class ResourceFetcher:
|
class ResourceFetcher:
|
||||||
def __init__(self, headers=None):
|
def __init__(self, utils_instance=None, integrity_checker_instance=None, headers=None):
|
||||||
self.request_headers = headers or {
|
self.request_headers = headers or {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||||
}
|
}
|
||||||
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
self.buffer_size = 16 * 1024
|
self.buffer_size = 16 * 1024
|
||||||
self.ssl_context = self.create_ssl_context()
|
self.ssl_context = self.create_ssl_context()
|
||||||
self.integrity_checker = integrity_checker.IntegrityChecker()
|
self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker()
|
||||||
self.utils = utils.Utils()
|
|
||||||
|
|
||||||
def create_ssl_context(self):
|
def create_ssl_context(self):
|
||||||
try:
|
try:
|
||||||
@@ -36,9 +36,10 @@ class ResourceFetcher:
|
|||||||
import certifi
|
import certifi
|
||||||
cafile = certifi.where()
|
cafile = certifi.where()
|
||||||
ssl_context = ssl.create_default_context(cafile=cafile)
|
ssl_context = ssl.create_default_context(cafile=cafile)
|
||||||
|
self.utils.log_message("[RESOURCE FETCHER] Created SSL context", level="INFO")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Failed to create SSL context: {}".format(e))
|
|
||||||
ssl_context = ssl._create_unverified_context()
|
ssl_context = ssl._create_unverified_context()
|
||||||
|
self.utils.log_message("[RESOURCE FETCHER] Created unverified SSL context", level="INFO")
|
||||||
return ssl_context
|
return ssl_context
|
||||||
|
|
||||||
def _make_request(self, resource_url, timeout=10):
|
def _make_request(self, resource_url, timeout=10):
|
||||||
@@ -48,13 +49,13 @@ class ResourceFetcher:
|
|||||||
|
|
||||||
return urlopen(Request(resource_url, headers=headers), timeout=timeout, context=self.ssl_context)
|
return urlopen(Request(resource_url, headers=headers), timeout=timeout, context=self.ssl_context)
|
||||||
except socket.timeout as e:
|
except socket.timeout as e:
|
||||||
print("Timeout error: {}".format(e))
|
self.utils.log_message("[RESOURCE FETCHER] Timeout error: {}".format(e), level="ERROR", to_build_log=True)
|
||||||
except ssl.SSLError as e:
|
except ssl.SSLError as e:
|
||||||
print("SSL error: {}".format(e))
|
self.utils.log_message("[RESOURCE FETCHER] SSL error: {}".format(e), level="ERROR", to_build_log=True)
|
||||||
except (URLError, socket.gaierror) as e:
|
except (URLError, socket.gaierror) as e:
|
||||||
print("Connection error: {}".format(e))
|
self.utils.log_message("[RESOURCE FETCHER] Connection error: {}".format(e), level="ERROR", to_build_log=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Request failed: {}".format(e))
|
self.utils.log_message("[RESOURCE FETCHER] Request failed: {}".format(e), level="ERROR", to_build_log=True)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -62,12 +63,14 @@ class ResourceFetcher:
|
|||||||
attempt = 0
|
attempt = 0
|
||||||
response = None
|
response = None
|
||||||
|
|
||||||
while attempt < 3:
|
self.utils.log_message("[RESOURCE FETCHER] Fetching and parsing content from {}".format(resource_url), level="INFO")
|
||||||
|
|
||||||
|
while attempt < MAX_ATTEMPTS:
|
||||||
response = self._make_request(resource_url)
|
response = self._make_request(resource_url)
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
attempt += 1
|
attempt += 1
|
||||||
print("Failed to fetch content from {}. Retrying...".format(resource_url))
|
self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.getcode() == 200:
|
if response.getcode() == 200:
|
||||||
@@ -76,7 +79,7 @@ class ResourceFetcher:
|
|||||||
attempt += 1
|
attempt += 1
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
print("Failed to fetch content from {}".format(resource_url))
|
self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}".format(resource_url), level="ERROR", to_build_log=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
content = response.read()
|
content = response.read()
|
||||||
@@ -85,12 +88,12 @@ class ResourceFetcher:
|
|||||||
try:
|
try:
|
||||||
content = gzip.decompress(content)
|
content = gzip.decompress(content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Failed to decompress gzip content: {}".format(e))
|
self.utils.log_message("[RESOURCE FETCHER] Failed to decompress gzip content: {}".format(e), level="ERROR", to_build_log=True)
|
||||||
elif response.info().get("Content-Encoding") == "deflate":
|
elif response.info().get("Content-Encoding") == "deflate":
|
||||||
try:
|
try:
|
||||||
content = zlib.decompress(content)
|
content = zlib.decompress(content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Failed to decompress deflate content: {}".format(e))
|
self.utils.log_message("[RESOURCE FETCHER] Failed to decompress deflate content: {}".format(e), level="ERROR", to_build_log=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if content_type == "json":
|
if content_type == "json":
|
||||||
@@ -100,7 +103,7 @@ class ResourceFetcher:
|
|||||||
else:
|
else:
|
||||||
return content.decode("utf-8")
|
return content.decode("utf-8")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error parsing content as {}: {}".format(content_type, e))
|
self.utils.log_message("[RESOURCE FETCHER] Error parsing content as {}: {}".format(content_type, e), level="ERROR", to_build_log=True)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -150,20 +153,19 @@ class ResourceFetcher:
|
|||||||
else:
|
else:
|
||||||
progress = "{} {:.1f}MB downloaded".format(speed_str, bytes_downloaded/(1024*1024))
|
progress = "{} {:.1f}MB downloaded".format(speed_str, bytes_downloaded/(1024*1024))
|
||||||
|
|
||||||
print(" " * 80, end="\r")
|
self.utils.log_message("[RESOURCE FETCHER] Download progress: {}".format(progress), level="INFO", to_build_log=True)
|
||||||
print(progress, end="\r")
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
def download_and_save_file(self, resource_url, destination_path, sha256_hash=None):
|
def download_and_save_file(self, resource_url, destination_path, sha256_hash=None):
|
||||||
attempt = 0
|
attempt = 0
|
||||||
|
|
||||||
|
self.utils.log_message("[RESOURCE FETCHER] Downloading and saving file from {} to {}".format(resource_url, destination_path), level="INFO")
|
||||||
|
|
||||||
while attempt < MAX_ATTEMPTS:
|
while attempt < MAX_ATTEMPTS:
|
||||||
attempt += 1
|
attempt += 1
|
||||||
response = self._make_request(resource_url)
|
response = self._make_request(resource_url)
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
print("Failed to fetch content from {}. Retrying...".format(resource_url))
|
self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with open(destination_path, "wb") as local_file:
|
with open(destination_path, "wb") as local_file:
|
||||||
@@ -171,24 +173,24 @@ class ResourceFetcher:
|
|||||||
|
|
||||||
if os.path.exists(destination_path) and os.path.getsize(destination_path) > 0:
|
if os.path.exists(destination_path) and os.path.getsize(destination_path) > 0:
|
||||||
if sha256_hash:
|
if sha256_hash:
|
||||||
print("Verifying SHA256 checksum...")
|
self.utils.log_message("[RESOURCE FETCHER] Verifying SHA256 checksum...", level="INFO", to_build_log=True)
|
||||||
downloaded_hash = self.integrity_checker.get_sha256(destination_path)
|
downloaded_hash = self.integrity_checker.get_sha256(destination_path)
|
||||||
if downloaded_hash.lower() == sha256_hash.lower():
|
if downloaded_hash.lower() == sha256_hash.lower():
|
||||||
print("Checksum verified successfully.")
|
self.utils.log_message("[RESOURCE FETCHER] Checksum verified successfully.", level="INFO", to_build_log=True)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("Checksum mismatch! Removing file and retrying download...")
|
self.utils.log_message("[RESOURCE FETCHER] Checksum mismatch! Removing file and retrying download...", level="WARNING", to_build_log=True)
|
||||||
os.remove(destination_path)
|
os.remove(destination_path)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
print("No SHA256 hash provided. Downloading file without verification.")
|
self.utils.log_message("[RESOURCE FETCHER] No SHA256 hash provided. Downloading file without verification.", level="INFO", to_build_log=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if os.path.exists(destination_path):
|
if os.path.exists(destination_path):
|
||||||
os.remove(destination_path)
|
os.remove(destination_path)
|
||||||
|
|
||||||
if attempt < MAX_ATTEMPTS:
|
if attempt < MAX_ATTEMPTS:
|
||||||
print("Download failed for {}. Retrying...".format(resource_url))
|
self.utils.log_message("[RESOURCE FETCHER] Download failed for {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True)
|
||||||
|
|
||||||
print("Failed to download {} after {} attempts.".format(resource_url, MAX_ATTEMPTS))
|
self.utils.log_message("[RESOURCE FETCHER] Failed to download {} after {} attempts.".format(resource_url, MAX_ATTEMPTS), level="ERROR", to_build_log=True)
|
||||||
return False
|
return False
|
||||||
53
Scripts/settings.py
Normal file
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.mac_model_data import mac_devices
|
||||||
from Scripts.datasets import kext_data
|
from Scripts.datasets import kext_data
|
||||||
from Scripts.datasets import os_data
|
from Scripts.datasets import os_data
|
||||||
|
from Scripts.custom_dialogs import show_smbios_selection_dialog
|
||||||
from Scripts import gathering_files
|
from Scripts import gathering_files
|
||||||
from Scripts import run
|
from Scripts import run
|
||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
|
from Scripts import settings
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import random
|
import random
|
||||||
@@ -12,10 +14,11 @@ import platform
|
|||||||
os_name = platform.system()
|
os_name = platform.system()
|
||||||
|
|
||||||
class SMBIOS:
|
class SMBIOS:
|
||||||
def __init__(self):
|
def __init__(self, gathering_files_instance=None, run_instance=None, utils_instance=None, settings_instance=None):
|
||||||
self.g = gathering_files.gatheringFiles()
|
self.g = gathering_files_instance if gathering_files_instance else gathering_files.gatheringFiles()
|
||||||
self.run = run.Run().run
|
self.run = run_instance.run if run_instance else run.Run().run
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
|
self.settings = settings_instance if settings_instance else settings.Settings()
|
||||||
self.script_dir = os.path.dirname(os.path.realpath(__file__))
|
self.script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
def check_macserial(self, retry_count=0):
|
def check_macserial(self, retry_count=0):
|
||||||
@@ -28,6 +31,7 @@ class SMBIOS:
|
|||||||
elif os_name == "Darwin":
|
elif os_name == "Darwin":
|
||||||
macserial_binary = ["macserial"]
|
macserial_binary = ["macserial"]
|
||||||
else:
|
else:
|
||||||
|
self.utils.log_message("[SMBIOS] Unknown OS for macserial", level="ERROR")
|
||||||
raise Exception("Unknown OS for macserial")
|
raise Exception("Unknown OS for macserial")
|
||||||
|
|
||||||
for binary in macserial_binary:
|
for binary in macserial_binary:
|
||||||
@@ -36,6 +40,7 @@ class SMBIOS:
|
|||||||
return macserial_path
|
return macserial_path
|
||||||
|
|
||||||
if retry_count >= max_retries:
|
if retry_count >= max_retries:
|
||||||
|
self.utils.log_message("[SMBIOS] Failed to find macserial after {} attempts".format(max_retries), level="ERROR")
|
||||||
raise Exception("Failed to find macserial after {} attempts".format(max_retries))
|
raise Exception("Failed to find macserial after {} attempts".format(max_retries))
|
||||||
|
|
||||||
download_history = self.utils.read_file(self.g.download_history_file)
|
download_history = self.utils.read_file(self.g.download_history_file)
|
||||||
@@ -68,13 +73,21 @@ class SMBIOS:
|
|||||||
else:
|
else:
|
||||||
serial = output[0].splitlines()[0].split(" | ")
|
serial = output[0].splitlines()[0].split(" | ")
|
||||||
|
|
||||||
return {
|
smbios_info = {
|
||||||
"MLB": "A" + "0"*15 + "Z" if not serial else serial[-1],
|
"MLB": "A" + "0"*15 + "Z" if not serial else serial[-1],
|
||||||
"ROM": random_mac_address,
|
"ROM": random_mac_address,
|
||||||
"SystemProductName": smbios_model,
|
"SystemProductName": smbios_model,
|
||||||
"SystemSerialNumber": "A" + "0"*10 + "9" if not serial else serial[0],
|
"SystemSerialNumber": "A" + "0"*10 + "9" if not serial else serial[0],
|
||||||
"SystemUUID": str(uuid.uuid4()).upper(),
|
"SystemUUID": str(uuid.uuid4()).upper(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.utils.log_message("[SMBIOS] Generated SMBIOS info: MLB: {}..., ROM: {}..., SystemProductName: {}, SystemSerialNumber: {}..., SystemUUID: {}...".format(
|
||||||
|
smbios_info["MLB"][:5],
|
||||||
|
smbios_info["ROM"][:5],
|
||||||
|
smbios_info["SystemProductName"],
|
||||||
|
smbios_info["SystemSerialNumber"][:5],
|
||||||
|
smbios_info["SystemUUID"].split("-")[0]), level="INFO")
|
||||||
|
return smbios_info
|
||||||
|
|
||||||
def smbios_specific_options(self, hardware_report, smbios_model, macos_version, acpi_patches, kext_maestro):
|
def smbios_specific_options(self, hardware_report, smbios_model, macos_version, acpi_patches, kext_maestro):
|
||||||
for patch in acpi_patches:
|
for patch in acpi_patches:
|
||||||
@@ -160,79 +173,45 @@ class SMBIOS:
|
|||||||
elif "Ice Lake" in codename:
|
elif "Ice Lake" in codename:
|
||||||
smbios_model = "MacBookAir9,1"
|
smbios_model = "MacBookAir9,1"
|
||||||
|
|
||||||
|
self.utils.log_message("[SMBIOS] Suggested SMBIOS model: {}".format(smbios_model), level="INFO")
|
||||||
return smbios_model
|
return smbios_model
|
||||||
|
|
||||||
def customize_smbios_model(self, hardware_report, selected_smbios_model, macos_version):
|
def customize_smbios_model(self, hardware_report, selected_smbios_model, macos_version, parent=None):
|
||||||
current_category = None
|
|
||||||
default_smbios_model = self.select_smbios_model(hardware_report, macos_version)
|
default_smbios_model = self.select_smbios_model(hardware_report, macos_version)
|
||||||
show_all_models = False
|
|
||||||
is_laptop = "Laptop" == hardware_report.get("Motherboard").get("Platform")
|
is_laptop = "Laptop" == hardware_report.get("Motherboard").get("Platform")
|
||||||
|
macos_name = os_data.get_macos_name_by_darwin(macos_version)
|
||||||
while True:
|
|
||||||
incompatible_models_by_index = []
|
items = []
|
||||||
contents = []
|
for index, device in enumerate(mac_devices):
|
||||||
contents.append("")
|
is_supported = self.utils.parse_darwin_version(device.initial_support) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(device.last_supported_version)
|
||||||
if show_all_models:
|
|
||||||
contents.append("List of available SMBIOS:")
|
platform_match = True
|
||||||
else:
|
if is_laptop and not device.name.startswith("MacBook"):
|
||||||
contents.append("List of compatible SMBIOS:")
|
platform_match = False
|
||||||
for index, device in enumerate(mac_devices, start=1):
|
elif not is_laptop and device.name.startswith("MacBook"):
|
||||||
isSupported = self.utils.parse_darwin_version(device.initial_support) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(device.last_supported_version)
|
platform_match = False
|
||||||
if device.name not in (default_smbios_model, selected_smbios_model) and not show_all_models and (not isSupported or (is_laptop and not device.name.startswith("MacBook")) or (not is_laptop and device.name.startswith("MacBook"))):
|
|
||||||
incompatible_models_by_index.append(index - 1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
category = ""
|
|
||||||
for char in device.name:
|
|
||||||
if char.isdigit():
|
|
||||||
break
|
|
||||||
category += char
|
|
||||||
if category != current_category:
|
|
||||||
current_category = category
|
|
||||||
category_header = "Category: {}".format(current_category if current_category else "Uncategorized")
|
|
||||||
contents.append(f"\n{category_header}\n" + "=" * len(category_header))
|
|
||||||
checkbox = "[*]" if device.name == selected_smbios_model else "[ ]"
|
|
||||||
|
|
||||||
line = "{} {:2}. {:15} - {:10} {:20}{}".format(checkbox, index, device.name, device.cpu, "({})".format(device.cpu_generation), "" if not device.discrete_gpu else " - {}".format(device.discrete_gpu))
|
is_compatible = is_supported and platform_match
|
||||||
if device.name == selected_smbios_model:
|
|
||||||
line = "\033[1;32m{}\033[0m".format(line)
|
category = ""
|
||||||
elif not isSupported:
|
for char in device.name:
|
||||||
line = "\033[90m{}\033[0m".format(line)
|
if char.isdigit():
|
||||||
contents.append(line)
|
break
|
||||||
contents.append("")
|
category += char
|
||||||
contents.append("\033[1;93mNote:\033[0m")
|
|
||||||
contents.append("- Lines in gray indicate mac models that are not officially supported by {}.".format(os_data.get_macos_name_by_darwin(macos_version)))
|
gpu_str = "" if not device.discrete_gpu else " - {}".format(device.discrete_gpu)
|
||||||
contents.append("")
|
label = "{} - {} ({}){}".format(device.name, device.cpu, device.cpu_generation, gpu_str)
|
||||||
if not show_all_models:
|
|
||||||
contents.append("A. Show all models")
|
|
||||||
else:
|
|
||||||
contents.append("C. Show compatible models only")
|
|
||||||
if selected_smbios_model != default_smbios_model:
|
|
||||||
contents.append("R. Restore default SMBIOS model ({})".format(default_smbios_model))
|
|
||||||
contents.append("")
|
|
||||||
contents.append("B. Back")
|
|
||||||
contents.append("Q. Quit")
|
|
||||||
contents.append("")
|
|
||||||
content = "\n".join(contents)
|
|
||||||
|
|
||||||
self.utils.adjust_window_size(content)
|
items.append({
|
||||||
self.utils.head("Customize SMBIOS Model", resize=False)
|
'name': device.name,
|
||||||
print(content)
|
'label': label,
|
||||||
option = self.utils.request_input("Select your option: ")
|
'category': category,
|
||||||
if option.lower() == "q":
|
'is_supported': is_supported,
|
||||||
self.utils.exit_program()
|
'is_compatible': is_compatible
|
||||||
if option.lower() == "b":
|
})
|
||||||
return selected_smbios_model
|
|
||||||
if option.lower() == "r" and selected_smbios_model != default_smbios_model:
|
content = "Lines in gray indicate mac models that are not officially supported by {}.".format(macos_name)
|
||||||
return default_smbios_model
|
|
||||||
if option.lower() in ("a", "c"):
|
result = show_smbios_selection_dialog("Customize SMBIOS Model", content, items, selected_smbios_model, default_smbios_model)
|
||||||
show_all_models = not show_all_models
|
|
||||||
continue
|
return result if result else selected_smbios_model
|
||||||
|
|
||||||
if option.strip().isdigit():
|
|
||||||
index = int(option) - 1
|
|
||||||
if index >= 0 and index < len(mac_devices):
|
|
||||||
if not show_all_models and index in incompatible_models_by_index:
|
|
||||||
continue
|
|
||||||
|
|
||||||
selected_smbios_model = mac_devices[index].name
|
|
||||||
36
Scripts/state.py
Normal file
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 os
|
||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
import plistlib
|
import plistlib
|
||||||
import shutil
|
import shutil
|
||||||
import re
|
import re
|
||||||
import binascii
|
import binascii
|
||||||
import subprocess
|
import subprocess
|
||||||
import pathlib
|
|
||||||
import zipfile
|
import zipfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
|
||||||
class Utils:
|
class Utils:
|
||||||
def __init__(self, script_name = "OpCore Simplify"):
|
def __init__(self):
|
||||||
self.script_name = script_name
|
self.gui_handler = None
|
||||||
|
self.logger = logging.getLogger("OpCoreSimplify")
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def safe_block(self, task_name="Operation", suppress_error=True):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except Exception as e:
|
||||||
|
error_details = "".join(traceback.format_exc())
|
||||||
|
self.log_message("Error during '{}': {}\n{}".format(task_name, str(e), error_details), level="ERROR")
|
||||||
|
if not suppress_error:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def log_message(self, message, level="INFO", to_build_log=False):
|
||||||
|
log_level = getattr(logging, level.upper(), logging.INFO)
|
||||||
|
|
||||||
|
extra = {'to_build_log': to_build_log}
|
||||||
|
|
||||||
|
self.logger.log(log_level, message, extra=extra)
|
||||||
|
return True
|
||||||
|
|
||||||
def clean_temporary_dir(self):
|
def clean_temporary_dir(self):
|
||||||
temporary_dir = tempfile.gettempdir()
|
temporary_dir = tempfile.gettempdir()
|
||||||
@@ -26,6 +46,7 @@ class Utils:
|
|||||||
try:
|
try:
|
||||||
shutil.rmtree(os.path.join(temporary_dir, file))
|
shutil.rmtree(os.path.join(temporary_dir, file))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.log_message("[UTILS] Failed to remove temp directory {}: {}".format(file, e), "Error")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_temporary_dir(self):
|
def get_temporary_dir(self):
|
||||||
@@ -127,23 +148,6 @@ class Utils:
|
|||||||
|
|
||||||
def contains_any(self, data, search_item, start=0, end=None):
|
def contains_any(self, data, search_item, start=0, end=None):
|
||||||
return next((item for item in data[start:end] if item.lower() in search_item.lower()), None)
|
return next((item for item in data[start:end] if item.lower() in search_item.lower()), None)
|
||||||
|
|
||||||
def normalize_path(self, path):
|
|
||||||
path = re.sub(r'^[\'"]+|[\'"]+$', '', path)
|
|
||||||
|
|
||||||
path = path.strip()
|
|
||||||
|
|
||||||
path = os.path.expanduser(path)
|
|
||||||
|
|
||||||
if os.name == 'nt':
|
|
||||||
path = path.replace('\\', '/')
|
|
||||||
path = re.sub(r'/+', '/', path)
|
|
||||||
else:
|
|
||||||
path = path.replace('\\', '')
|
|
||||||
|
|
||||||
path = os.path.normpath(path)
|
|
||||||
|
|
||||||
return str(pathlib.Path(path).resolve())
|
|
||||||
|
|
||||||
def parse_darwin_version(self, darwin_version):
|
def parse_darwin_version(self, darwin_version):
|
||||||
major, minor, patch = map(int, darwin_version.split('.'))
|
major, minor, patch = map(int, darwin_version.split('.'))
|
||||||
@@ -156,78 +160,4 @@ class Utils:
|
|||||||
else:
|
else:
|
||||||
subprocess.run(['xdg-open', folder_path])
|
subprocess.run(['xdg-open', folder_path])
|
||||||
elif os.name == 'nt':
|
elif os.name == 'nt':
|
||||||
os.startfile(folder_path)
|
os.startfile(folder_path)
|
||||||
|
|
||||||
def request_input(self, prompt="Press Enter to continue..."):
|
|
||||||
if sys.version_info[0] < 3:
|
|
||||||
user_response = raw_input(prompt)
|
|
||||||
else:
|
|
||||||
user_response = input(prompt)
|
|
||||||
|
|
||||||
if not isinstance(user_response, str):
|
|
||||||
user_response = str(user_response)
|
|
||||||
|
|
||||||
return user_response
|
|
||||||
|
|
||||||
def progress_bar(self, title, steps, current_step_index, done=False):
|
|
||||||
self.head(title)
|
|
||||||
print("")
|
|
||||||
if done:
|
|
||||||
for step in steps:
|
|
||||||
print(" [\033[92m✓\033[0m] {}".format(step))
|
|
||||||
else:
|
|
||||||
for i, step in enumerate(steps):
|
|
||||||
if i < current_step_index:
|
|
||||||
print(" [\033[92m✓\033[0m] {}".format(step))
|
|
||||||
elif i == current_step_index:
|
|
||||||
print(" [\033[1;93m>\033[0m] {}...".format(step))
|
|
||||||
else:
|
|
||||||
print(" [ ] {}".format(step))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
def head(self, text = None, width = 68, resize=True):
|
|
||||||
if resize:
|
|
||||||
self.adjust_window_size()
|
|
||||||
os.system('cls' if os.name=='nt' else 'clear')
|
|
||||||
if text == None:
|
|
||||||
text = self.script_name
|
|
||||||
separator = "═" * (width - 2)
|
|
||||||
title = " {} ".format(text)
|
|
||||||
if len(title) > width - 2:
|
|
||||||
title = title[:width-4] + "..."
|
|
||||||
title = title.center(width - 2)
|
|
||||||
|
|
||||||
print("╔{}╗\n║{}║\n╚{}╝".format(separator, title, separator))
|
|
||||||
|
|
||||||
def adjust_window_size(self, content=""):
|
|
||||||
lines = content.splitlines()
|
|
||||||
rows = len(lines)
|
|
||||||
cols = max(len(line) for line in lines) if lines else 0
|
|
||||||
print('\033[8;{};{}t'.format(max(rows+6, 30), max(cols+2, 100)))
|
|
||||||
|
|
||||||
def exit_program(self):
|
|
||||||
self.head()
|
|
||||||
width = 68
|
|
||||||
print("")
|
|
||||||
print("For more information, to report errors, or to contribute to the product:".center(width))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
separator = "─" * (width - 4)
|
|
||||||
print(f" ┌{separator}┐ ")
|
|
||||||
|
|
||||||
contacts = {
|
|
||||||
"Facebook": "https://www.facebook.com/macforce2601",
|
|
||||||
"Telegram": "https://t.me/lzhoang2601",
|
|
||||||
"GitHub": "https://github.com/lzhoang2801/OpCore-Simplify"
|
|
||||||
}
|
|
||||||
|
|
||||||
for platform, link in contacts.items():
|
|
||||||
line = f" * {platform}: {link}"
|
|
||||||
print(f" │{line.ljust(width - 4)}│ ")
|
|
||||||
|
|
||||||
print(f" └{separator}┘ ")
|
|
||||||
print("")
|
|
||||||
print("Thank you for using our program!".center(width))
|
|
||||||
print("")
|
|
||||||
self.request_input("Press Enter to exit.".center(width))
|
|
||||||
sys.exit(0)
|
|
||||||
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 run
|
||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
|
from Scripts.custom_dialogs import ask_network_count, show_info, show_confirmation
|
||||||
import platform
|
import platform
|
||||||
import json
|
import json
|
||||||
|
|
||||||
os_name = platform.system()
|
os_name = platform.system()
|
||||||
|
|
||||||
class WifiProfileExtractor:
|
class WifiProfileExtractor:
|
||||||
def __init__(self):
|
def __init__(self, run_instance=None, utils_instance=None):
|
||||||
self.run = run.Run().run
|
self.run = run_instance.run if run_instance else run.Run().run
|
||||||
self.utils = utils.Utils()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
|
|
||||||
def get_authentication_type(self, authentication_type):
|
def get_authentication_type(self, authentication_type):
|
||||||
authentication_type = authentication_type.lower()
|
authentication_type = authentication_type.lower()
|
||||||
@@ -27,14 +28,16 @@ class WifiProfileExtractor:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def validate_wifi_password(self, authentication_type=None, password=None):
|
def validate_wifi_password(self, authentication_type=None, password=None):
|
||||||
print("Validating password with authentication type: {}".format(authentication_type))
|
|
||||||
|
|
||||||
if password is None:
|
if password is None:
|
||||||
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Password is not found", level="INFO")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if authentication_type is None:
|
if authentication_type is None:
|
||||||
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Authentication type is not found", level="INFO")
|
||||||
return password
|
return password
|
||||||
|
|
||||||
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Validating password for \"{}\" with {} authentication type".format(password, authentication_type), level="INFO")
|
||||||
|
|
||||||
if authentication_type == "open":
|
if authentication_type == "open":
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -103,30 +106,14 @@ class WifiProfileExtractor:
|
|||||||
return self.validate_wifi_password(authentication_type, password)
|
return self.validate_wifi_password(authentication_type, password)
|
||||||
|
|
||||||
def ask_network_count(self, total_networks):
|
def ask_network_count(self, total_networks):
|
||||||
self.utils.head("WiFi Network Retrieval")
|
if self.utils.gui_handler:
|
||||||
print("")
|
result = ask_network_count(total_networks)
|
||||||
print("Found {} WiFi networks on this device.".format(total_networks))
|
if result == 'a':
|
||||||
print("")
|
return total_networks
|
||||||
print("How many networks would you like to process?")
|
return int(result)
|
||||||
print(" 1-{} - Specific number (default: 5)".format(total_networks))
|
|
||||||
print(" A - All available networks")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
num_choice = self.utils.request_input("Enter your choice: ").strip().lower() or "5"
|
return 5
|
||||||
|
|
||||||
if num_choice == "a":
|
|
||||||
print("Will process all available networks.")
|
|
||||||
return total_networks
|
|
||||||
|
|
||||||
try:
|
|
||||||
max_networks = min(int(num_choice), total_networks)
|
|
||||||
print("Will process up to {} networks.".format(max_networks))
|
|
||||||
return max_networks
|
|
||||||
except:
|
|
||||||
max_networks = min(5, total_networks)
|
|
||||||
print("Invalid choice. Will process up to {} networks.".format(max_networks))
|
|
||||||
return max_networks
|
|
||||||
|
|
||||||
def process_networks(self, ssid_list, max_networks, get_password_func):
|
def process_networks(self, ssid_list, max_networks, get_password_func):
|
||||||
networks = []
|
networks = []
|
||||||
processed_count = 0
|
processed_count = 0
|
||||||
@@ -137,39 +124,35 @@ class WifiProfileExtractor:
|
|||||||
ssid = ssid_list[processed_count]
|
ssid = ssid_list[processed_count]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("")
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving password for \"{}\" ({} of {})".format(ssid, processed_count + 1, len(ssid_list)), level="INFO", to_build_log=True)
|
||||||
print("Processing {}/{}: {}".format(processed_count + 1, len(ssid_list), ssid))
|
|
||||||
if os_name == "Darwin":
|
|
||||||
print("Please enter your administrator name and password or click 'Deny' to skip this network.")
|
|
||||||
|
|
||||||
password = get_password_func(ssid)
|
password = get_password_func(ssid)
|
||||||
if password is not None:
|
if password is not None:
|
||||||
if (ssid, password) not in networks:
|
if (ssid, password) not in networks:
|
||||||
consecutive_failures = 0
|
consecutive_failures = 0
|
||||||
networks.append((ssid, password))
|
networks.append((ssid, password))
|
||||||
print("Successfully retrieved password.")
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Successfully retrieved password for \"{}\"".format(ssid), level="INFO", to_build_log=True)
|
||||||
|
|
||||||
if len(networks) == max_networks:
|
if len(networks) == max_networks:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Could not retrieve password for \"{}\"".format(ssid), level="INFO", to_build_log=True)
|
||||||
consecutive_failures += 1 if os_name == "Darwin" else 0
|
consecutive_failures += 1 if os_name == "Darwin" else 0
|
||||||
print("Could not retrieve password for this network.")
|
|
||||||
|
|
||||||
if consecutive_failures >= max_consecutive_failures:
|
if consecutive_failures >= max_consecutive_failures:
|
||||||
continue_input = self.utils.request_input("\nUnable to retrieve passwords. Continue trying? (Yes/no): ").strip().lower() or "yes"
|
result = show_confirmation("WiFi Profile Extractor", "Unable to retrieve passwords. Continue trying?")
|
||||||
|
|
||||||
if continue_input != "yes":
|
if not result:
|
||||||
break
|
break
|
||||||
|
|
||||||
consecutive_failures = 0
|
consecutive_failures = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
consecutive_failures += 1 if os_name == "Darwin" else 0
|
consecutive_failures += 1 if os_name == "Darwin" else 0
|
||||||
print("Error processing network '{}': {}".format(ssid, str(e)))
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Error processing network \"{}\": {}".format(ssid, str(e)), level="ERROR", to_build_log=True)
|
||||||
|
|
||||||
if consecutive_failures >= max_consecutive_failures:
|
if consecutive_failures >= max_consecutive_failures:
|
||||||
continue_input = self.utils.request_input("\nUnable to retrieve passwords. Continue trying? (Yes/no): ").strip().lower() or "yes"
|
result = show_confirmation("WiFi Profile Extractor", "Unable to retrieve passwords. Continue trying?")
|
||||||
|
|
||||||
if continue_input != "yes":
|
if not result:
|
||||||
break
|
break
|
||||||
|
|
||||||
consecutive_failures = 0
|
consecutive_failures = 0
|
||||||
@@ -177,12 +160,11 @@ class WifiProfileExtractor:
|
|||||||
processed_count += 1
|
processed_count += 1
|
||||||
|
|
||||||
if processed_count >= max_networks and len(networks) < max_networks and processed_count < len(ssid_list):
|
if processed_count >= max_networks and len(networks) < max_networks and processed_count < len(ssid_list):
|
||||||
continue_input = self.utils.request_input("\nOnly retrieved {}/{} networks. Try more to reach your target? (Yes/no): ".format(len(networks), max_networks)).strip().lower() or "yes"
|
|
||||||
|
|
||||||
if continue_input != "yes":
|
|
||||||
break
|
|
||||||
|
|
||||||
consecutive_failures = 0
|
result = show_confirmation("WiFi Profile Extractor", "Only retrieved {}/{} networks. Try more to reach your target?".format(len(networks), max_networks))
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
break
|
||||||
|
|
||||||
return networks
|
return networks
|
||||||
|
|
||||||
@@ -201,10 +183,12 @@ class WifiProfileExtractor:
|
|||||||
|
|
||||||
max_networks = self.ask_network_count(len(ssid_list))
|
max_networks = self.ask_network_count(len(ssid_list))
|
||||||
|
|
||||||
self.utils.head("Administrator Authentication Required")
|
if self.utils.gui_handler:
|
||||||
print("")
|
content = (
|
||||||
print("To retrieve WiFi passwords from the Keychain, macOS will prompt")
|
"To retrieve WiFi passwords from the Keychain, macOS will prompt<br>"
|
||||||
print("you for administrator credentials for each WiFi network.")
|
"you for administrator credentials for each WiFi network."
|
||||||
|
)
|
||||||
|
show_info("Administrator Authentication Required", content)
|
||||||
|
|
||||||
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_macos)
|
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_macos)
|
||||||
|
|
||||||
@@ -232,9 +216,7 @@ class WifiProfileExtractor:
|
|||||||
|
|
||||||
max_networks = len(ssid_list)
|
max_networks = len(ssid_list)
|
||||||
|
|
||||||
self.utils.head("WiFi Profile Extractor")
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving passwords for {} network(s)".format(len(ssid_list)), level="INFO", to_build_log=True)
|
||||||
print("")
|
|
||||||
print("Retrieving passwords for {} network(s)...".format(len(ssid_list)))
|
|
||||||
|
|
||||||
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_windows)
|
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_windows)
|
||||||
|
|
||||||
@@ -253,9 +235,7 @@ class WifiProfileExtractor:
|
|||||||
|
|
||||||
max_networks = len(ssid_list)
|
max_networks = len(ssid_list)
|
||||||
|
|
||||||
self.utils.head("WiFi Profile Extractor")
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving passwords for {} network(s)".format(len(ssid_list)), level="INFO", to_build_log=True)
|
||||||
print("")
|
|
||||||
print("Retrieving passwords for {} network(s)...".format(len(ssid_list)))
|
|
||||||
|
|
||||||
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_linux)
|
return self.process_networks(ssid_list, max_networks, self.get_wifi_password_linux)
|
||||||
|
|
||||||
@@ -286,31 +266,21 @@ class WifiProfileExtractor:
|
|||||||
return interfaces
|
return interfaces
|
||||||
|
|
||||||
def get_profiles(self):
|
def get_profiles(self):
|
||||||
os_name = platform.system()
|
content = (
|
||||||
|
"<b>Note:</b><br>"
|
||||||
self.utils.head("WiFi Profile Extractor")
|
"<ul>"
|
||||||
print("")
|
"<li>When using itlwm kext, WiFi appears as Ethernet in macOS</li>"
|
||||||
print("\033[1;93mNote:\033[0m")
|
"<li>You'll need Heliport app to manage WiFi connections in macOS</li>"
|
||||||
print("- When using itlwm kext, WiFi appears as Ethernet in macOS")
|
"<li>This step will enable auto WiFi connections at boot time<br>"
|
||||||
print("- You'll need Heliport app to manage WiFi connections in macOS")
|
"and is useful for users installing macOS via Recovery OS</li>"
|
||||||
print("- This step will enable auto WiFi connections at boot time")
|
"</ul><br>"
|
||||||
print(" and is useful for users installing macOS via Recovery OS")
|
"Would you like to scan for WiFi profiles?"
|
||||||
print("")
|
)
|
||||||
|
if not show_confirmation("WiFi Profile Extractor", content):
|
||||||
|
return []
|
||||||
|
|
||||||
while True:
|
|
||||||
user_input = self.utils.request_input("Would you like to scan for WiFi profiles? (Yes/no): ").strip().lower()
|
|
||||||
|
|
||||||
if user_input == "yes":
|
|
||||||
break
|
|
||||||
elif user_input == "no":
|
|
||||||
return []
|
|
||||||
else:
|
|
||||||
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
|
|
||||||
|
|
||||||
profiles = []
|
profiles = []
|
||||||
self.utils.head("Detecting WiFi Profiles")
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Detecting WiFi Profiles", level="INFO", to_build_log=True)
|
||||||
print("")
|
|
||||||
print("Scanning for WiFi profiles...")
|
|
||||||
|
|
||||||
if os_name == "Windows":
|
if os_name == "Windows":
|
||||||
profiles = self.get_preferred_networks_windows()
|
profiles = self.get_preferred_networks_windows()
|
||||||
@@ -321,31 +291,17 @@ class WifiProfileExtractor:
|
|||||||
|
|
||||||
if wifi_interfaces:
|
if wifi_interfaces:
|
||||||
for interface in wifi_interfaces:
|
for interface in wifi_interfaces:
|
||||||
print("Checking interface: {}".format(interface))
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Checking interface: {}".format(interface), level="INFO", to_build_log=True)
|
||||||
interface_profiles = self.get_preferred_networks_macos(interface)
|
interface_profiles = self.get_preferred_networks_macos(interface)
|
||||||
if interface_profiles:
|
if interface_profiles:
|
||||||
profiles = interface_profiles
|
profiles = interface_profiles
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print("No WiFi interfaces detected.")
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] No WiFi interfaces detected.", level="INFO", to_build_log=True)
|
||||||
|
|
||||||
if not profiles:
|
if not profiles:
|
||||||
self.utils.head("WiFi Profile Extractor")
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] No WiFi profiles with saved passwords were found.", level="INFO", to_build_log=True)
|
||||||
print("")
|
|
||||||
print("No WiFi profiles with saved passwords were found.")
|
|
||||||
self.utils.request_input()
|
|
||||||
|
|
||||||
self.utils.head("WiFi Profile Extractor")
|
self.utils.log_message("[WIFI PROFILE EXTRACTOR] Successfully applied {} WiFi profiles".format(len(profiles)), level="INFO", to_build_log=True)
|
||||||
print("")
|
|
||||||
print("Found the following WiFi profiles with saved passwords:")
|
|
||||||
print("")
|
|
||||||
print("Index SSID Password")
|
|
||||||
print("-------------------------------------------------------")
|
|
||||||
for index, (ssid, password) in enumerate(profiles, start=1):
|
|
||||||
print("{:<6} {:<32} {:<8}".format(index, ssid[:31] + "..." if len(ssid) > 31 else ssid, password[:12] + "..." if len(password) > 12 else password))
|
|
||||||
print("")
|
|
||||||
print("Successfully applied {} WiFi profiles.".format(len(profiles)))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
self.utils.request_input()
|
|
||||||
return profiles
|
return profiles
|
||||||
4
requirements.txt
Normal file
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 resource_fetcher
|
||||||
from Scripts import github
|
from Scripts import github
|
||||||
from Scripts import run
|
from Scripts import run
|
||||||
from Scripts import utils
|
from Scripts import utils
|
||||||
import os
|
from Scripts import integrity_checker
|
||||||
import tempfile
|
from Scripts.custom_dialogs import show_update_dialog, show_info, show_confirmation
|
||||||
import shutil
|
|
||||||
|
class UpdateCheckerThread(QThread):
|
||||||
|
update_available = pyqtSignal(dict)
|
||||||
|
check_failed = pyqtSignal(str)
|
||||||
|
no_update = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, updater_instance):
|
||||||
|
super().__init__()
|
||||||
|
self.updater = updater_instance
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
remote_manifest = self.updater.get_remote_manifest()
|
||||||
|
if not remote_manifest:
|
||||||
|
self.check_failed.emit("Could not fetch update information from GitHub.\n\nPlease check your internet connection and try again later.")
|
||||||
|
return
|
||||||
|
|
||||||
|
local_manifest = self.updater.get_local_manifest()
|
||||||
|
if not local_manifest:
|
||||||
|
self.check_failed.emit("Could not generate local manifest.\n\nPlease try again later.")
|
||||||
|
return
|
||||||
|
|
||||||
|
files_to_update = self.updater.compare_manifests(local_manifest, remote_manifest)
|
||||||
|
if not files_to_update:
|
||||||
|
self.no_update.emit()
|
||||||
|
else:
|
||||||
|
self.update_available.emit(files_to_update)
|
||||||
|
except Exception as e:
|
||||||
|
self.check_failed.emit("An error occurred during update check:\n\n{}".format(str(e)))
|
||||||
|
|
||||||
class Updater:
|
class Updater:
|
||||||
def __init__(self):
|
def __init__(self, utils_instance=None, github_instance=None, resource_fetcher_instance=None, run_instance=None, integrity_checker_instance=None):
|
||||||
self.github = github.Github()
|
self.utils = utils_instance if utils_instance else utils.Utils()
|
||||||
self.fetcher = resource_fetcher.ResourceFetcher()
|
self.github = github_instance if github_instance else github.Github(utils_instance=self.utils)
|
||||||
self.run = run.Run().run
|
self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher(utils_instance=self.utils)
|
||||||
self.utils = utils.Utils()
|
self.run = run_instance.run if run_instance else run.Run().run
|
||||||
self.sha_version = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sha_version.txt")
|
self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker(utils_instance=self.utils)
|
||||||
|
self.remote_manifest_url = "https://nightly.link/lzhoang2801/OpCore-Simplify/workflows/generate-manifest/main/manifest.json.zip"
|
||||||
self.download_repo_url = "https://github.com/lzhoang2801/OpCore-Simplify/archive/refs/heads/main.zip"
|
self.download_repo_url = "https://github.com/lzhoang2801/OpCore-Simplify/archive/refs/heads/main.zip"
|
||||||
self.temporary_dir = tempfile.mkdtemp()
|
self.temporary_dir = tempfile.mkdtemp()
|
||||||
self.current_step = 0
|
self.root_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
def get_current_sha_version(self):
|
def get_remote_manifest(self, dialog=None):
|
||||||
print("Checking current version...")
|
if dialog:
|
||||||
try:
|
dialog.update_progress(10, "Fetching remote manifest...")
|
||||||
current_sha_version = self.utils.read_file(self.sha_version)
|
|
||||||
|
|
||||||
if not current_sha_version:
|
|
||||||
print("SHA version information is missing.")
|
|
||||||
return "missing_sha_version"
|
|
||||||
|
|
||||||
return current_sha_version.decode()
|
|
||||||
except Exception as e:
|
|
||||||
print("Error reading current SHA version: {}".format(str(e)))
|
|
||||||
return "error_reading_sha_version"
|
|
||||||
|
|
||||||
def get_latest_sha_version(self):
|
|
||||||
print("Fetching latest version from GitHub...")
|
|
||||||
try:
|
|
||||||
commits = self.github.get_commits("lzhoang2801", "OpCore-Simplify")
|
|
||||||
return commits["commitGroups"][0]["commits"][0]["oid"]
|
|
||||||
except Exception as e:
|
|
||||||
print("Error fetching latest SHA version: {}".format(str(e)))
|
|
||||||
|
|
||||||
return None
|
try:
|
||||||
|
temp_manifest_zip_path = os.path.join(self.temporary_dir, "remote_manifest.json.zip")
|
||||||
|
success = self.fetcher.download_and_save_file(self.remote_manifest_url, temp_manifest_zip_path)
|
||||||
|
|
||||||
|
if not success or not os.path.exists(temp_manifest_zip_path):
|
||||||
|
return None
|
||||||
|
|
||||||
def download_update(self):
|
self.utils.extract_zip_file(temp_manifest_zip_path, self.temporary_dir)
|
||||||
self.current_step += 1
|
|
||||||
print("")
|
remote_manifest_path = os.path.join(self.temporary_dir, "manifest.json")
|
||||||
print("Step {}: Creating temporary directory...".format(self.current_step))
|
manifest_data = self.utils.read_file(remote_manifest_path)
|
||||||
|
|
||||||
|
if dialog:
|
||||||
|
dialog.update_progress(20, "Manifest downloaded successfully")
|
||||||
|
|
||||||
|
return manifest_data
|
||||||
|
except Exception as e:
|
||||||
|
self.utils.log_message("[UPDATER] Error fetching remote manifest: {}".format(str(e)), level="ERROR")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_local_manifest(self, dialog=None):
|
||||||
|
if dialog:
|
||||||
|
dialog.update_progress(40, "Generating local manifest...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest_data = self.integrity_checker.generate_folder_manifest(self.root_dir, save_manifest=False)
|
||||||
|
|
||||||
|
if dialog:
|
||||||
|
dialog.update_progress(50, "Local manifest generated")
|
||||||
|
|
||||||
|
return manifest_data
|
||||||
|
except Exception as e:
|
||||||
|
self.utils.log_message("[UPDATER] Error generating local manifest: {}".format(str(e)), level="ERROR")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def compare_manifests(self, local_manifest, remote_manifest):
|
||||||
|
if not local_manifest or not remote_manifest:
|
||||||
|
return None
|
||||||
|
|
||||||
|
files_to_update = {
|
||||||
|
"modified": [],
|
||||||
|
"missing": [],
|
||||||
|
"new": []
|
||||||
|
}
|
||||||
|
|
||||||
|
local_files = set(local_manifest.keys())
|
||||||
|
remote_files = set(remote_manifest.keys())
|
||||||
|
|
||||||
|
for file_path in local_files & remote_files:
|
||||||
|
if local_manifest[file_path] != remote_manifest[file_path]:
|
||||||
|
files_to_update["modified"].append(file_path)
|
||||||
|
|
||||||
|
files_to_update["missing"] = list(remote_files - local_files)
|
||||||
|
|
||||||
|
files_to_update["new"] = list(local_files - remote_files)
|
||||||
|
|
||||||
|
total_changes = len(files_to_update["modified"]) + len(files_to_update["missing"])
|
||||||
|
|
||||||
|
return files_to_update if total_changes > 0 else None
|
||||||
|
|
||||||
|
def download_update(self, dialog=None):
|
||||||
|
if dialog:
|
||||||
|
dialog.update_progress(60, "Creating temporary directory...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.utils.create_folder(self.temporary_dir)
|
self.utils.create_folder(self.temporary_dir)
|
||||||
print(" Temporary directory created.")
|
|
||||||
|
|
||||||
self.current_step += 1
|
if dialog:
|
||||||
print("Step {}: Downloading update package...".format(self.current_step))
|
dialog.update_progress(65, "Downloading update package...")
|
||||||
print(" ", end="")
|
|
||||||
file_path = os.path.join(self.temporary_dir, os.path.basename(self.download_repo_url))
|
|
||||||
self.fetcher.download_and_save_file(self.download_repo_url, file_path)
|
|
||||||
|
|
||||||
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
|
file_path = os.path.join(self.temporary_dir, "update.zip")
|
||||||
print(" Update package downloaded ({:.1f} KB)".format(os.path.getsize(file_path)/1024))
|
success = self.fetcher.download_and_save_file(self.download_repo_url, file_path)
|
||||||
|
|
||||||
self.current_step += 1
|
if not success or not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
|
||||||
print("Step {}: Extracting files...".format(self.current_step))
|
|
||||||
self.utils.extract_zip_file(file_path)
|
|
||||||
print(" Files extracted successfully")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(" Download failed or file is empty")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if dialog:
|
||||||
|
dialog.update_progress(75, "Extracting files...")
|
||||||
|
|
||||||
|
self.utils.extract_zip_file(file_path, self.temporary_dir)
|
||||||
|
|
||||||
|
if dialog:
|
||||||
|
dialog.update_progress(80, "Files extracted successfully")
|
||||||
|
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(" Error during download/extraction: {}".format(str(e)))
|
self.utils.log_message("[UPDATER] Error during download/extraction: {}".format(str(e)), level="ERROR")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_files(self):
|
def update_files(self, files_to_update, dialog=None):
|
||||||
self.current_step += 1
|
if not files_to_update:
|
||||||
print("Step {}: Updating files...".format(self.current_step))
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
target_dir = os.path.join(self.temporary_dir, "OpCore-Simplify-main")
|
target_dir = os.path.join(self.temporary_dir, "OpCore-Simplify-main")
|
||||||
|
|
||||||
if not os.path.exists(target_dir):
|
if not os.path.exists(target_dir):
|
||||||
target_dir = os.path.join(self.temporary_dir, "main", "OpCore-Simplify-main")
|
self.utils.log_message("[UPDATER] Target directory not found: {}".format(target_dir), level="ERROR")
|
||||||
|
|
||||||
if not os.path.exists(target_dir):
|
|
||||||
print(" Could not locate extracted files directory")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
file_paths = self.utils.find_matching_paths(target_dir, type_filter="file")
|
|
||||||
|
|
||||||
total_files = len(file_paths)
|
all_files = files_to_update["modified"] + files_to_update["missing"]
|
||||||
print(" Found {} files to update".format(total_files))
|
total_files = len(all_files)
|
||||||
|
|
||||||
|
if dialog:
|
||||||
|
dialog.update_progress(85, "Updating {} files...".format(total_files))
|
||||||
|
|
||||||
updated_count = 0
|
updated_count = 0
|
||||||
for index, (path, type) in enumerate(file_paths, start=1):
|
for index, relative_path in enumerate(all_files, start=1):
|
||||||
source = os.path.join(target_dir, path)
|
source = os.path.join(target_dir, relative_path)
|
||||||
destination = source.replace(target_dir, os.path.dirname(os.path.realpath(__file__)))
|
|
||||||
|
if not os.path.exists(source):
|
||||||
|
self.utils.log_message("[UPDATER] Source file not found: {}".format(source), level="ERROR")
|
||||||
|
continue
|
||||||
|
|
||||||
|
destination = os.path.join(self.root_dir, relative_path)
|
||||||
|
|
||||||
self.utils.create_folder(os.path.dirname(destination))
|
self.utils.create_folder(os.path.dirname(destination))
|
||||||
|
|
||||||
print(" Updating [{}/{}]: {}".format(index, total_files, os.path.basename(path)), end="\r")
|
self.utils.log_message("[UPDATER] Updating [{}/{}]: {}".format(index, total_files, os.path.basename(relative_path)), level="INFO")
|
||||||
|
if dialog:
|
||||||
|
progress = 85 + int((index / total_files) * 10)
|
||||||
|
dialog.update_progress(progress, "Updating [{}/{}]: {}".format(index, total_files, os.path.basename(relative_path)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shutil.move(source, destination)
|
shutil.move(source, destination)
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
|
|
||||||
if ".command" in os.path.splitext(path)[-1] and os.name != "nt":
|
if ".command" in os.path.splitext(relative_path)[-1] and os.name != "nt":
|
||||||
self.run({
|
self.run({
|
||||||
"args": ["chmod", "+x", destination]
|
"args": ["chmod", "+x", destination]
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(" Failed to update {}: {}".format(path, str(e)))
|
self.utils.log_message("[UPDATER] Failed to update {}: {}".format(relative_path, str(e)), level="ERROR")
|
||||||
|
|
||||||
print("")
|
if dialog:
|
||||||
print(" Successfully updated {}/{} files".format(updated_count, total_files))
|
dialog.update_progress(95, "Successfully updated {}/{} files".format(updated_count, total_files))
|
||||||
|
|
||||||
self.current_step += 1
|
if os.path.exists(self.temporary_dir):
|
||||||
print("Step {}: Cleaning up temporary files...".format(self.current_step))
|
shutil.rmtree(self.temporary_dir)
|
||||||
shutil.rmtree(self.temporary_dir)
|
|
||||||
print(" Cleanup complete")
|
if dialog:
|
||||||
|
dialog.update_progress(100, "Update completed!")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(" Error during file update: {}".format(str(e)))
|
self.utils.log_message("[UPDATER] Error during file update: {}".format(str(e)), level="ERROR")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def save_latest_sha_version(self, latest_sha):
|
def run_update(self):
|
||||||
try:
|
checker_thread = UpdateCheckerThread(self)
|
||||||
self.utils.write_file(self.sha_version, latest_sha.encode())
|
|
||||||
self.current_step += 1
|
|
||||||
print("Step {}: Version information updated.".format(self.current_step))
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print("Failed to save version information: {}".format(str(e)))
|
|
||||||
return False
|
|
||||||
|
|
||||||
def run_update(self):
|
|
||||||
self.utils.head("Check for Updates")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
current_sha_version = self.get_current_sha_version()
|
def on_update_available(files_to_update):
|
||||||
latest_sha_version = self.get_latest_sha_version()
|
checker_thread.quit()
|
||||||
|
checker_thread.wait()
|
||||||
print("")
|
|
||||||
|
|
||||||
if latest_sha_version is None:
|
|
||||||
print("Could not verify the latest version from GitHub.")
|
|
||||||
print("Current script SHA version: {}".format(current_sha_version))
|
|
||||||
print("Please check your internet connection and try again later.")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
while True:
|
if not show_confirmation("An update is available!", "Would you like to update now?", yes_text="Update", no_text="Later"):
|
||||||
user_input = self.utils.request_input("Do you want to skip the update process? (yes/No): ").strip().lower()
|
return False
|
||||||
if user_input == "yes":
|
|
||||||
print("")
|
|
||||||
print("Update process skipped.")
|
|
||||||
return False
|
|
||||||
elif user_input == "no":
|
|
||||||
print("")
|
|
||||||
print("Continuing with update using default version check...")
|
|
||||||
latest_sha_version = "update_forced_by_user"
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("\033[91mInvalid selection, please try again.\033[0m\n\n")
|
|
||||||
else:
|
|
||||||
print("Current script SHA version: {}".format(current_sha_version))
|
|
||||||
print("Latest script SHA version: {}".format(latest_sha_version))
|
|
||||||
|
|
||||||
print("")
|
|
||||||
|
|
||||||
if latest_sha_version != current_sha_version:
|
|
||||||
print("Update available!")
|
|
||||||
print("Updating from version {} to {}".format(current_sha_version, latest_sha_version))
|
|
||||||
print("")
|
|
||||||
print("Starting update process...")
|
|
||||||
|
|
||||||
if not self.download_update():
|
dialog = show_update_dialog("Updating", "Starting update process...")
|
||||||
print("")
|
dialog.show()
|
||||||
print(" Update failed: Could not download or extract update package")
|
|
||||||
|
try:
|
||||||
|
if not self.download_update(dialog):
|
||||||
|
dialog.close()
|
||||||
|
show_info("Update Failed", "Could not download or extract update package.\n\nPlease check your internet connection and try again.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.update_files(files_to_update, dialog):
|
||||||
|
dialog.close()
|
||||||
|
show_info("Update Failed", "Could not update files.\n\nPlease try again later.")
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog.close()
|
||||||
|
show_info("Update Complete", "Update completed successfully!\n\nThe program needs to restart to complete the update process.")
|
||||||
|
|
||||||
|
os.execv(sys.executable, ["python3"] + sys.argv)
|
||||||
|
except Exception as e:
|
||||||
|
dialog.close()
|
||||||
|
self.utils.log_message("[UPDATER] Error during update: {}".format(str(e)), level="ERROR")
|
||||||
|
show_info("Update Error", "An error occurred during the update process:\n\n{}".format(str(e)))
|
||||||
|
finally:
|
||||||
if os.path.exists(self.temporary_dir):
|
if os.path.exists(self.temporary_dir):
|
||||||
self.current_step += 1
|
try:
|
||||||
print("Step {}: Cleaning up temporary files...".format(self.current_step))
|
shutil.rmtree(self.temporary_dir)
|
||||||
shutil.rmtree(self.temporary_dir)
|
except:
|
||||||
print(" Cleanup complete")
|
pass
|
||||||
|
|
||||||
return False
|
def on_check_failed(error_message):
|
||||||
|
checker_thread.quit()
|
||||||
if not self.update_files():
|
checker_thread.wait()
|
||||||
print("")
|
show_info("Update Check Failed", error_message)
|
||||||
print(" Update failed: Could not update files")
|
|
||||||
return False
|
def on_no_update():
|
||||||
|
checker_thread.quit()
|
||||||
if not self.save_latest_sha_version(latest_sha_version):
|
checker_thread.wait()
|
||||||
print("")
|
|
||||||
print(" Update completed but version information could not be saved")
|
checker_thread.update_available.connect(on_update_available)
|
||||||
|
checker_thread.check_failed.connect(on_check_failed)
|
||||||
print("")
|
checker_thread.no_update.connect(on_no_update)
|
||||||
print("Update completed successfully!")
|
|
||||||
print("")
|
checker_thread.start()
|
||||||
print("The program needs to restart to complete the update process.")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("You are already using the latest version")
|
|
||||||
return False
|
|
||||||
Reference in New Issue
Block a user