mirror of
https://github.com/sickcodes/Docker-OSX.git
synced 2025-06-21 09:02:48 +02:00
492 lines
29 KiB
Python
492 lines
29 KiB
Python
# main_app.py
|
|
import sys
|
|
import subprocess
|
|
import os
|
|
import psutil
|
|
import platform
|
|
import ctypes
|
|
import json
|
|
import re
|
|
import traceback # For better error logging
|
|
import shutil # For shutil.which
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QLabel, QComboBox, QPushButton, QTextEdit, QMessageBox, QMenuBar,
|
|
QFileDialog, QGroupBox, QLineEdit, QProgressBar, QCheckBox
|
|
)
|
|
from PyQt6.QtGui import QAction, QIcon
|
|
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, QTimer, Qt
|
|
|
|
from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS
|
|
# DOCKER_IMAGE_BASE and Docker-related utils are no longer primary for this flow.
|
|
# utils.py might be refactored or parts removed later.
|
|
|
|
# Platform specific USB writers
|
|
USBWriterLinux = None
|
|
USBWriterMacOS = None
|
|
USBWriterWindows = None
|
|
|
|
if platform.system() == "Linux":
|
|
try: from usb_writer_linux import USBWriterLinux
|
|
except ImportError as e: print(f"Could not import USBWriterLinux: {e}")
|
|
elif platform.system() == "Darwin":
|
|
try: from usb_writer_macos import USBWriterMacOS
|
|
except ImportError as e: print(f"Could not import USBWriterMacOS: {e}")
|
|
elif platform.system() == "Windows":
|
|
try: from usb_writer_windows import USBWriterWindows
|
|
except ImportError as e: print(f"Could not import USBWriterWindows: {e}")
|
|
|
|
GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "scripts", "gibMacOS", "gibMacOS.py")
|
|
if not os.path.exists(GIBMACOS_SCRIPT_PATH):
|
|
GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "gibMacOS.py")
|
|
|
|
|
|
class WorkerSignals(QObject):
|
|
progress = pyqtSignal(str)
|
|
finished = pyqtSignal(str)
|
|
error = pyqtSignal(str)
|
|
progress_value = pyqtSignal(int)
|
|
|
|
class GibMacOSWorker(QObject):
|
|
signals = WorkerSignals()
|
|
def __init__(self, version_key: str, download_path: str, catalog_key: str = "publicrelease"):
|
|
super().__init__()
|
|
self.version_key = version_key
|
|
self.download_path = download_path
|
|
self.catalog_key = catalog_key
|
|
self.process = None
|
|
self._is_running = True
|
|
|
|
@pyqtSlot()
|
|
def run(self):
|
|
try:
|
|
script_to_run = ""
|
|
if os.path.exists(GIBMACOS_SCRIPT_PATH):
|
|
script_to_run = GIBMACOS_SCRIPT_PATH
|
|
elif shutil.which("gibMacOS.py"): # Check if it's in PATH
|
|
script_to_run = "gibMacOS.py"
|
|
elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")): # Check alongside main_app.py
|
|
script_to_run = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")
|
|
else:
|
|
self.signals.error.emit(f"gibMacOS.py not found at expected locations or in PATH.")
|
|
return
|
|
|
|
version_for_gib = MACOS_VERSIONS.get(self.version_key, self.version_key)
|
|
os.makedirs(self.download_path, exist_ok=True)
|
|
|
|
command = [sys.executable, script_to_run, "-n", "-c", self.catalog_key, "-v", version_for_gib, "-d", self.download_path]
|
|
self.signals.progress.emit(f"Downloading macOS '{self.version_key}' (as '{version_for_gib}') installer assets...\nCommand: {' '.join(command)}\nOutput will be in: {self.download_path}\n")
|
|
|
|
self.process = subprocess.Popen(
|
|
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
text=True, bufsize=1, universal_newlines=True,
|
|
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
)
|
|
|
|
if self.process.stdout:
|
|
for line in iter(self.process.stdout.readline, ''):
|
|
if not self._is_running:
|
|
self.signals.progress.emit("macOS download process stopping at user request.\n")
|
|
break
|
|
line_strip = line.strip()
|
|
self.signals.progress.emit(line_strip)
|
|
progress_match = re.search(r"(\d+)%", line_strip)
|
|
if progress_match:
|
|
try: self.signals.progress_value.emit(int(progress_match.group(1)))
|
|
except ValueError: pass
|
|
self.process.stdout.close()
|
|
|
|
return_code = self.process.wait()
|
|
|
|
if not self._is_running and return_code != 0:
|
|
self.signals.finished.emit(f"macOS download cancelled or stopped early (exit code {return_code}).")
|
|
return
|
|
|
|
if return_code == 0:
|
|
self.signals.finished.emit(f"macOS '{self.version_key}' installer assets downloaded to '{self.download_path}'.")
|
|
else:
|
|
self.signals.error.emit(f"Failed to download macOS '{self.version_key}' (gibMacOS exit code {return_code}). Check logs.")
|
|
except FileNotFoundError:
|
|
self.signals.error.emit(f"Error: Python or gibMacOS.py script not found. Ensure Python is in PATH and gibMacOS script is correctly located.")
|
|
except Exception as e:
|
|
self.signals.error.emit(f"An error occurred during macOS download: {str(e)}\n{traceback.format_exc()}")
|
|
finally:
|
|
self._is_running = False
|
|
|
|
def stop(self):
|
|
self._is_running = False
|
|
if self.process and self.process.poll() is None:
|
|
self.signals.progress.emit("Attempting to stop macOS download (may not be effective for active downloads)...\n")
|
|
try:
|
|
self.process.terminate(); self.process.wait(timeout=2)
|
|
except subprocess.TimeoutExpired: self.process.kill()
|
|
self.signals.progress.emit("macOS download process termination requested.\n")
|
|
|
|
|
|
class USBWriterWorker(QObject):
|
|
signals = WorkerSignals()
|
|
def __init__(self, device: str, macos_download_path: str,
|
|
enhance_plist: bool, target_macos_version: str):
|
|
super().__init__()
|
|
self.device = device
|
|
self.macos_download_path = macos_download_path
|
|
self.enhance_plist = enhance_plist
|
|
self.target_macos_version = target_macos_version
|
|
self.writer_instance = None
|
|
|
|
@pyqtSlot()
|
|
def run(self):
|
|
current_os = platform.system()
|
|
try:
|
|
writer_cls = None
|
|
if current_os == "Linux": writer_cls = USBWriterLinux
|
|
elif current_os == "Darwin": writer_cls = USBWriterMacOS
|
|
elif current_os == "Windows": writer_cls = USBWriterWindows
|
|
|
|
if writer_cls is None:
|
|
self.signals.error.emit(f"{current_os} USB writer module not available or OS not supported."); return
|
|
|
|
# Platform writers' __init__ will need to be updated for macos_download_path
|
|
# This assumes usb_writer_*.py __init__ signatures are now:
|
|
# __init__(self, device, macos_download_path, progress_callback, enhance_plist_enabled, target_macos_version)
|
|
self.writer_instance = writer_cls(
|
|
device=self.device,
|
|
macos_download_path=self.macos_download_path,
|
|
progress_callback=lambda msg: self.signals.progress.emit(msg),
|
|
enhance_plist_enabled=self.enhance_plist,
|
|
target_macos_version=self.target_macos_version
|
|
)
|
|
|
|
if self.writer_instance.format_and_write():
|
|
self.signals.finished.emit("USB writing process completed successfully.")
|
|
else:
|
|
self.signals.error.emit("USB writing process failed. Check output for details.")
|
|
except Exception as e:
|
|
self.signals.error.emit(f"USB writing preparation error: {str(e)}\n{traceback.format_exc()}")
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setWindowTitle(APP_NAME)
|
|
self.setGeometry(100, 100, 800, 700) # Adjusted height
|
|
|
|
self.active_worker_thread = None
|
|
self.macos_download_path = None
|
|
self.current_worker_instance = None
|
|
|
|
self.spinner_chars = ["|", "/", "-", "\\"]; self.spinner_index = 0
|
|
self.spinner_timer = QTimer(self); self.spinner_timer.timeout.connect(self._update_spinner_status)
|
|
self.base_status_message = "Ready."
|
|
|
|
self._setup_ui()
|
|
self.status_bar = self.statusBar()
|
|
# self.status_bar.addPermanentWidget(self.progress_bar) # Progress bar now in main layout
|
|
self.status_bar.showMessage(self.base_status_message, 5000)
|
|
self.refresh_usb_drives()
|
|
|
|
def _setup_ui(self):
|
|
menubar = self.menuBar(); file_menu = menubar.addMenu("&File"); help_menu = menubar.addMenu("&Help")
|
|
exit_action = QAction("&Exit", self); exit_action.triggered.connect(self.close); file_menu.addAction(exit_action)
|
|
about_action = QAction("&About", self); about_action.triggered.connect(self.show_about_dialog); help_menu.addAction(about_action)
|
|
central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget)
|
|
|
|
# Step 1: Download macOS
|
|
download_group = QGroupBox("Step 1: Download macOS Installer Assets")
|
|
download_layout = QVBoxLayout()
|
|
selection_layout = QHBoxLayout(); self.version_label = QLabel("Select macOS Version:"); self.version_combo = QComboBox()
|
|
self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo)
|
|
download_layout.addLayout(selection_layout)
|
|
|
|
self.download_macos_button = QPushButton("Download macOS Installer Assets")
|
|
self.download_macos_button.clicked.connect(self.start_macos_download_flow)
|
|
download_layout.addWidget(self.download_macos_button)
|
|
|
|
self.cancel_operation_button = QPushButton("Cancel Current Operation")
|
|
self.cancel_operation_button.clicked.connect(self.stop_current_operation)
|
|
self.cancel_operation_button.setEnabled(False)
|
|
download_layout.addWidget(self.cancel_operation_button)
|
|
download_group.setLayout(download_layout)
|
|
main_layout.addWidget(download_group)
|
|
|
|
# Step 2: USB Drive Selection & Writing
|
|
usb_group = QGroupBox("Step 2: Create Bootable USB Installer")
|
|
self.usb_layout = QVBoxLayout()
|
|
self.usb_drive_label = QLabel("Available USB Drives:"); self.usb_layout.addWidget(self.usb_drive_label)
|
|
usb_selection_layout = QHBoxLayout(); self.usb_drive_combo = QComboBox(); self.usb_drive_combo.currentIndexChanged.connect(self.update_all_button_states)
|
|
usb_selection_layout.addWidget(self.usb_drive_combo); self.refresh_usb_button = QPushButton("Refresh List"); self.refresh_usb_button.clicked.connect(self.refresh_usb_drives)
|
|
usb_selection_layout.addWidget(self.refresh_usb_button); self.usb_layout.addLayout(usb_selection_layout)
|
|
self.windows_usb_guidance_label = QLabel("For Windows: Select USB disk from dropdown (WMI). Manual input below if empty/unreliable.")
|
|
self.windows_disk_id_input = QLineEdit(); self.windows_disk_id_input.setPlaceholderText("Disk No. (e.g., 1)"); self.windows_disk_id_input.textChanged.connect(self.update_all_button_states)
|
|
if platform.system() == "Windows": self.usb_layout.addWidget(self.windows_usb_guidance_label); self.usb_layout.addWidget(self.windows_disk_id_input); self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(True)
|
|
else: self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False)
|
|
self.enhance_plist_checkbox = QCheckBox("Try to auto-enhance config.plist for this system's hardware (Experimental, Linux Host Only for detection)")
|
|
self.enhance_plist_checkbox.setChecked(False); self.usb_layout.addWidget(self.enhance_plist_checkbox)
|
|
warning_label = QLabel("WARNING: USB drive will be ERASED!"); warning_label.setStyleSheet("color: red; font-weight: bold;"); self.usb_layout.addWidget(warning_label)
|
|
self.write_to_usb_button = QPushButton("Create macOS Installer USB"); self.write_to_usb_button.clicked.connect(self.handle_write_to_usb)
|
|
self.write_to_usb_button.setEnabled(False); self.usb_layout.addWidget(self.write_to_usb_button); usb_group.setLayout(self.usb_layout); main_layout.addWidget(usb_group)
|
|
|
|
self.progress_bar = QProgressBar(self); self.progress_bar.setRange(0, 0); self.progress_bar.setVisible(False); main_layout.addWidget(self.progress_bar)
|
|
self.output_area = QTextEdit(); self.output_area.setReadOnly(True); main_layout.addWidget(self.output_area)
|
|
self.update_all_button_states()
|
|
|
|
def show_about_dialog(self): QMessageBox.about(self, f"About {APP_NAME}", f"Version: 1.0.0 (Installer Flow)\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using gibMacOS and OpenCore.")
|
|
|
|
def _set_ui_busy(self, busy_status: bool, message: str = "Processing..."):
|
|
self.progress_bar.setVisible(busy_status)
|
|
if busy_status:
|
|
self.base_status_message = message
|
|
if not self.spinner_timer.isActive(): self.spinner_timer.start(150)
|
|
self._update_spinner_status()
|
|
self.progress_bar.setRange(0,0)
|
|
else:
|
|
self.spinner_timer.stop()
|
|
self.status_bar.showMessage(message or "Ready.", 7000)
|
|
self.update_all_button_states()
|
|
|
|
|
|
def _update_spinner_status(self):
|
|
if self.spinner_timer.isActive():
|
|
char = self.spinner_chars[self.spinner_index % len(self.spinner_chars)]
|
|
active_worker_provides_progress = False
|
|
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
|
active_worker_provides_progress = getattr(self.active_worker_thread, "provides_progress", False)
|
|
|
|
if active_worker_provides_progress and self.progress_bar.maximum() == 100: # Determinate
|
|
self.status_bar.showMessage(f"{char} {self.base_status_message} ({self.progress_bar.value()}%)")
|
|
else:
|
|
if self.progress_bar.maximum() != 0: self.progress_bar.setRange(0,0)
|
|
self.status_bar.showMessage(f"{char} {self.base_status_message}")
|
|
self.spinner_index = (self.spinner_index + 1) % len(self.spinner_chars)
|
|
elif not (self.active_worker_thread and self.active_worker_thread.isRunning()):
|
|
self.spinner_timer.stop()
|
|
|
|
def update_all_button_states(self):
|
|
is_worker_active = self.active_worker_thread is not None and self.active_worker_thread.isRunning()
|
|
|
|
self.download_macos_button.setEnabled(not is_worker_active)
|
|
self.version_combo.setEnabled(not is_worker_active)
|
|
self.cancel_operation_button.setEnabled(is_worker_active and self.current_worker_instance is not None)
|
|
|
|
self.refresh_usb_button.setEnabled(not is_worker_active)
|
|
self.usb_drive_combo.setEnabled(not is_worker_active)
|
|
if platform.system() == "Windows": self.windows_disk_id_input.setEnabled(not is_worker_active)
|
|
self.enhance_plist_checkbox.setEnabled(not is_worker_active)
|
|
|
|
# Write to USB button logic
|
|
macos_assets_ready = bool(self.macos_download_path and os.path.isdir(self.macos_download_path))
|
|
usb_identified = False
|
|
current_os = platform.system(); writer_module = None
|
|
if current_os == "Linux": writer_module = USBWriterLinux; usb_identified = bool(self.usb_drive_combo.currentData())
|
|
elif current_os == "Darwin": writer_module = USBWriterMacOS; usb_identified = bool(self.usb_drive_combo.currentData())
|
|
elif current_os == "Windows":
|
|
writer_module = USBWriterWindows
|
|
usb_identified = bool(self.usb_drive_combo.currentData()) or bool(self.windows_disk_id_input.text().strip())
|
|
|
|
self.write_to_usb_button.setEnabled(not is_worker_active and macos_assets_ready and usb_identified and writer_module is not None)
|
|
tooltip = ""
|
|
if writer_module is None: tooltip = f"USB Writing not supported on {current_os} or module missing."
|
|
elif not macos_assets_ready: tooltip = "Download macOS installer assets first (Step 1)."
|
|
elif not usb_identified: tooltip = "Select or identify a target USB drive."
|
|
else: tooltip = ""
|
|
self.write_to_usb_button.setToolTip(tooltip)
|
|
|
|
|
|
def _start_worker(self, worker_instance, on_finished_slot, on_error_slot, worker_name="worker", provides_progress=False):
|
|
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
|
QMessageBox.warning(self, "Busy", "Another operation is in progress."); return False
|
|
|
|
self._set_ui_busy(True, f"Starting {worker_name.replace('_', ' ')}...")
|
|
self.current_worker_instance = worker_instance
|
|
|
|
if provides_progress:
|
|
self.progress_bar.setRange(0,100)
|
|
worker_instance.signals.progress_value.connect(self.update_progress_bar_value)
|
|
else:
|
|
self.progress_bar.setRange(0,0)
|
|
|
|
self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread")
|
|
setattr(self.active_worker_thread, "provides_progress", provides_progress)
|
|
|
|
worker_instance.moveToThread(self.active_worker_thread)
|
|
worker_instance.signals.progress.connect(self.update_output)
|
|
worker_instance.signals.finished.connect(lambda msg, wn=worker_name, slot=on_finished_slot: self._handle_worker_finished(msg, wn, slot))
|
|
worker_instance.signals.error.connect(lambda err, wn=worker_name, slot=on_error_slot: self._handle_worker_error(err, wn, slot))
|
|
self.active_worker_thread.finished.connect(self.active_worker_thread.deleteLater)
|
|
self.active_worker_thread.started.connect(worker_instance.run)
|
|
self.active_worker_thread.start()
|
|
return True
|
|
|
|
@pyqtSlot(int)
|
|
def update_progress_bar_value(self, value):
|
|
if self.progress_bar.maximum() == 0: self.progress_bar.setRange(0,100)
|
|
self.progress_bar.setValue(value)
|
|
# Spinner update will happen on its timer, it can check progress_bar.value()
|
|
|
|
def _handle_worker_finished(self, message, worker_name, specific_finished_slot):
|
|
final_msg = f"{worker_name.replace('_', ' ').capitalize()} completed."
|
|
self.current_worker_instance = None # Clear current worker
|
|
self.active_worker_thread = None
|
|
if specific_finished_slot: specific_finished_slot(message)
|
|
self._set_ui_busy(False, final_msg)
|
|
|
|
def _handle_worker_error(self, error_message, worker_name, specific_error_slot):
|
|
final_msg = f"{worker_name.replace('_', ' ').capitalize()} failed."
|
|
self.current_worker_instance = None # Clear current worker
|
|
self.active_worker_thread = None
|
|
if specific_error_slot: specific_error_slot(error_message)
|
|
self._set_ui_busy(False, final_msg)
|
|
|
|
def start_macos_download_flow(self):
|
|
self.output_area.clear(); selected_version_name = self.version_combo.currentText()
|
|
gibmacos_version_arg = MACOS_VERSIONS.get(selected_version_name, selected_version_name)
|
|
|
|
chosen_path = QFileDialog.getExistingDirectory(self, "Select Directory to Download macOS Installer Assets")
|
|
if not chosen_path: self.output_area.append("Download directory selection cancelled."); return
|
|
self.macos_download_path = chosen_path
|
|
|
|
worker = GibMacOSWorker(gibmacos_version_arg, self.macos_download_path)
|
|
if not self._start_worker(worker, self.macos_download_finished, self.macos_download_error,
|
|
"macos_download",
|
|
f"Downloading macOS {selected_version_name} assets...",
|
|
provides_progress=True): # Assuming GibMacOSWorker will emit progress_value
|
|
self._set_ui_busy(False, "Failed to start macOS download operation.")
|
|
|
|
|
|
@pyqtSlot(str)
|
|
def macos_download_finished(self, message):
|
|
QMessageBox.information(self, "Download Complete", message)
|
|
# self.macos_download_path is set. UI update handled by generic handler.
|
|
|
|
@pyqtSlot(str)
|
|
def macos_download_error(self, error_message):
|
|
QMessageBox.critical(self, "Download Error", error_message)
|
|
self.macos_download_path = None
|
|
# UI reset by generic handler.
|
|
|
|
def stop_current_operation(self):
|
|
if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'):
|
|
self.output_area.append(f"
|
|
--- Attempting to stop {self.active_worker_thread.objectName().replace('_thread','')} ---")
|
|
self.current_worker_instance.stop()
|
|
else:
|
|
self.output_area.append("
|
|
--- No active stoppable operation or stop method not implemented for current worker. ---")
|
|
|
|
def handle_error(self, message):
|
|
self.output_area.append(f"ERROR: {message}"); QMessageBox.critical(self, "Error", message)
|
|
self._set_ui_busy(False, "Error occurred.")
|
|
|
|
def check_admin_privileges(self) -> bool: # ... (same)
|
|
try:
|
|
if platform.system() == "Windows": return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
|
else: return os.geteuid() == 0
|
|
except Exception as e: self.output_area.append(f"Could not check admin privileges: {e}"); return False
|
|
|
|
def refresh_usb_drives(self): # ... (same logic as before)
|
|
self.usb_drive_combo.clear(); current_selection_text = getattr(self, '_current_usb_selection_text', None)
|
|
self.output_area.append("
|
|
Scanning for disk devices...")
|
|
if platform.system() == "Windows":
|
|
self.usb_drive_label.setText("Available USB Disks (Windows - via WMI/PowerShell):")
|
|
self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(False);
|
|
powershell_command = "Get-WmiObject Win32_DiskDrive | Where-Object {$_.InterfaceType -eq 'USB'} | Select-Object DeviceID, Index, Model, @{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}} | ConvertTo-Json"
|
|
try:
|
|
process = subprocess.run(["powershell", "-Command", powershell_command], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
disks_data = json.loads(process.stdout); disks_json = disks_data if isinstance(disks_data, list) else [disks_data] if disks_data else []
|
|
if disks_json:
|
|
for disk in disks_json:
|
|
if disk.get('DeviceID') is None or disk.get('Index') is None: continue
|
|
disk_text = f"Disk {disk['Index']}: {disk.get('Model','N/A')} ({disk.get('SizeGB','N/A')} GB) - {disk['DeviceID']}"
|
|
self.usb_drive_combo.addItem(disk_text, userData=str(disk['Index']))
|
|
self.output_area.append(f"Found {len(disks_json)} USB disk(s) via WMI.");
|
|
if current_selection_text:
|
|
for i in range(self.usb_drive_combo.count()):
|
|
if self.usb_drive_combo.itemText(i) == current_selection_text: self.usb_drive_combo.setCurrentIndex(i); break
|
|
else: self.output_area.append("No USB disks found via WMI/PowerShell. Manual input field shown as fallback."); self.windows_disk_id_input.setVisible(True)
|
|
except Exception as e: self.output_area.append(f"Error scanning Windows USBs with PowerShell: {e}"); self.windows_disk_id_input.setVisible(True)
|
|
else:
|
|
self.usb_drive_label.setText("Available USB Drives (for Linux/macOS):")
|
|
self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False)
|
|
try:
|
|
partitions = psutil.disk_partitions(all=False); potential_usbs = []
|
|
for p in partitions:
|
|
is_removable = 'removable' in p.opts; is_likely_usb = False
|
|
if platform.system() == "Darwin" and p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True
|
|
elif platform.system() == "Linux" and ((p.mountpoint and ("/media/" in p.mountpoint or "/run/media/" in p.mountpoint)) or (p.device.startswith("/dev/sd") and not p.device.endswith("da"))): is_likely_usb = True
|
|
if is_removable or is_likely_usb:
|
|
try: usage = psutil.disk_usage(p.mountpoint); size_gb = usage.total / (1024**3)
|
|
except Exception: continue
|
|
if size_gb < 0.1 : continue
|
|
drive_text = f"{p.device} @ {p.mountpoint} ({p.fstype}, {size_gb:.2f} GB)"
|
|
potential_usbs.append((drive_text, p.device))
|
|
if potential_usbs:
|
|
idx_to_select = -1
|
|
for i, (text, device_path) in enumerate(potential_usbs): self.usb_drive_combo.addItem(text, userData=device_path);
|
|
if text == current_selection_text: idx_to_select = i
|
|
if idx_to_select != -1: self.usb_drive_combo.setCurrentIndex(idx_to_select)
|
|
self.output_area.append(f"Found {len(potential_usbs)} potential USB drive(s). Please verify carefully.")
|
|
else: self.output_area.append("No suitable USB drives found for Linux/macOS.")
|
|
except ImportError: self.output_area.append("psutil library not found.")
|
|
except Exception as e: self.output_area.append(f"Error scanning for USB drives: {e}")
|
|
self.update_all_button_states()
|
|
|
|
|
|
def handle_write_to_usb(self):
|
|
if not self.check_admin_privileges(): QMessageBox.warning(self, "Privileges Required", "This operation requires Administrator/root privileges."); return
|
|
if not self.macos_download_path or not os.path.isdir(self.macos_download_path): QMessageBox.warning(self, "Missing macOS Assets", "Download macOS installer assets first."); return
|
|
current_os = platform.system(); usb_writer_module = None; target_device_id_for_worker = None
|
|
if current_os == "Windows": target_device_id_for_worker = self.usb_drive_combo.currentData() or self.windows_disk_id_input.text().strip(); usb_writer_module = USBWriterWindows
|
|
else: target_device_id_for_worker = self.usb_drive_combo.currentData(); usb_writer_module = USBWriterLinux if current_os == "Linux" else USBWriterMacOS if current_os == "Darwin" else None
|
|
if not usb_writer_module: QMessageBox.warning(self, "Unsupported Platform", f"USB writing not supported for {current_os}."); return
|
|
if not target_device_id_for_worker: QMessageBox.warning(self, "No USB Selected/Identified", f"Please select/identify target USB."); return
|
|
if current_os == "Windows" and target_device_id_for_worker.isdigit(): target_device_id_for_worker = f"disk {target_device_id_for_worker}"
|
|
|
|
enhance_plist_state = self.enhance_plist_checkbox.isChecked()
|
|
target_macos_name = self.version_combo.currentText()
|
|
reply = QMessageBox.warning(self, "Confirm Write Operation", f"WARNING: ALL DATA ON TARGET '{target_device_id_for_worker}' WILL BE ERASED.
|
|
Proceed?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel)
|
|
if reply == QMessageBox.StandardButton.Cancel: self.output_area.append("
|
|
USB write cancelled."); return
|
|
|
|
# USBWriterWorker now needs different args
|
|
# The platform specific writers (USBWriterLinux etc) will need to be updated to accept macos_download_path
|
|
# and use it to find BaseSystem.dmg, EFI/OC etc. instead of opencore_qcow2_path, macos_qcow2_path
|
|
usb_worker_adapted = USBWriterWorker(
|
|
device=target_device_id_for_worker,
|
|
macos_download_path=self.macos_download_path,
|
|
enhance_plist=enhance_plist_state,
|
|
target_macos_version=target_macos_name
|
|
)
|
|
|
|
if not self._start_worker(usb_worker_adapted, self.usb_write_finished, self.usb_write_error, "usb_write_worker",
|
|
busy_message=f"Creating USB for {target_device_id_for_worker}...",
|
|
provides_progress=False): # USB writing can be long, but progress parsing is per-platform script.
|
|
self._set_ui_busy(False, "Failed to start USB write operation.")
|
|
|
|
@pyqtSlot(str)
|
|
def usb_write_finished(self, message): QMessageBox.information(self, "USB Write Complete", message)
|
|
@pyqtSlot(str)
|
|
def usb_write_error(self, error_message): QMessageBox.critical(self, "USB Write Error", error_message)
|
|
|
|
def closeEvent(self, event): # ... (same logic)
|
|
self._current_usb_selection_text = self.usb_drive_combo.currentText()
|
|
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
|
reply = QMessageBox.question(self, 'Confirm Exit', "An operation is running. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'): self.current_worker_instance.stop()
|
|
else: self.active_worker_thread.quit()
|
|
self.active_worker_thread.wait(1000); event.accept()
|
|
else: event.ignore(); return
|
|
else: event.accept()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import traceback # Ensure traceback is available for GibMacOSWorker
|
|
import shutil # Ensure shutil is available for GibMacOSWorker path check
|
|
app = QApplication(sys.argv)
|
|
window = MainWindow()
|
|
window.show()
|
|
sys.exit(app.exec())
|