mirror of
https://github.com/sickcodes/Docker-OSX.git
synced 2025-06-21 17:12:47 +02:00
178 lines
9.5 KiB
Python
178 lines
9.5 KiB
Python
# usb_writer_windows.py
|
|
import subprocess
|
|
import os
|
|
import time
|
|
import shutil
|
|
|
|
class USBWriterWindows:
|
|
def __init__(self, device_id: str, opencore_qcow2_path: str, macos_qcow2_path: str, progress_callback=None):
|
|
self.device_id = device_id
|
|
# Construct PhysicalDrive path carefully
|
|
disk_number_str = "".join(filter(str.isdigit, device_id))
|
|
self.physical_drive_path = f"\\\\.\\PhysicalDrive{disk_number_str}"
|
|
self.opencore_qcow2_path = opencore_qcow2_path
|
|
self.macos_qcow2_path = macos_qcow2_path
|
|
self.progress_callback = progress_callback
|
|
|
|
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):
|
|
script_file_path = f"diskpart_script_{os.getpid()}.txt"
|
|
with open(script_file_path, "w") as f:
|
|
f.write(script_content)
|
|
try:
|
|
self._report_progress(f"Running diskpart script...\n{script_content}")
|
|
self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False)
|
|
finally:
|
|
if os.path.exists(script_file_path): os.remove(script_file_path)
|
|
|
|
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): os.remove(f_path)
|
|
for d_path in self.temp_dirs_to_clean:
|
|
if os.path.exists(d_path): shutil.rmtree(d_path, ignore_errors=True)
|
|
|
|
def _find_available_drive_letter(self) -> str | None:
|
|
import string
|
|
# This is a placeholder. Actual psutil or ctypes calls would be more robust.
|
|
# For now, assume 'S' is available if not 'E' through 'Z'.
|
|
return 'S'
|
|
|
|
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()
|
|
os.makedirs(self.temp_efi_extract_dir, exist_ok=True)
|
|
|
|
disk_number = "".join(filter(str.isdigit, self.device_id))
|
|
self._report_progress(f"WARNING: ALL DATA ON DISK {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"Attempting to use letter {self.assigned_efi_letter}: for EFI.")
|
|
|
|
script = f"select disk {disk_number}\nclean\nconvert gpt\n"
|
|
script += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n"
|
|
script += "create partition primary label=macOS_USB\nexit\n"
|
|
self._run_diskpart_script(script)
|
|
time.sleep(5)
|
|
|
|
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])
|
|
|
|
self._report_progress("Extracting EFI files (using 7z if available)...")
|
|
if shutil.which("7z"):
|
|
# Simplified 7z call, assumes EFI folder is at root of first partition image by 7z
|
|
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):
|
|
# Fallback: check if files were extracted to temp_efi_extract_dir directly
|
|
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.")
|
|
|
|
target_efi_on_usb = f"{self.assigned_efi_letter}:\\EFI"
|
|
if not os.path.exists(f"{self.assigned_efi_letter}:\\"):
|
|
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after 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 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"], check=True)
|
|
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])
|
|
|
|
self._report_progress("Windows RAW macOS image writing is a placeholder.")
|
|
self._report_progress(f"RAW image at: {self.macos_raw_path}")
|
|
self._report_progress(f"Target physical drive: {self.physical_drive_path}")
|
|
self._report_progress("User needs to use 'dd for Windows' to write the above raw image to the second partition of the USB drive.")
|
|
# Placeholder for actual dd command, as it's complex and risky to automate fully without specific dd tool knowledge
|
|
# E.g. dd if=self.macos_raw_path of=\\\\.\\PhysicalDriveX --partition 2 bs=4M status=progress (syntax depends on dd variant)
|
|
|
|
self._report_progress("Windows USB writing process (EFI part done, macOS part placeholder) completed.")
|
|
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 - Partial Implementation")
|
|
# Requires Admin privileges
|
|
mock_oc = "mock_oc_win.qcow2"
|
|
mock_mac = "mock_mac_win.qcow2"
|
|
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("Enter target disk ID (e.g., '1' for 'disk 1'). WIPES DISK: ")
|
|
if not disk_id.isdigit(): print("Invalid disk ID."); exit(1)
|
|
actual_disk_id = f"disk {disk_id}" # This is how it's used in the class, but the input is just the number.
|
|
|
|
if input(f"Sure to wipe disk {disk_id}? (yes/NO): ").lower() == 'yes':
|
|
# Pass the disk number string to the constructor, it will form \\.\PhysicalDriveX
|
|
writer = USBWriterWindows(disk_id, 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.")
|