mirror of
https://github.com/sickcodes/Docker-OSX.git
synced 2025-06-21 00:52:51 +02:00
This commit introduces several major enhancements: 1. **Experimental `config.plist` Auto-Enhancement (Linux Host for Detection):** * `linux_hardware_info.py`: Added audio codec detection. * `plist_modifier.py`: * Uses detected audio codecs for more accurate `layout-id` selection. * Expanded mappings for Intel Alder Lake iGPUs, more audio devices, and Ethernet kexts. * Refined NVIDIA GTX 970 `boot-args` logic based on target macOS version and iGPU presence. * Creates a `.backup` of `config.plist` before modification and attempts restore on save failure. * Integrated into `main_app.py` with a user-selectable experimental checkbox. 2. **Reworked `README.md`:** * Completely rewritten for "Skyscope" branding and project vision. * Details all current features, including platform-specific USB writing (manual Windows dd step). * Comprehensive prerequisites, including `apfs-fuse` build dependencies for Debian. * Updated usage guide and future enhancement plans. Version set to 0.8.2. 3. **UI/UX Enhancements for Task Progress:** * Added a QTimer-driven text-based spinner to the status bar for active operations. * Centralized UI state management (`_set_ui_busy`, `update_all_button_states`) for consistent feedback and control enabling/disabling. * Refactored completion/error handling into generic slots. 4. **Improved Windows USB Writing Guidance:** * `usb_writer_windows.py` now uses `diskpart` to fetch and display the macOS partition number and byte offset, providing more specific details for your manual `dd` operation. 5. **Debian 13 "Trixie" Compatibility:** * Reviewed dependencies and updated `README.md` with specific notes for `hfsprogs` and `apfs-fuse` installation on Debian-based systems. This set of changes makes the application more intelligent in its OpenCore configuration attempts, improves your feedback during operations, and provides much more comprehensive documentation, while also advancing the capabilities of the platform-specific USB writers.
270 lines
16 KiB
Python
270 lines
16 KiB
Python
# usb_writer_windows.py
|
|
import subprocess
|
|
import os
|
|
import time
|
|
import shutil
|
|
import re # For parsing diskpart output
|
|
import sys # For checking psutil import
|
|
|
|
# Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test
|
|
try:
|
|
from PyQt6.QtWidgets import QMessageBox
|
|
except ImportError:
|
|
class QMessageBox: # Mock for standalone testing
|
|
@staticmethod
|
|
def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'")
|
|
@staticmethod
|
|
def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox # Mock button press
|
|
Yes = 1 # Mock value
|
|
No = 0 # Mock value
|
|
Cancel = 0 # Mock value
|
|
|
|
|
|
class USBWriterWindows:
|
|
def __init__(self, device_id: str, opencore_qcow2_path: str, macos_qcow2_path: str,
|
|
progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""):
|
|
# device_id is expected to be the disk number string, e.g., "1", "2" or "disk 1", "disk 2"
|
|
self.disk_number = "".join(filter(str.isdigit, device_id))
|
|
if not self.disk_number:
|
|
raise ValueError(f"Invalid device_id format: '{device_id}'. Must contain a disk number.")
|
|
|
|
self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}"
|
|
|
|
self.opencore_qcow2_path = opencore_qcow2_path
|
|
self.macos_qcow2_path = macos_qcow2_path
|
|
self.progress_callback = progress_callback
|
|
self.enhance_plist_enabled = enhance_plist_enabled # Not used in Windows writer yet
|
|
self.target_macos_version = target_macos_version # Not used in Windows writer yet
|
|
|
|
pid = os.getpid()
|
|
self.opencore_raw_path = f"opencore_temp_{pid}.raw"
|
|
self.macos_raw_path = f"macos_main_temp_{pid}.raw"
|
|
self.temp_efi_extract_dir = f"temp_efi_files_{pid}"
|
|
|
|
self.temp_files_to_clean = [self.opencore_raw_path, self.macos_raw_path]
|
|
self.temp_dirs_to_clean = [self.temp_efi_extract_dir]
|
|
self.assigned_efi_letter = None
|
|
|
|
def _report_progress(self, message: str):
|
|
if self.progress_callback: self.progress_callback(message)
|
|
else: print(message)
|
|
|
|
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None):
|
|
self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}")
|
|
try:
|
|
process = subprocess.run(
|
|
command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir,
|
|
creationflags=subprocess.CREATE_NO_WINDOW
|
|
)
|
|
if capture_output:
|
|
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
|
|
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
|
|
return process
|
|
except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
|
|
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
|
|
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise
|
|
|
|
|
|
def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None:
|
|
script_file_path = f"diskpart_script_{os.getpid()}.txt"
|
|
with open(script_file_path, "w") as f: f.write(script_content)
|
|
output_text = "" # Initialize to empty string
|
|
try:
|
|
self._report_progress(f"Running diskpart script:\n{script_content}")
|
|
process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False)
|
|
output_text = (process.stdout or "") + "\n" + (process.stderr or "") # Combine, as diskpart output can be inconsistent
|
|
|
|
# Check for known success messages, otherwise assume potential issue or log output for manual check.
|
|
# This is not a perfect error check for diskpart.
|
|
success_indicators = [
|
|
"DiskPart successfully", "successfully completed", "succeeded in creating",
|
|
"successfully formatted", "successfully assigned"
|
|
]
|
|
has_success_indicator = any(indicator in output_text for indicator in success_indicators)
|
|
has_error_indicator = "Virtual Disk Service error" in output_text or "DiskPart has encountered an error" in output_text
|
|
|
|
if has_error_indicator:
|
|
self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}")
|
|
# Optionally raise an error here if script is critical
|
|
# raise subprocess.CalledProcessError(1, "diskpart", output=output_text)
|
|
elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text: # Allow benign message
|
|
self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}")
|
|
|
|
|
|
if capture_output_for_parse:
|
|
return output_text
|
|
finally:
|
|
if os.path.exists(script_file_path): os.remove(script_file_path)
|
|
return output_text if capture_output_for_parse else None # Return None if not capturing for parse
|
|
|
|
|
|
def _cleanup_temp_files_and_dirs(self):
|
|
self._report_progress("Cleaning up...")
|
|
for f_path in self.temp_files_to_clean:
|
|
if os.path.exists(f_path):
|
|
try: os.remove(f_path)
|
|
except Exception as e: self._report_progress(f"Could not remove temp file {f_path}: {e}")
|
|
for d_path in self.temp_dirs_to_clean:
|
|
if os.path.exists(d_path):
|
|
try: shutil.rmtree(d_path, ignore_errors=True)
|
|
except Exception as e: self._report_progress(f"Could not remove temp dir {d_path}: {e}")
|
|
|
|
|
|
def _find_available_drive_letter(self) -> str | None:
|
|
import string; used_letters = set()
|
|
try:
|
|
# Check if psutil was imported by the main application
|
|
if 'psutil' in sys.modules:
|
|
partitions = sys.modules['psutil'].disk_partitions(all=True)
|
|
for p in partitions:
|
|
if p.mountpoint and len(p.mountpoint) >= 2 and p.mountpoint[1] == ':': # Check for "X:"
|
|
used_letters.add(p.mountpoint[0].upper())
|
|
except Exception as e:
|
|
self._report_progress(f"Could not list used drive letters with psutil: {e}. Will try common letters.")
|
|
|
|
for letter in "STUVWXYZGHIJKLMNOPQR":
|
|
if letter not in used_letters and letter > 'D': # Avoid A, B, C, D
|
|
# Further check if letter is truly available (e.g. subst) - more complex, skip for now
|
|
return letter
|
|
return None
|
|
|
|
def check_dependencies(self):
|
|
self._report_progress("Checking dependencies (qemu-img, diskpart, robocopy)... DD for Win & 7z are manual checks.")
|
|
dependencies = ["qemu-img", "diskpart", "robocopy"]; missing = [dep for dep in dependencies if not shutil.which(dep)]
|
|
if missing: raise RuntimeError(f"Missing dependencies: {', '.join(missing)}. qemu-img needs install & PATH.")
|
|
self._report_progress("Base dependencies found. Ensure 'dd for Windows' and '7z.exe' are in PATH if needed.")
|
|
return True
|
|
|
|
def format_and_write(self) -> bool:
|
|
try:
|
|
self.check_dependencies()
|
|
self._cleanup_temp_files_and_dirs() # Clean before start
|
|
os.makedirs(self.temp_efi_extract_dir, exist_ok=True)
|
|
|
|
self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!")
|
|
|
|
self.assigned_efi_letter = self._find_available_drive_letter()
|
|
if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.")
|
|
self._report_progress(f"Will attempt to assign letter {self.assigned_efi_letter}: to EFI partition.")
|
|
|
|
diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n"
|
|
diskpart_script_part1 += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n"
|
|
diskpart_script_part1 += "create partition primary label=macOS_USB\nexit\n"
|
|
self._run_diskpart_script(diskpart_script_part1)
|
|
time.sleep(5)
|
|
|
|
macos_partition_offset_str = "Offset not determined"
|
|
macos_partition_number_str = "2 (assumed)"
|
|
|
|
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n"
|
|
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
|
|
|
|
if detail_output:
|
|
self._report_progress(f"Detail Partition Output:\n{detail_output}")
|
|
offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE)
|
|
if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
|
|
|
|
# Try to find the line "Partition X" where X is the number we want
|
|
part_num_search = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE)
|
|
if part_num_search:
|
|
macos_partition_number_str = part_num_search.group(1)
|
|
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
|
|
else: # Fallback if the above specific regex fails
|
|
# Look for lines like "Partition 2", "Type : xxxxx"
|
|
# This is brittle if diskpart output format changes
|
|
partition_lines = [line for line in detail_output.splitlines() if "Partition " in line and "Type :" in line]
|
|
if len(partition_lines) > 0 : # Assuming the one we want is the last "Partition X" before other details
|
|
last_part_match = re.search(r"Partition\s*(\d+)", partition_lines[-1])
|
|
if last_part_match: macos_partition_number_str = last_part_match.group(1)
|
|
|
|
|
|
self._report_progress(f"Converting OpenCore QCOW2 to RAW: {self.opencore_raw_path}")
|
|
self._run_command(["qemu-img", "convert", "-O", "raw", self.opencore_qcow2_path, self.opencore_raw_path])
|
|
|
|
if shutil.which("7z"):
|
|
self._report_progress("Attempting EFI extraction using 7-Zip...")
|
|
self._run_command(["7z", "x", self.opencore_raw_path, f"-o{self.temp_efi_extract_dir}", "EFI", "-r", "-y"], check=False)
|
|
source_efi_folder = os.path.join(self.temp_efi_extract_dir, "EFI")
|
|
if not os.path.isdir(source_efi_folder):
|
|
if os.path.exists(os.path.join(self.temp_efi_extract_dir, "BOOTX64.EFI")): source_efi_folder = self.temp_efi_extract_dir
|
|
else: raise RuntimeError("Could not extract EFI folder using 7-Zip from OpenCore image.")
|
|
|
|
target_efi_on_usb = f"{self.assigned_efi_letter}:\\EFI"
|
|
if not os.path.exists(f"{self.assigned_efi_letter}:\\"): # Check if drive letter is mounted
|
|
time.sleep(3) # Wait a bit more
|
|
if not os.path.exists(f"{self.assigned_efi_letter}:\\"):
|
|
# Attempt to re-assign just in case
|
|
self._report_progress(f"Re-assigning drive letter {self.assigned_efi_letter} to EFI partition...")
|
|
reassign_script = f"select disk {self.disk_number}\nselect partition 1\nassign letter={self.assigned_efi_letter}\nexit\n"
|
|
self._run_diskpart_script(reassign_script)
|
|
time.sleep(3)
|
|
if not os.path.exists(f"{self.assigned_efi_letter}:\\"):
|
|
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign/re-assign.")
|
|
|
|
if not os.path.exists(target_efi_on_usb): os.makedirs(target_efi_on_usb, exist_ok=True)
|
|
self._report_progress(f"Copying EFI files from '{source_efi_folder}' to '{target_efi_on_usb}'")
|
|
self._run_command(["robocopy", source_efi_folder, target_efi_on_usb, "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True) # Added /XO to exclude older
|
|
else: raise RuntimeError("7-Zip CLI (7z.exe) not found in PATH for EFI extraction.")
|
|
|
|
self._report_progress(f"Converting macOS QCOW2 to RAW: {self.macos_raw_path}")
|
|
self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path])
|
|
|
|
abs_macos_raw_path = os.path.abspath(self.macos_raw_path)
|
|
guidance_message = (
|
|
f"RAW macOS image conversion complete:\n'{abs_macos_raw_path}'\n\n"
|
|
f"Target USB: Disk {self.disk_number} (Path: {self.physical_drive_path})\n"
|
|
f"The target macOS partition is: Partition {macos_partition_number_str}\n"
|
|
f"Calculated Offset (approx): {macos_partition_offset_str}\n\n"
|
|
"MANUAL STEP REQUIRED using a 'dd for Windows' utility:\n"
|
|
"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
|
|
"2. Carefully identify your 'dd for Windows' utility and its exact syntax.\n"
|
|
" Common utilities: dd from SUSE (recommended), dd by chrysocome.net.\n"
|
|
"3. Example 'dd' command (SYNTAX VARIES GREATLY BETWEEN DD TOOLS!):\n"
|
|
f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} bs=4M --progress`\n"
|
|
" (This example writes to the whole disk, which might be okay if your macOS partition is the first primary after EFI and occupies the rest). \n"
|
|
" A SAFER (but more complex) approach if your 'dd' supports it, is to write directly to the partition's OFFSET (requires dd that handles PhysicalDrive offsets correctly):\n"
|
|
f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} seek=<PARTITION_OFFSET_IN_BLOCKS_OR_BYTES> bs=<YOUR_BLOCK_SIZE> ...`\n"
|
|
" (The 'seek' parameter and its units depend on your dd tool. The offset from diskpart is in bytes.)\n\n"
|
|
"VERIFY YOUR DD COMMAND AND TARGETS BEFORE EXECUTION. DATA LOSS IS LIKELY IF INCORRECT.\n"
|
|
"This tool cannot automate this step due to the variability and risks of 'dd' utilities on Windows."
|
|
)
|
|
self._report_progress(f"GUIDANCE:\n{guidance_message}")
|
|
QMessageBox.information(None, "Manual macOS Image Write Required", guidance_message)
|
|
|
|
self._report_progress("Windows USB writing (EFI part automated, macOS part manual guidance provided) process initiated.")
|
|
return True
|
|
|
|
except Exception as e:
|
|
self._report_progress(f"Error during Windows USB writing: {e}")
|
|
import traceback; self._report_progress(traceback.format_exc())
|
|
return False
|
|
finally:
|
|
if self.assigned_efi_letter:
|
|
self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit")
|
|
self._cleanup_temp_files_and_dirs()
|
|
|
|
if __name__ == '__main__':
|
|
if platform.system() != "Windows":
|
|
print("This script is for Windows standalone testing."); exit(1)
|
|
print("USB Writer Windows Standalone Test - Improved Guidance")
|
|
mock_oc = "mock_oc_win.qcow2"; mock_mac = "mock_mac_win.qcow2"
|
|
# Ensure qemu-img is available for mock file creation
|
|
if not shutil.which("qemu-img"):
|
|
print("qemu-img not found, cannot create mock files for test. Exiting.")
|
|
exit(1)
|
|
if not os.path.exists(mock_oc): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_oc, "384M"])
|
|
if not os.path.exists(mock_mac): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_mac, "1G"])
|
|
|
|
disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). THIS DISK WILL BE WIPES: ")
|
|
if not disk_id_input.isdigit(): print("Invalid disk number."); exit(1)
|
|
|
|
if input(f"Sure to wipe disk {disk_id_input}? (yes/NO): ").lower() == 'yes':
|
|
# USBWriterWindows expects just the disk number string (e.g., "1")
|
|
writer = USBWriterWindows(disk_id_input, mock_oc, mock_mac, print)
|
|
writer.format_and_write()
|
|
else: print("Cancelled.")
|
|
|
|
if os.path.exists(mock_oc): os.remove(mock_oc)
|
|
if os.path.exists(mock_mac): os.remove(mock_mac)
|
|
print("Mocks cleaned.")
|