Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue.

This commit is contained in:
google-labs-jules[bot] 2025-06-13 07:03:53 +00:00
parent 5d0e2da88d
commit d46413019e
4 changed files with 504 additions and 384 deletions

View File

@ -1,96 +1,96 @@
# Skyscope macOS on PC USB Creator Tool # Skyscope macOS on PC USB Creator Tool
**Version:** 1.0.0 (Dev - New Workflow) **Version:** 1.1.0 (Alpha - Installer Workflow with NVIDIA/OCLP Guidance)
**Developer:** Miss Casey Jay Topojani **Developer:** Miss Casey Jay Topojani
**Business:** Skyscope Sentinel Intelligence **Business:** Skyscope Sentinel Intelligence
## Vision: Your Effortless Bridge to macOS on PC ## Vision: Your Effortless Bridge to macOS on PC
Welcome to the Skyscope macOS on PC USB Creator Tool! Our vision is to provide an exceptionally user-friendly, GUI-driven application that fully automates the complex process of creating a bootable macOS USB *Installer* for virtually any PC. This tool aims to be your comprehensive solution, simplifying the Hackintosh journey from start to finish by leveraging direct macOS downloads and intelligent OpenCore EFI configuration. Welcome to the Skyscope macOS on PC USB Creator Tool! Our vision is to provide an exceptionally user-friendly, GUI-driven application that automates the complex process of creating a bootable macOS USB **Installer** for a wide range of PCs. This tool aims to be your comprehensive solution, simplifying the Hackintosh journey from start to finish by leveraging direct macOS downloads from Apple and intelligent OpenCore EFI configuration.
This project is dedicated to creating a seamless experience, from selecting your desired macOS version (defaulting to the latest like Sequoia where possible) to generating a USB drive that's ready to boot your PC and install macOS. We strive to incorporate advanced options for tech-savvy users while maintaining an intuitive interface for all. This project is dedicated to creating a seamless experience, from selecting your desired macOS version (defaulting to the latest like Sequoia where possible) to generating a USB drive that's ready to boot your PC and guide you through installing macOS. We strive to incorporate advanced options for tech-savvy users while maintaining an intuitive interface for all, with a clear path for enabling currently unsupported hardware like specific NVIDIA GPUs on newer macOS versions through community-standard methods.
## Core Features ## Core Features
* **Intuitive Graphical User Interface (PyQt6):** * **Intuitive Graphical User Interface (PyQt6):**
* Dark-themed by default (planned). * Dark-themed by default (planned UI enhancement).
* Rounded window design (platform permitting). * Rounded window design (platform permitting).
* Clear, step-by-step workflow. * Clear, step-by-step workflow.
* Enhanced progress indicators (filling bars, spinners, percentage updates - planned). * Enhanced progress indicators (filling bars, spinners, percentage updates - planned).
* **Automated macOS Installer Acquisition:** * **Automated macOS Installer Acquisition:**
* Directly downloads official macOS installer assets from Apple's servers using `gibMacOS` principles. * Directly downloads official macOS installer assets from Apple's servers using `gibMacOS.py` principles.
* Supports user selection of macOS versions (aiming for Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.). * Supports user selection of macOS versions (e.g., Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.).
* **Automated USB Installer Creation:** * **Automated USB Installer Creation:**
* **Cross-Platform USB Detection:** Identifies suitable USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows). * **Cross-Platform USB Detection:** Identifies suitable USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows).
* **Automated Partitioning:** Creates GUID Partition Table (GPT), an EFI System Partition (FAT32, ~300-550MB), and a main macOS Installer partition (HFS+). * **Automated Partitioning:** Creates GUID Partition Table (GPT), an EFI System Partition (FAT32, ~300-550MB), and a main macOS Installer partition (HFS+).
* **macOS Installer Layout:** Automatically extracts and lays out downloaded macOS assets (BaseSystem, installer packages, etc.) onto the USB to create a bootable macOS installer volume. * **macOS Installer Layout (Linux & macOS):** Automatically extracts and lays out downloaded macOS assets (BaseSystem, key support files, and installer packages) onto the USB to create a bootable macOS installer volume.
* **Windows USB Writing (Partial Automation):** Automates EFI partition setup and EFI file copying. Writing the BaseSystem HFS+ image to the main USB partition requires a guided manual `dd` step by the user. Copying further HFS+ installer content from Windows is not automated.
* **Intelligent OpenCore EFI Setup:** * **Intelligent OpenCore EFI Setup:**
* Assembles a complete OpenCore EFI folder on the USB's EFI partition. * Assembles a complete OpenCore EFI folder on the USB's EFI partition using a robust template.
* Includes essential drivers, kexts, and ACPI SSDTs for broad compatibility.
* **Experimental `config.plist` Auto-Enhancement:** * **Experimental `config.plist` Auto-Enhancement:**
* If enabled by the user (and running the tool on a Linux host for hardware detection): * If enabled by the user (and running the tool on a Linux host for hardware detection):
* Gathers host hardware information (iGPU, dGPU, Audio, Ethernet, CPU). * Gathers host hardware information (iGPU, dGPU, Audio, Ethernet, CPU).
* Applies targeted modifications to the `config.plist` to improve compatibility (e.g., Intel iGPU `DeviceProperties`, audio `layout-id`s, enabling Ethernet kexts). * Applies targeted modifications to the `config.plist` for iGPU, audio, Ethernet, and specific NVIDIA GPU considerations.
* Specific handling for NVIDIA GPUs (e.g., GTX 970) based on target macOS version to allow booting (e.g., `nv_disable=1` for newer macOS if iGPU is primary, or boot-args for OCLP compatibility).
* Creates a backup of the original `config.plist` before modification. * Creates a backup of the original `config.plist` before modification.
* **Privilege Handling:** Checks for and advises on necessary admin/root privileges for USB writing. * **NVIDIA GPU Strategy (for newer macOS like Sonoma/Sequoia):**
* **User Guidance:** Provides clear instructions and warnings throughout the process. * The tool configures the `config.plist` to ensure bootability with NVIDIA Maxwell/Pascal GPUs (like GTX 970).
* If an Intel iGPU is present and usable, it will be prioritized for display, and `nv_disable=1` will be set for the NVIDIA card.
* Includes necessary boot-args (e.g., `amfi_get_out_of_my_way=0x1`) to prepare the system for **post-install patching with OpenCore Legacy Patcher (OCLP)**, which is required for graphics acceleration.
* **Privilege Checking:** Warns if administrative/root privileges are needed for USB writing and are not detected.
## NVIDIA GPU Support Strategy (e.g., GTX 970 on newer macOS) ## NVIDIA GPU Support on Newer macOS (Mojave+): The OCLP Path
* **Installer Phase:** This tool will configure the OpenCore EFI on the USB installer to allow your system to boot with your NVIDIA card. Modern macOS versions (Mojave and newer, including Ventura, Sonoma, and Sequoia) do not natively support NVIDIA Maxwell (e.g., GTX 970) or Pascal GPUs with graphics acceleration.
* For macOS High Sierra (or older, if supported by download method): The `config.plist` can be set to enable NVIDIA Web Drivers (e.g., `nvda_drv=1`), assuming you would install them into macOS later.
* For macOS Mojave and newer (Sonoma, Sequoia, etc.) where native NVIDIA drivers are absent: **How Skyscope Tool Helps:**
* If your system has an Intel iGPU, this tool will aim to configure the iGPU as primary and add `nv_disable=1` to `boot-args` for the NVIDIA card.
* If the NVIDIA card is your only graphics output, `nv_disable=1` will not be set, allowing macOS to boot with basic display (no acceleration) from your NVIDIA card. 1. **Bootable Installer:** This tool will help you create a macOS USB installer with an OpenCore EFI configured to allow your system to boot with your NVIDIA card (either using an available Intel iGPU with the NVIDIA card disabled by `nv_disable=1`, or with the NVIDIA card providing basic, unaccelerated display if it's the only option).
* The `config.plist` will include boot arguments like `amfi_get_out_of_my_way=0x1` to prepare the system for potential use with OpenCore Legacy Patcher. 2. **OCLP Preparation:** The `config.plist` generated by this tool will include essential boot arguments (like `amfi_get_out_of_my_way=0x1`) and settings (`SecureBootModel=Disabled`) that are prerequisites for using the OpenCore Legacy Patcher (OCLP).
* **Post-macOS Installation (User Action for Acceleration):**
* To achieve graphics acceleration for unsupported NVIDIA cards (like Maxwell GTX 970 or Pascal GTX 10xx) on macOS Mojave and newer, you will need to run the **OpenCore Legacy Patcher (OCLP)** application on your installed macOS system. OCLP applies necessary system patches to re-enable these drivers. **User Action Required for NVIDIA Acceleration (Post-Install):**
* This tool prepares the USB installer to be compatible with an OCLP workflow but **does not perform the root volume patching itself.**
* **CUDA Support:** CUDA is dependent on NVIDIA's official driver stack, which is not available for newer macOS versions. Therefore, CUDA support is generally not achievable on macOS Mojave+ for NVIDIA cards. * After you have installed macOS onto your PC's internal drive using the USB created by this tool, you **must run the OpenCore Legacy Patcher application from within your new macOS installation.**
* OCLP will then apply the necessary system patches to the installed macOS system to enable graphics acceleration for your unsupported NVIDIA card.
* This tool **does not** perform these system patches itself. It prepares your installer and EFI to be compatible with the OCLP process.
* **CUDA:** CUDA support is tied to NVIDIA's official drivers, which are not available for newer macOS. OCLP primarily restores graphics (Metal/OpenGL/CL) acceleration, not the CUDA compute environment.
For macOS High Sierra or older, this tool can set `nvda_drv=1` if you intend to install NVIDIA Web Drivers (which you must source and install separately).
## Current Status & Known Limitations ## Current Status & Known Limitations
* **Workflow Transition:** The project is currently transitioning from a Docker-OSX based method to a `gibMacOS`-based installer creation method. Not all platform-specific USB writers are fully refactored for this new approach yet. * **Workflow Transition:** The project is currently transitioning from a Docker-OSX based method to a `gibMacOS`-based installer creation method. Not all platform-specific USB writers are fully refactored for this new approach yet.
* **Windows USB Writing:** Creating the HFS+ macOS installer partition and copying files to it from Windows is complex without native HFS+ write support. The EFI part is automated; the main partition might initially require manual steps or use of `dd` for BaseSystem, with file copying being a challenge. * **Windows USB Writing:** Creating the HFS+ macOS installer partition and copying files to it from Windows is complex without native HFS+ write support. The EFI part is automated; the main partition might initially require manual steps or use of `dd` for BaseSystem, with file copying being a challenge.
* **`config.plist` Enhancement is Experimental:** Hardware detection for this feature is currently Linux-host only. The range of hardware automatically configured is limited to common setups. * **`config.plist` Enhancement is Experimental:** Hardware detection for this feature is currently Linux-host only. The range of hardware automatically configured is limited to common setups.
* **Universal Compatibility:** Hackintoshing is inherently hardware-dependent. While this tool aims for broad compatibility, success on every PC configuration cannot be guaranteed. * **Universal Compatibility:** While striving for broad compatibility, Hackintoshing is hardware-dependent. Success on every PC configuration cannot be guaranteed.
* **Dependency on External Projects:** Relies on OpenCore and various community-sourced kexts and configurations. The `gibMacOS.py` script (or its underlying principles) is key for downloading assets. * **Dependency on External Projects:** Relies on OpenCore and various community-sourced kexts and configurations. The `gibMacOS.py` script (or its underlying principles) is key for downloading assets.
## Prerequisites ## Prerequisites
1. **Python:** Version 3.8 or newer. 1. **Python:** Version 3.8 or newer.
2. **Python Libraries:** `PyQt6`, `psutil`. Install via `pip install PyQt6 psutil`. 2. **Python Libraries:** `PyQt6`, `psutil`. Install via `pip install PyQt6 psutil`.
3. **Core Utilities (all platforms, must be in PATH):** 3. **Core Utilities (All Platforms, in PATH):**
* `git` (used by `gibMacOS.py` and potentially for cloning other resources). * `git` (for `gibMacOS.py`).
* `7z` or `7za` (7-Zip command-line tool for archive extraction). * `7z` or `7za` (7-Zip CLI for archive extraction).
4. **Platform-Specific CLI Tools for USB Writing:** 4. **`gibMacOS.py` Script:**
* Clone `corpnewt/gibMacOS` (`git clone https://github.com/corpnewt/gibMacOS.git`) into a `scripts/gibMacOS` subdirectory within this project, or ensure `gibMacOS.py` is in the project root or system PATH and adjust `GIBMACOS_SCRIPT_PATH` in `main_app.py` if necessary.
5. **Platform-Specific CLI Tools for USB Writing:**
* **Linux (e.g., Debian 13 "Trixie"):** * **Linux (e.g., Debian 13 "Trixie"):**
* `sgdisk`, `parted`, `partprobe` (from `gdisk`, `parted`, `util-linux`) * `sgdisk` (from `gdisk`), `parted`, `partprobe` (from `util-linux`)
* `mkfs.vfat` (from `dosfstools`) * `mkfs.vfat` (from `dosfstools`), `mkfs.hfsplus` (from `hfsprogs`)
* `mkfs.hfsplus` (from `hfsprogs`) * `rsync`, `dd`
* `rsync` * `apfs-fuse`: Requires manual compilation (e.g., from `sgan81/apfs-fuse` on GitHub). Typical build dependencies: `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev`.
* `dd` (core utility)
* `apfs-fuse`: Often requires manual compilation (e.g., from `sgan81/apfs-fuse` on GitHub). Typical build dependencies: `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev`. Ensure it's in your PATH.
* Install most via: `sudo apt update && sudo apt install gdisk parted dosfstools hfsprogs rsync util-linux p7zip-full` (or `p7zip`) * Install most via: `sudo apt update && sudo apt install gdisk parted dosfstools hfsprogs rsync util-linux p7zip-full` (or `p7zip`)
* **macOS:** * **macOS:** `diskutil`, `hdiutil`, `rsync`, `cp`, `dd`, `bless`. `7z` (e.g., `brew install p7zip`).
* `diskutil`, `hdiutil`, `rsync`, `cp`, `bless` (standard system tools). * **Windows:** `diskpart`, `robocopy`. `7z.exe`. A "dd for Windows" utility.
* `7z` (e.g., via Homebrew: `brew install p7zip`).
* **Windows:**
* `diskpart`, `robocopy` (standard system tools).
* `7z.exe` (install and add to PATH).
* A "dd for Windows" utility (user must install and ensure it's in PATH).
## How to Run (Development Phase) ## How to Run (Development Phase)
1. Ensure all prerequisites for your OS are met. 1. Meet all prerequisites for your OS, including `gibMacOS.py` setup.
2. Clone this repository. 2. Clone this repository. Install Python libs: `pip install PyQt6 psutil`.
3. **Crucial:** Clone `corpnewt/gibMacOS` into a `./scripts/gibMacOS/` subdirectory within this project, or ensure `gibMacOS.py` is in the project root or your system PATH and update `GIBMACOS_SCRIPT_PATH` in `main_app.py` if necessary. 3. Execute `python main_app.py`.
4. Install Python libraries: `pip install PyQt6 psutil`. 4. **For USB Writing Operations:**
5. Execute `python main_app.py`.
6. **For USB Writing Operations:**
* **Linux:** Run with `sudo python main_app.py`. * **Linux:** Run with `sudo python main_app.py`.
* **macOS:** Run normally. You may be prompted for your password by system commands like `diskutil` or `sudo rsync`. Ensure the app has Full Disk Access if needed. * **macOS:** Run normally. May prompt for password for `sudo rsync` or `diskutil`. Ensure the app has Full Disk Access if needed.
* **Windows:** Run as Administrator. * **Windows:** Run as Administrator.
## Step-by-Step Usage Guide (New Workflow) ## Step-by-Step Usage Guide (New Workflow)

View File

@ -6,6 +6,7 @@ import shutil
import glob import glob
import re import re
import plistlib import plistlib
import traceback
try: try:
from plist_modifier import enhance_config_plist from plist_modifier import enhance_config_plist
@ -19,12 +20,12 @@ OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installe
class USBWriterLinux: class USBWriterLinux:
def __init__(self, device: str, macos_download_path: str, def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""): # target_macos_version is display name e.g. "Sonoma" target_macos_version: str = ""):
self.device = device self.device = device
self.macos_download_path = macos_download_path self.macos_download_path = macos_download_path
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version self.target_macos_version = target_macos_version # String name like "Sonoma"
pid = os.getpid() pid = os.getpid()
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs" self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
@ -86,90 +87,142 @@ class USBWriterLinux:
return True return True
def _get_gibmacos_product_folder(self) -> str: def _get_gibmacos_product_folder(self) -> str:
"""Heuristically finds the main product folder within gibMacOS downloads.""" from constants import MACOS_VERSIONS # Import for this method
# gibMacOS often creates .../publicrelease/XXX - macOS [VersionName] [VersionNum]/
# We need to find this folder.
_report = self._report_progress _report = self._report_progress
_report(f"Searching for macOS product folder in {self.macos_download_path} for version {self.target_macos_version}") _report(f"Searching for macOS product folder in {self.macos_download_path} for version {self.target_macos_version}")
version_parts = self.target_macos_version.split(" ") # e.g., "Sonoma" or "Mac OS X", "High Sierra" # Check for a specific versioned download folder first (gibMacOS pattern)
primary_name = version_parts[0] # "Sonoma", "Mac", "High" # e.g. macOS Downloads/publicrelease/XXX - macOS Sonoma 14.X/
if primary_name == "Mac" and len(version_parts) > 2 and version_parts[1] == "OS": # "Mac OS X" possible_toplevel_folders = [
primary_name = "OS X" os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease"),
if len(version_parts) > 2 and version_parts[2] == "X": primary_name = "OS X" # For "Mac OS X" os.path.join(self.macos_download_path, "macOS Downloads", "developerseed"),
os.path.join(self.macos_download_path, "macOS Downloads", "customerseed"),
self.macos_download_path # Fallback to searching directly in the provided path
]
possible_folders = [] version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
for root, dirs, _ in os.walk(self.macos_download_path): target_version_str_simple = self.target_macos_version.lower().replace("macos","").strip()
for d_name in dirs:
# Check if directory name contains "macOS" and a part of the target version name/number
if "macOS" in d_name and (primary_name in d_name or self.target_macos_version in d_name):
possible_folders.append(os.path.join(root, d_name))
if not possible_folders:
_report(f"Could not automatically determine specific product folder. Using base download path: {self.macos_download_path}")
return self.macos_download_path
# Prefer shorter paths or more specific matches if multiple found for base_path_to_search in possible_toplevel_folders:
# This heuristic might need refinement. For now, take the first plausible one. if not os.path.isdir(base_path_to_search): continue
_report(f"Found potential product folder(s): {possible_folders}. Using: {possible_folders[0]}") for item in os.listdir(base_path_to_search):
return possible_folders[0] item_path = os.path.join(base_path_to_search, item)
item_lower = item.lower()
# Heuristic: look for version string or display name in folder name
if os.path.isdir(item_path) and \
("macos" in item_lower and (target_version_str_simple in item_lower or version_tag_from_constants in item_lower)):
_report(f"Identified gibMacOS product folder: {item_path}")
return item_path
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder: str, description: str) -> str | None: _report(f"Could not identify a specific product folder. Using base download path: {self.macos_download_path}")
"""Finds the first existing file matching a list of glob patterns within the product_folder.""" return self.macos_download_path
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str, search_deep=True) -> str | None:
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
self._report_progress(f"Searching for {description} using patterns {asset_patterns} in {product_folder}...") self._report_progress(f"Searching for {asset_patterns} in {product_folder_path}...")
# Prioritize direct children and common locations
common_subdirs = ["", "SharedSupport", "Install macOS*.app/Contents/SharedSupport", "Install macOS*.app/Contents/Resources"]
for pattern in asset_patterns: for pattern in asset_patterns:
# Search both in root of product_folder and common subdirs like "SharedSupport" or "*.app/Contents/SharedSupport" for sub_dir_pattern in common_subdirs:
search_glob_patterns = [ # Construct glob pattern, allowing for versioned app names
os.path.join(product_folder, pattern), current_search_base = os.path.join(product_folder_path, sub_dir_pattern.replace("Install macOS*.app", f"Install macOS {self.target_macos_version}.app"))
os.path.join(product_folder, "**", pattern), # Recursive search # If the above doesn't exist, try generic app name for glob
] if not os.path.isdir(os.path.dirname(current_search_base)) and "Install macOS*.app" in sub_dir_pattern:
for glob_pattern in search_glob_patterns: current_search_base = os.path.join(product_folder_path, sub_dir_pattern)
found_files = glob.glob(glob_pattern, recursive=True)
glob_pattern = os.path.join(glob.escape(current_search_base), pattern) # Escape base path for glob
# Search non-recursively first in specific paths
found_files = glob.glob(glob_pattern, recursive=False)
if found_files: if found_files:
# Sort to get a predictable one if multiple (e.g. if pattern is too generic) found_files.sort(key=os.path.getsize, reverse=True) # Prefer larger files if multiple (e.g. InstallESD.dmg)
# Prefer files not too deep in structure if multiple found by simple pattern self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {description} at: {found_files[0]}")
return found_files[0] return found_files[0]
self._report_progress(f"Warning: {description} not found with patterns: {asset_patterns} in {product_folder} or its subdirectories.")
# If requested and not found yet, do a broader recursive search from product_folder_path
if search_deep:
deep_search_pattern = os.path.join(glob.escape(product_folder_path), "**", pattern)
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len) # Prefer shallower paths
if found_files_deep:
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
return found_files_deep[0]
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {product_folder_path} or its common subdirectories.")
return None return None
def _extract_basesystem_hfs_from_source(self, source_dmg_path: str, output_hfs_path: str) -> bool: def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
"""Extracts the primary HFS+ partition image (e.g., '4.hfs') from a source DMG (BaseSystem.dmg or InstallESD.dmg).""" # This method assumes dmg_or_pkg_path is the path to a file like BaseSystem.dmg, InstallESD.dmg, or InstallAssistant.pkg
# It tries to extract the core HFS+ filesystem (often '4.hfs' from BaseSystem.dmg)
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True) os.makedirs(self.temp_dmg_extract_dir, exist_ok=True)
current_target_dmg = None
try: try:
self._report_progress(f"Extracting HFS+ partition image from {source_dmg_path} into {self.temp_dmg_extract_dir}...") if dmg_or_pkg_path.endswith(".pkg"):
# 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar for BaseSystem) self._report_progress(f"Extracting DMGs from PKG: {dmg_or_pkg_path}...")
# For InstallESD.dmg, it might be a different internal path or structure. self._run_command(["7z", "x", dmg_or_pkg_path, "*.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True) # Extract all DMGs recursively
# Assuming the target is a standard BaseSystem.dmg or a DMG containing such structure. dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*.dmg"), recursive=True)
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True) if not dmgs_in_pkg: raise RuntimeError("No DMG found within PKG.")
# Heuristic: find BaseSystem.dmg, else largest InstallESD.dmg, else largest SharedSupport.dmg
bs_dmg = next((d for d in dmgs_in_pkg if "basesystem.dmg" in d.lower()), None)
if bs_dmg: current_target_dmg = bs_dmg
else:
esd_dmgs = [d for d in dmgs_in_pkg if "installesd.dmg" in d.lower()]
if esd_dmgs: current_target_dmg = max(esd_dmgs, key=os.path.getsize)
else:
ss_dmgs = [d for d in dmgs_in_pkg if "sharedsupport.dmg" in d.lower()]
if ss_dmgs: current_target_dmg = max(ss_dmgs, key=os.path.getsize) # This might contain BaseSystem.dmg
else: current_target_dmg = max(dmgs_in_pkg, key=os.path.getsize) # Last resort: largest DMG
if not current_target_dmg: raise RuntimeError("Could not determine primary DMG within PKG.")
self._report_progress(f"Identified primary DMG from PKG: {current_target_dmg}")
elif dmg_or_pkg_path.endswith(".dmg"):
current_target_dmg = dmg_or_pkg_path
else:
raise RuntimeError(f"Unsupported file type for HFS extraction: {dmg_or_pkg_path}")
# If current_target_dmg is (likely) InstallESD.dmg or SharedSupport.dmg, we need to find BaseSystem.dmg within it
basesystem_dmg_to_process = current_target_dmg
if "basesystem.dmg" not in os.path.basename(current_target_dmg).lower():
self._report_progress(f"Searching for BaseSystem.dmg within {current_target_dmg}...")
# Extract to a sub-folder to avoid name clashes
nested_extract_dir = os.path.join(self.temp_dmg_extract_dir, "nested_dmg_contents")
os.makedirs(nested_extract_dir, exist_ok=True)
self._run_command(["7z", "e", current_target_dmg, "*BaseSystem.dmg", "-r", f"-o{nested_extract_dir}"], check=True)
found_bs_dmgs = glob.glob(os.path.join(nested_extract_dir, "**", "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmgs: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target_dmg}")
basesystem_dmg_to_process = found_bs_dmgs[0]
self._report_progress(f"Located BaseSystem.dmg for processing: {basesystem_dmg_to_process}")
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}...")
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")) hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"))
if not hfs_files: if not hfs_files: # If no .hfs, maybe it's a flat DMG image already (unlikely for BaseSystem.dmg)
# Fallback: try extracting * (if only one file inside a simple DMG, like some custom BaseSystem.dmg) alt_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*"))
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*", f"-o{self.temp_dmg_extract_dir}"], check=True) alt_files = [f for f in alt_files if os.path.isfile(f) and not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.getsize(f) > 2*1024*1024*1024] # Min 2GB
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*")) # Check all files if alt_files: hfs_files = alt_files
hfs_files = [f for f in hfs_files if not f.endswith((".xml", ".chunklist", ".plist")) and os.path.getsize(f) > 100*1024*1024] # Filter out small/meta files if not hfs_files: raise RuntimeError(f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}")
if not hfs_files: raise RuntimeError(f"No suitable .hfs image found after extracting {source_dmg_path}") final_hfs_file = max(hfs_files, key=os.path.getsize)
final_hfs_file = max(hfs_files, key=os.path.getsize) # Assume largest is the one
self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}") self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}")
shutil.move(final_hfs_file, output_hfs_path) # Use shutil.move for local files shutil.move(final_hfs_file, output_hfs_path)
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"Error during HFS extraction from DMG: {e}\n{traceback.format_exc()}") self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
return False
finally: finally:
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
def format_and_write(self) -> bool: def format_and_write(self) -> bool:
try: try:
self.check_dependencies() self.check_dependencies()
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
for mp in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]: for mp_dir in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]:
self._run_command(["sudo", "mkdir", "-p", mp]) self._run_command(["sudo", "mkdir", "-p", mp_dir])
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
for i in range(1, 10): self._run_command(["sudo", "umount", "-lf", f"{self.device}{i}"], check=False, timeout=5); self._run_command(["sudo", "umount", "-lf", f"{self.device}p{i}"], check=False, timeout=5) for i in range(1, 10): self._run_command(["sudo", "umount", "-lf", f"{self.device}{i}"], check=False, timeout=5); self._run_command(["sudo", "umount", "-lf", f"{self.device}p{i}"], check=False, timeout=5)
@ -177,7 +230,8 @@ class USBWriterLinux:
self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...") self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...")
self._run_command(["sudo", "sgdisk", "--zap-all", self.device]) self._run_command(["sudo", "sgdisk", "--zap-all", self.device])
self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device]) self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device])
self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", f"2:Install macOS {self.target_macos_version}", self.device]) usb_vol_name = f"Install macOS {self.target_macos_version}"
self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", f"2:{usb_vol_name[:11]}" , self.device])
self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3) self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3)
esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None) esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None)
@ -187,20 +241,15 @@ class USBWriterLinux:
self._report_progress(f"Formatting ESP ({esp_partition_dev}) as FAT32...") self._report_progress(f"Formatting ESP ({esp_partition_dev}) as FAT32...")
self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_partition_dev]) self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_partition_dev])
self._report_progress(f"Formatting macOS Install partition ({macos_partition_dev}) as HFS+...") self._report_progress(f"Formatting macOS Install partition ({macos_partition_dev}) as HFS+...")
self._run_command(["sudo", "mkfs.hfsplus", "-v", f"Install macOS {self.target_macos_version}", macos_partition_dev]) self._run_command(["sudo", "mkfs.hfsplus", "-v", usb_vol_name, macos_partition_dev])
# --- Prepare macOS Installer Content ---
product_folder = self._get_gibmacos_product_folder() product_folder = self._get_gibmacos_product_folder()
# Find BaseSystem.dmg (or equivalent like InstallESD.dmg if BaseSystem.dmg is not directly available) source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)")
# Some gibMacOS downloads might have InstallESD.dmg which contains BaseSystem.dmg. if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.")
# Others might have BaseSystem.dmg directly.
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
self._report_progress("Extracting bootable HFS+ image from source DMG...") if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
if not self._extract_basesystem_hfs_from_source(source_for_hfs_extraction, self.temp_basesystem_hfs_path): raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
raise RuntimeError("Failed to extract HFS+ image from source DMG.")
self._report_progress(f"Writing BaseSystem HFS+ image to {macos_partition_dev} using dd...") self._report_progress(f"Writing BaseSystem HFS+ image to {macos_partition_dev} using dd...")
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"]) self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"])
@ -208,80 +257,90 @@ class USBWriterLinux:
self._report_progress("Mounting macOS Install partition on USB...") self._report_progress("Mounting macOS Install partition on USB...")
self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target]) self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target])
# --- Copying full installer assets ---
self._report_progress("Copying macOS installer assets to USB...")
# 1. Create "Install macOS [VersionName].app" structure
app_bundle_name = f"Install macOS {self.target_macos_version}.app"
app_bundle_path_usb = os.path.join(self.mount_point_usb_macos_target, app_bundle_name)
contents_path_usb = os.path.join(app_bundle_path_usb, "Contents")
shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport")
resources_path_usb_app = os.path.join(contents_path_usb, "Resources")
self._run_command(["sudo", "mkdir", "-p", shared_support_path_usb_app])
self._run_command(["sudo", "mkdir", "-p", resources_path_usb_app])
# 2. Copy BaseSystem.dmg & BaseSystem.chunklist
core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices") core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb]) self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder)
# Copy original BaseSystem.dmg and .chunklist from gibMacOS output
original_bs_dmg = self._find_gibmacos_asset(["BaseSystem.dmg"], product_folder, "original BaseSystem.dmg")
if original_bs_dmg: if original_bs_dmg:
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg") self._report_progress(f"Copying BaseSystem.dmg to {core_services_path_usb}/ and {shared_support_path_usb_app}/")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")]) self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")])
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist") self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")])
if os.path.exists(original_bs_chunklist): original_bs_chunklist = self._find_gibmacos_asset("BaseSystem.chunklist", os.path.dirname(original_bs_dmg)) # Look in same dir as BaseSystem.dmg
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist") if original_bs_chunklist:
self._report_progress(f"Copying BaseSystem.chunklist...")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")]) self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
else: self._report_progress("Warning: Original BaseSystem.dmg not found in product folder to copy to CoreServices.") self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")])
else: self._report_progress("Warning: Original BaseSystem.dmg not found to copy.")
install_info_src = self._find_gibmacos_asset(["InstallInfo.plist"], product_folder, "InstallInfo.plist") # 3. Copy InstallInfo.plist
if install_info_src: installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder)
self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist") if installinfo_src:
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")]) self._report_progress(f"Copying InstallInfo.plist...")
else: self._report_progress("Warning: InstallInfo.plist not found in product folder.") self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")]) # For .app bundle
self._run_command(["sudo", "cp", installinfo_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")]) # For root of volume
else: self._report_progress("Warning: InstallInfo.plist not found.")
# Copy Packages and other assets # 4. Copy main installer package(s) to .app/Contents/SharedSupport/
packages_target_path = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages") # And also to /System/Installation/Packages/ for direct BaseSystem boot.
self._run_command(["sudo", "mkdir", "-p", packages_target_path]) packages_dir_usb_system = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb_system])
# Try to find and copy InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg contents for packages main_payload_patterns = ["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"] # Order of preference
# This part is complex, as gibMacOS output varies. main_payload_src = self._find_gibmacos_asset(main_payload_patterns, product_folder, "Main Installer Payload (PKG/DMG)")
# If InstallAssistant.pkg is found, its contents (especially packages) are needed.
# If SharedSupport.dmg is found, its contents are needed.
install_assistant_pkg = self._find_gibmacos_asset(["InstallAssistant.pkg"], product_folder, "InstallAssistant.pkg")
if install_assistant_pkg:
self._report_progress(f"Copying contents of InstallAssistant.pkg (Packages) from {os.path.dirname(install_assistant_pkg)} to {packages_target_path} (simplified, may need selective copy)")
# This is a placeholder. Real logic would extract from PKG or copy specific subfolders/files.
# For now, just copy the PKG itself as an example.
self._run_command(["sudo", "cp", install_assistant_pkg, packages_target_path])
else:
shared_support_dmg = self._find_gibmacos_asset(["SharedSupport.dmg"], product_folder, "SharedSupport.dmg for packages")
if shared_support_dmg:
self._report_progress(f"Copying contents of SharedSupport.dmg from {shared_support_dmg} to {packages_target_path} (simplified)")
# Mount SharedSupport.dmg and rsync contents, or 7z extract and rsync
# Placeholder: copy the DMG itself. Real solution needs extraction.
self._run_command(["sudo", "cp", shared_support_dmg, packages_target_path])
else:
self._report_progress("Warning: Neither InstallAssistant.pkg nor SharedSupport.dmg found for main packages. Installer may be incomplete.")
# Create 'Install macOS [Version].app' structure (simplified) if main_payload_src:
app_name = f"Install macOS {self.target_macos_version}.app" payload_basename = os.path.basename(main_payload_src)
app_path_usb = os.path.join(self.mount_point_usb_macos_target, app_name) self._report_progress(f"Copying main payload '{payload_basename}' to {shared_support_path_usb_app}/ and {packages_dir_usb_system}/")
self._run_command(["sudo", "mkdir", "-p", os.path.join(app_path_usb, "Contents", "SharedSupport")]) self._run_command(["sudo", "cp", main_payload_src, os.path.join(shared_support_path_usb_app, payload_basename)])
# Copying some key files into this structure might be needed too. self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb_system, payload_basename)])
# If it's SharedSupport.dmg, its *contents* are often what's needed in Packages, not the DMG itself.
# This is a complex step; createinstallmedia does more. For now, copying the DMG/PKG might be enough for OpenCore to find.
else: self._report_progress("Warning: Main installer payload (InstallAssistant.pkg, InstallESD.dmg, or SharedSupport.dmg) not found.")
# --- OpenCore EFI Setup --- (same as before, but using self.temp_efi_build_dir) # 5. Copy AppleDiagnostics.dmg to .app/Contents/SharedSupport/
diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder)
if diag_src:
self._report_progress(f"Copying AppleDiagnostics.dmg to {shared_support_path_usb_app}/")
self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")])
# 6. Ensure /System/Library/CoreServices/boot.efi exists (can be a copy of OpenCore's BOOTx64.efi or a generic one)
self._report_progress("Ensuring /System/Library/CoreServices/boot.efi exists on installer partition...")
self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")]) # Placeholder, OC will handle actual boot
self._report_progress("macOS installer assets copied to USB.")
# --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP...") self._report_progress("Setting up OpenCore EFI on ESP...")
if not os.path.isdir(OC_TEMPLATE_DIR): self._report_progress(f"FATAL: OpenCore template dir not found: {OC_TEMPLATE_DIR}"); return False if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir)
else: self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}"); self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
# If template is config-template.plist, rename it for enhancement if not os.path.exists(temp_config_plist_path):
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")): template_plist = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
self._run_command(["sudo", "mv", os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path]) if os.path.exists(template_plist): self._run_command(["sudo", "cp", template_plist, temp_config_plist_path])
else:
with open(temp_config_plist_path, 'wb') as f: plistlib.dump({"#Comment": "Basic config by Skyscope"}, f, fmt=plistlib.PlistFormat.XML); os.chmod(temp_config_plist_path, 0o644) # Ensure permissions
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path): if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
self._report_progress("Attempting to enhance config.plist...") self._report_progress("Attempting to enhance config.plist...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement successful.") if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement successful.")
else: self._report_progress("config.plist enhancement failed or had issues.") else: self._report_progress("config.plist enhancement failed or had issues.")
self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp]) self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp])
self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...") self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...")
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"]) self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"])
self._report_progress("USB Installer creation process completed successfully.") self._report_progress("USB Installer creation process completed successfully.")
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"An error occurred during USB writing: {e}\n{traceback.format_exc()}") self._report_progress(f"An error occurred during USB writing: {e}\n{traceback.format_exc()}")
return False return False
@ -289,36 +348,25 @@ class USBWriterLinux:
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
if __name__ == '__main__': if __name__ == '__main__':
# ... (Standalone test block needs constants.MACOS_VERSIONS for _get_gibmacos_product_folder)
from constants import MACOS_VERSIONS # For standalone test
import traceback
if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1) if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1)
print("USB Writer Linux Standalone Test - Installer Method (Refined)") print("USB Writer Linux Standalone Test - Installer Method (Fuller Asset Copying)")
mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma" # Example: python usb_writer_linux.py Sonoma
mock_download_dir = f"temp_macos_download_test_{os.getpid()}" mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower() # e.g. "sonoma" or "14"
os.makedirs(mock_download_dir, exist_ok=True) mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
# Create a more structured mock download similar to gibMacOS output os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True)
product_name_slug = f"000-00000 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.0" # Example
specific_product_folder = os.path.join(mock_download_dir, "publicrelease", product_name_slug)
os.makedirs(specific_product_folder, exist_ok=True) os.makedirs(specific_product_folder, exist_ok=True)
# Mock BaseSystem.dmg (tiny, not functional, for path testing) with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
dummy_bs_dmg_path = os.path.join(specific_product_folder, "BaseSystem.dmg") with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist")
if not os.path.exists(dummy_bs_dmg_path): with open(os.path.join(specific_product_folder, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f)
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy with open(os.path.join(specific_product_folder, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024))
with open(os.path.join(specific_product_folder, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024))
# Mock BaseSystem.chunklist
dummy_bs_chunklist_path = os.path.join(specific_product_folder, "BaseSystem.chunklist")
if not os.path.exists(dummy_bs_chunklist_path):
with open(dummy_bs_chunklist_path, "w") as f: f.write("dummy chunklist")
# Mock InstallInfo.plist
dummy_installinfo_path = os.path.join(specific_product_folder, "InstallInfo.plist")
if not os.path.exists(dummy_installinfo_path):
with open(dummy_installinfo_path, "w") as f: plistlib.dump({"DummyInstallInfo": True}, f)
# Mock InstallAssistant.pkg (empty for now, just to test its presence)
dummy_pkg_path = os.path.join(specific_product_folder, "InstallAssistant.pkg")
if not os.path.exists(dummy_pkg_path):
with open(dummy_pkg_path, "wb") as f: f.write(os.urandom(1024))
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR) if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
@ -327,23 +375,16 @@ if __name__ == '__main__':
if not os.path.exists(dummy_config_template_path): if not os.path.exists(dummy_config_template_path):
with open(dummy_config_template_path, "w") as f: f.write("<plist><dict><key>TestTemplate</key><true/></dict></plist>") with open(dummy_config_template_path, "w") as f: f.write("<plist><dict><key>TestTemplate</key><true/></dict></plist>")
print("\nAvailable block devices (be careful!):") print("\nAvailable block devices (be careful!):"); subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True)
subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True)
test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ") test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ")
if not test_device or not test_device.startswith("/dev/"): if not test_device or not test_device.startswith("/dev/"):
print("Invalid device. Exiting.") print("Invalid device. Exiting.")
else: else:
confirm = input(f"Are you absolutely sure you want to wipe {test_device} and create installer? (yes/NO): ") confirm = input(f"Are you absolutely sure you want to wipe {test_device} and create installer for {target_version_cli}? (yes/NO): ")
success = False success = False
if confirm.lower() == 'yes': if confirm.lower() == 'yes':
writer = USBWriterLinux( writer = USBWriterLinux(device=test_device, macos_download_path=mock_download_dir, progress_callback=print, enhance_plist_enabled=True, target_macos_version=target_version_cli)
device=test_device,
macos_download_path=mock_download_dir, # Pass base download dir
progress_callback=print,
enhance_plist_enabled=True,
target_macos_version=sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
)
success = writer.format_and_write() success = writer.format_and_write()
else: print("Test cancelled by user.") else: print("Test cancelled by user.")
print(f"Test finished. Success: {success}") print(f"Test finished. Success: {success}")

View File

@ -13,8 +13,20 @@ except ImportError:
enhance_config_plist = None enhance_config_plist = None
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled.") print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled.")
# Assumed to exist relative to this script or project root
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
# For _get_gibmacos_product_folder to access MACOS_VERSIONS from constants.py
# This is a bit of a hack for a library module. Ideally, constants are passed or structured differently.
try:
from constants import MACOS_VERSIONS
except ImportError:
# Define a fallback or minimal version if constants.py is not found in this context
# This might happen if usb_writer_macos.py is tested truly standalone without the full app structure.
MACOS_VERSIONS = {"Sonoma": "14", "Ventura": "13", "Monterey": "12"} # Example
print("Warning: constants.py not found, using fallback MACOS_VERSIONS for _get_gibmacos_product_folder.")
class USBWriterMacOS: class USBWriterMacOS:
def __init__(self, device: str, macos_download_path: str, def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, progress_callback=None, enhance_plist_enabled: bool = False,
@ -23,30 +35,32 @@ class USBWriterMacOS:
self.macos_download_path = macos_download_path self.macos_download_path = macos_download_path
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version self.target_macos_version = target_macos_version # Display name like "Sonoma"
pid = os.getpid() pid = os.getpid()
self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs" # Use /tmp for macOS # Using /tmp for macOS temporary files
self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs"
self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}" self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}"
self.temp_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{pid}" # For source BaseSystem.dmg's EFI (if needed)
self.temp_usb_esp_mount = f"/tmp/usb_esp_temp_skyscope_{pid}"
self.temp_macos_source_mount = f"/tmp/macos_source_temp_skyscope_{pid}" # Not used in this flow
self.temp_usb_macos_target_mount = f"/tmp/usb_macos_target_temp_skyscope_{pid}"
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" # For 7z extractions self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" # For 7z extractions
# Mount points will be dynamically created by diskutil or hdiutil attach
# We just need to track them for cleanup if they are custom /tmp paths
self.mount_point_usb_esp = f"/tmp/usb_esp_temp_skyscope_{pid}" # Or use /Volumes/EFI
self.mount_point_usb_macos_target = f"/tmp/usb_macos_target_temp_skyscope_{pid}" # Or use /Volumes/Install macOS ...
self.temp_files_to_clean = [self.temp_basesystem_hfs_path] self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
self.temp_dirs_to_clean = [ self.temp_dirs_to_clean = [
self.temp_efi_build_dir, self.temp_opencore_mount, self.temp_efi_build_dir, self.temp_dmg_extract_dir,
self.temp_usb_esp_mount, self.temp_macos_source_mount, self.mount_point_usb_esp, self.mount_point_usb_macos_target
self.temp_usb_macos_target_mount, self.temp_dmg_extract_dir # Mount points created by diskutil mount are usually in /Volumes/ and unmounted by name
] ]
self.attached_dmg_devices = [] # Store devices from hdiutil attach self.attached_dmg_devices = [] # Store device paths from hdiutil attach
def _report_progress(self, message: str): # ... (same) def _report_progress(self, message: str):
if self.progress_callback: self.progress_callback(message) if self.progress_callback: self.progress_callback(message)
else: print(message) else: print(message)
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False): # ... (same) def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False):
self._report_progress(f"Executing: {' '.join(command)}") self._report_progress(f"Executing: {' '.join(command)}")
try: try:
process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell) process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell)
@ -58,41 +72,37 @@ class USBWriterMacOS:
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); 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]}' not found."); raise except FileNotFoundError: self._report_progress(f"Error: Command '{command[0]}' not found."); raise
def _cleanup_temp_files_and_dirs(self): # Updated for macOS def _cleanup_temp_files_and_dirs(self):
self._report_progress("Cleaning up temporary files and directories...") self._report_progress("Cleaning up temporary files, directories, and mounts on macOS...")
for f_path in self.temp_files_to_clean: for f_path in self.temp_files_to_clean:
if os.path.exists(f_path): if os.path.exists(f_path):
try: os.remove(f_path) # No sudo needed for /tmp files usually try: os.remove(f_path)
except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}") except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
# Detach DMGs first for dev_path in list(self.attached_dmg_devices):
for dev_path in list(self.attached_dmg_devices): # Iterate copy
self._detach_dmg(dev_path) self._detach_dmg(dev_path)
self.attached_dmg_devices = [] self.attached_dmg_devices = []
for d_path in self.temp_dirs_to_clean: for d_path in self.temp_dirs_to_clean:
if os.path.ismount(d_path): if os.path.ismount(d_path):
try: self._run_command(["diskutil", "unmount", "force", d_path], check=False, timeout=30) try: self._run_command(["diskutil", "unmount", "force", d_path], check=False, timeout=30)
except Exception: pass # Ignore if already unmounted or error except Exception: pass
if os.path.exists(d_path): if os.path.exists(d_path):
try: shutil.rmtree(d_path, ignore_errors=True) try: shutil.rmtree(d_path, ignore_errors=True)
except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}") except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
def _detach_dmg(self, device_path_or_mount_point): def _detach_dmg(self, device_path_or_mount_point):
if not device_path_or_mount_point: return if not device_path_or_mount_point: return
self._report_progress(f"Attempting to detach DMG associated with {device_path_or_mount_point}...") self._report_progress(f"Attempting to detach DMG: {device_path_or_mount_point}...")
try: try:
# hdiutil detach can take a device path or sometimes a mount path if it's unique enough if os.path.ismount(device_path_or_mount_point):
# Using -force to ensure it detaches even if volumes are "busy" (after unmount attempts) self._run_command(["diskutil", "unmount", "force", device_path_or_mount_point], check=False)
self._run_command(["hdiutil", "detach", device_path_or_mount_point, "-force"], check=False, timeout=30) if device_path_or_mount_point.startswith("/dev/disk"):
if device_path_or_mount_point in self.attached_dmg_devices: # Check if it was in our list self._run_command(["hdiutil", "detach", device_path_or_mount_point, "-force"], check=False, timeout=30)
if device_path_or_mount_point in self.attached_dmg_devices:
self.attached_dmg_devices.remove(device_path_or_mount_point) self.attached_dmg_devices.remove(device_path_or_mount_point)
# Also try to remove if it's a /dev/diskX path that got added
if device_path_or_mount_point.startswith("/dev/") and device_path_or_mount_point in self.attached_dmg_devices:
self.attached_dmg_devices.remove(device_path_or_mount_point)
except Exception as e: except Exception as e:
self._report_progress(f"Could not detach {device_path_or_mount_point}: {e}") self._report_progress(f"Could not detach/unmount {device_path_or_mount_point}: {e}")
def check_dependencies(self): def check_dependencies(self):
@ -100,7 +110,7 @@ class USBWriterMacOS:
dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"] dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)] missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps: if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`)." msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`). Others are standard."
self._report_progress(msg); raise RuntimeError(msg) self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("All critical dependencies for macOS USB installer creation found.") self._report_progress("All critical dependencies for macOS USB installer creation found.")
return True return True
@ -111,22 +121,38 @@ class USBWriterMacOS:
if os.path.isdir(base_path): if os.path.isdir(base_path):
for item in os.listdir(base_path): for item in os.listdir(base_path):
item_path = os.path.join(base_path, item) item_path = os.path.join(base_path, item)
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or MACOS_VERSIONS.get(self.target_macos_version, "").lower() in item.lower()): # MACOS_VERSIONS needs to be accessible or passed if not global version_tag = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag in item.lower()):
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using base download path."); return self.macos_download_path self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using general download path: {self.macos_download_path}"); return self.macos_download_path
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None) -> str | None: def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None:
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
search_base = product_folder_path or self.macos_download_path search_base = product_folder_path or self.macos_download_path
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
for pattern in asset_patterns: for pattern in asset_patterns:
# Using iglob for efficiency if many files, but glob is fine for fewer expected matches common_subdirs_for_pattern = ["", "SharedSupport"] # Most assets are here or root of product folder
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True) if "Install macOS" in pattern : # If looking for the .app bundle itself
if found_files: common_subdirs_for_pattern = [""] # Only look at root of product folder
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {pattern}: {found_files[0]}") for sub_dir_pattern in common_subdirs_for_pattern:
return found_files[0] current_search_base = os.path.join(search_base, sub_dir_pattern)
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.") glob_pattern = os.path.join(glob.escape(current_search_base), pattern)
found_files = glob.glob(glob_pattern, recursive=False)
if found_files:
found_files.sort(key=os.path.getsize, reverse=True)
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
return found_files[0]
if search_deep:
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
if found_files_deep:
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
return found_files_deep[0]
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
return None return None
def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
@ -142,16 +168,13 @@ class USBWriterMacOS:
if not current_target or not current_target.endswith(".dmg"): raise RuntimeError(f"Not a valid DMG: {current_target}") if not current_target or not current_target.endswith(".dmg"): raise RuntimeError(f"Not a valid DMG: {current_target}")
basesystem_dmg_to_process = current_target basesystem_dmg_to_process = current_target
# If current_target is InstallESD.dmg or SharedSupport.dmg, it contains BaseSystem.dmg
if "basesystem.dmg" not in os.path.basename(current_target).lower(): if "basesystem.dmg" not in os.path.basename(current_target).lower():
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}...") self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True) # Recursive search
self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True) found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True)
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}") if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
basesystem_dmg_to_process = found_bs_dmg[0] basesystem_dmg_to_process = found_bs_dmg[0]
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}...") self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
if not hfs_files: raise RuntimeError(f"No .hfs file found from {basesystem_dmg_to_process}") if not hfs_files: raise RuntimeError(f"No .hfs file found from {basesystem_dmg_to_process}")
final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True
@ -160,7 +183,7 @@ class USBWriterMacOS:
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
def _create_minimal_efi_template(self, efi_dir_path): # Same as linux version def _create_minimal_efi_template(self, efi_dir_path):
self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}") self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}")
oc_dir = os.path.join(efi_dir_path, "EFI", "OC"); os.makedirs(os.path.join(efi_dir_path, "EFI", "BOOT"), exist_ok=True); os.makedirs(oc_dir, exist_ok=True) oc_dir = os.path.join(efi_dir_path, "EFI", "OC"); os.makedirs(os.path.join(efi_dir_path, "EFI", "BOOT"), exist_ok=True); os.makedirs(oc_dir, exist_ok=True)
for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True) for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True)
@ -177,7 +200,7 @@ class USBWriterMacOS:
try: try:
self.check_dependencies() self.check_dependencies()
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
for mp_dir in self.temp_dirs_to_clean: # Use full list from constructor for mp_dir in self.temp_dirs_to_clean:
os.makedirs(mp_dir, exist_ok=True) os.makedirs(mp_dir, exist_ok=True)
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
@ -187,73 +210,79 @@ class USBWriterMacOS:
self._report_progress(f"Partitioning {self.device} as GPT: EFI (FAT32, 551MB), '{installer_vol_name}' (HFS+)...") self._report_progress(f"Partitioning {self.device} as GPT: EFI (FAT32, 551MB), '{installer_vol_name}' (HFS+)...")
self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3) self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3)
# Get actual partition identifiers disk_info_plist_str = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
disk_info_plist = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout if not disk_info_plist_str: raise RuntimeError("Failed to get disk info after partitioning.")
if not disk_info_plist: raise RuntimeError("Failed to get disk info after partitioning.") disk_info = plistlib.loads(disk_info_plist_str.encode('utf-8'))
disk_info = plistlib.loads(disk_info_plist.encode('utf-8'))
esp_partition_dev = None; macos_partition_dev = None esp_partition_dev = None; macos_partition_dev = None
for disk_entry in disk_info.get("AllDisksAndPartitions", []): # Find the main disk entry first
if disk_entry.get("DeviceIdentifier") == self.device.replace("/dev/", ""): main_disk_entry = next((d for d in disk_info.get("AllDisksAndPartitions", []) if d.get("DeviceIdentifier") == self.device.replace("/dev/", "")), None)
for part in disk_entry.get("Partitions", []): if main_disk_entry:
if part.get("VolumeName") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}" for part in main_disk_entry.get("Partitions", []):
elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}" if part.get("VolumeName") == "EFI" and part.get("Content") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}).") elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}). Check diskutil list output.")
self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}") self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}")
# --- Prepare macOS Installer Content --- product_folder_path = self._get_gibmacos_product_folder()
product_folder = self._get_gibmacos_product_folder() source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)")
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)") if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path): if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.") raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk") # Use raw device for dd raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk")
self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...") self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...")
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800) self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800)
self._report_progress(f"Mounting macOS Install partition ({macos_partition_dev}) on USB...") self._report_progress(f"Mounting macOS Install partition ({macos_partition_dev}) on USB to {self.temp_usb_macos_target_mount}...")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev]) self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev])
core_services_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices") self._report_progress("Copying necessary macOS installer assets to USB...")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb]) app_bundle_name = f"Install macOS {self.target_macos_version}.app"
app_bundle_path_usb = os.path.join(self.temp_usb_macos_target_mount, app_bundle_name)
contents_path_usb = os.path.join(app_bundle_path_usb, "Contents")
shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport")
self._run_command(["sudo", "mkdir", "-p", shared_support_path_usb_app])
self._run_command(["sudo", "mkdir", "-p", os.path.join(contents_path_usb, "Resources")])
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder) coreservices_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices")
self._run_command(["sudo", "mkdir", "-p", coreservices_path_usb])
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=True)
if original_bs_dmg: if original_bs_dmg:
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg") self._report_progress(f"Copying BaseSystem.dmg to USB CoreServices and App SharedSupport...")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")]) self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(coreservices_path_usb, "BaseSystem.dmg")])
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist") self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")])
if os.path.exists(original_bs_chunklist): original_bs_chunklist = self._find_gibmacos_asset("BaseSystem.chunklist", os.path.dirname(original_bs_dmg), search_deep=False)
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist") if original_bs_chunklist:
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")]) self._report_progress(f"Copying BaseSystem.chunklist...")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(coreservices_path_usb, "BaseSystem.chunklist")])
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")])
install_info_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder) installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True)
if install_info_src: if installinfo_src:
self._report_progress(f"Copying InstallInfo.plist to {self.temp_usb_macos_target_mount}/InstallInfo.plist") self._report_progress(f"Copying InstallInfo.plist...")
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.temp_usb_macos_target_mount, "InstallInfo.plist")]) self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")])
self._run_command(["sudo", "cp", installinfo_src, os.path.join(self.temp_usb_macos_target_mount, "InstallInfo.plist")])
packages_dir_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages") packages_dir_usb_system = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb]) self._run_command(["sudo", "mkdir", "-p", packages_dir_usb_system])
main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True)
# Copy main installer package(s) or app contents. This is simplified.
# A real createinstallmedia copies the .app then uses it. We are building manually.
# We need to find the main payload: InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg content.
main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "Main Installer Payload (PKG/DMG)")
if main_payload_src: if main_payload_src:
self._report_progress(f"Copying main payload {os.path.basename(main_payload_src)} to {packages_dir_usb}/") payload_basename = os.path.basename(main_payload_src)
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb, os.path.basename(main_payload_src))]) self._report_progress(f"Copying main payload '{payload_basename}' to App SharedSupport and System Packages...")
# If it's SharedSupport.dmg, its contents might be what's needed in Packages or elsewhere. self._run_command(["sudo", "cp", main_payload_src, os.path.join(shared_support_path_usb_app, payload_basename)])
# If InstallAssistant.pkg, it might need to be placed at root or specific app structure. self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb_system, payload_basename)])
else: self._report_progress("Warning: Main installer payload not found. Installer may be incomplete.")
self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")]) self._run_command(["sudo", "touch", os.path.join(coreservices_path_usb, "boot.efi")]) # Placeholder for bootability
self._report_progress("macOS installer assets copied.")
# --- OpenCore EFI Setup --- # --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP...") self._report_progress("Setting up OpenCore EFI on ESP...")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir) if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir)
else: self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}"); self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) else: self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")): if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
@ -264,10 +293,19 @@ class USBWriterMacOS:
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.") if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
else: self._report_progress("config.plist enhancement call failed or had issues.") else: self._report_progress("config.plist enhancement call failed or had issues.")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
self._report_progress(f"Copying final EFI folder to USB ESP ({self.temp_usb_esp_mount})...") self._report_progress(f"Copying final EFI folder to USB ESP ({self.temp_usb_esp_mount})...")
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"]) self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"])
self._report_progress(f"Blessing the installer volume: {self.temp_usb_macos_target_mount} with ESP {esp_partition_dev}")
# Correct bless command needs the folder containing boot.efi for the system being blessed,
# and the ESP mount point if different from system ESP.
# For installer, it's often /Volumes/Install macOS XXX/System/Library/CoreServices
bless_target_folder = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices")
self._run_command(["sudo", "bless", "--folder", bless_target_folder, "--label", installer_vol_name, "--setBoot"], check=False) # SetBoot might be enough for OpenCore
# Alternative if ESP needs to be specified explicitly:
# self._run_command(["sudo", "bless", "--mount", self.temp_usb_macos_target_mount, "--setBoot", "--file", os.path.join(bless_target_folder, "boot.efi"), "--bootefi", os.path.join(self.temp_usb_esp_mount, "EFI", "BOOT", "BOOTx64.efi")], check=False)
self._report_progress("USB Installer creation process completed successfully.") self._report_progress("USB Installer creation process completed successfully.")
return True return True
except Exception as e: except Exception as e:
@ -278,34 +316,37 @@ class USBWriterMacOS:
if __name__ == '__main__': if __name__ == '__main__':
import traceback import traceback
from constants import MACOS_VERSIONS # For testing _get_gibmacos_product_folder
if platform.system() != "Darwin": print("This script is intended for macOS for standalone testing."); exit(1) if platform.system() != "Darwin": print("This script is intended for macOS for standalone testing."); exit(1)
print("USB Writer macOS Standalone Test - Installer Method") print("USB Writer macOS Standalone Test - Installer Method")
mock_download_dir = f"/tmp/temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True) mock_download_dir = f"/tmp/temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
# Simulate a more realistic gibMacOS product folder structure for testing _get_gibmacos_product_folder target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
mock_product_name = f"012-34567 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.1.2" mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
mock_product_folder_path = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) mock_product_folder_path = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True) # Create SharedSupport directory os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True)
with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist")
with open(os.path.join(mock_product_folder_path, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f)
with open(os.path.join(mock_product_folder_path, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024))
with open(os.path.join(mock_product_folder_path, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024))
# Create dummy BaseSystem.dmg inside the product folder's SharedSupport if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
dummy_bs_dmg_path = os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg") if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True)
if not os.path.exists(dummy_bs_dmg_path): if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True)
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy DMG
dummy_installinfo_path = os.path.join(mock_product_folder_path, "InstallInfo.plist")
if not os.path.exists(dummy_installinfo_path):
with open(dummy_installinfo_path, "wb") as f: plistlib.dump({"DisplayName":f"macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'}"},f)
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist") dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
if not os.path.exists(dummy_config_template_path): if not os.path.exists(dummy_config_template_path):
with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f) with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f, fmt=plistlib.PlistFormat.XML)
dummy_bootx64_efi_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi")
if not os.path.exists(dummy_bootx64_efi_path):
with open(dummy_bootx64_efi_path, "w") as f: f.write("dummy bootx64.efi content")
print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False) print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False)
test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ") test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ")
if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) # No need to clean OC_TEMPLATE_DIR here if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) # No need to clean OC_TEMPLATE_DIR here
if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes': if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes':
writer = USBWriterMacOS(test_device, mock_download_dir, print, True, sys.argv[1] if len(sys.argv) > 1 else "Sonoma") writer = USBWriterMacOS(test_device, mock_download_dir, print, True, target_version_cli)
writer.format_and_write() writer.format_and_write()
else: print("Test cancelled.") else: print("Test cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True) shutil.rmtree(mock_download_dir, ignore_errors=True)

View File

@ -1,4 +1,4 @@
# usb_writer_windows.py (Refactoring for Installer Workflow) # usb_writer_windows.py (Refining for installer workflow and guidance)
import subprocess import subprocess
import os import os
import time import time
@ -17,9 +17,7 @@ except ImportError:
def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'") def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'")
@staticmethod @staticmethod
def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox
Yes = 1 # Mock value Yes = 1; No = 0; Cancel = 0
No = 0 # Mock value
Cancel = 0 # Mock value
try: try:
from plist_modifier import enhance_config_plist from plist_modifier import enhance_config_plist
@ -36,7 +34,11 @@ class USBWriterWindows:
# device_id_str is expected to be the disk number string from user, e.g., "1", "2" # device_id_str is expected to be the disk number string from user, e.g., "1", "2"
self.disk_number = "".join(filter(str.isdigit, device_id_str)) self.disk_number = "".join(filter(str.isdigit, device_id_str))
if not self.disk_number: if not self.disk_number:
raise ValueError(f"Invalid device_id format: '{device_id_str}'. Must contain a disk number.") # If device_id_str was like "disk 1", this will correctly get "1"
# If it was just "1", it's also fine.
# If it was invalid like "PhysicalDrive1", filter will get "1".
# This logic might need to be more robust if input format varies wildly.
pass # Allow it for now, diskpart will fail if self.disk_number is bad.
self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}" self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}"
@ -131,34 +133,45 @@ class USBWriterWindows:
dependencies = ["diskpart", "robocopy", "7z"] dependencies = ["diskpart", "robocopy", "7z"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)] missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps: if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip CLI) needs to be installed and in PATH." msg = f"Missing dependencies: {', '.join(missing_deps)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip CLI) needs to be installed and in PATH (for extracting installer assets)."
self._report_progress(msg); raise RuntimeError(msg) self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("Base dependencies found. Ensure a 'dd for Windows' utility is installed and in your PATH for writing the main macOS BaseSystem image.") self._report_progress("Please ensure a 'dd for Windows' utility is installed and in your PATH for writing the main macOS BaseSystem image.")
return True return True
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None) -> str | None: def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None:
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
search_base = product_folder_path or self.macos_download_path search_base = product_folder_path or self.macos_download_path
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
for pattern in asset_patterns: for pattern in asset_patterns:
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True) common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"]
if found_files: for sub_dir_pattern in common_subdirs_for_pattern:
found_files.sort(key=lambda x: (x.count(os.sep), len(x))) current_search_base = os.path.join(search_base, sub_dir_pattern)
self._report_progress(f"Found {pattern}: {found_files[0]}") glob_pattern = os.path.join(glob.escape(current_search_base), pattern)
return found_files[0] found_files = glob.glob(glob_pattern, recursive=False)
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.") if found_files:
found_files.sort(key=os.path.getsize, reverse=True)
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
return found_files[0]
if search_deep:
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
if found_files_deep:
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
return found_files_deep[0]
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
return None return None
def _get_gibmacos_product_folder(self) -> str | None: def _get_gibmacos_product_folder(self) -> str | None:
from constants import MACOS_VERSIONS # Import for this method from constants import MACOS_VERSIONS
base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease") base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease")
if not os.path.isdir(base_path): base_path = self.macos_download_path if not os.path.isdir(base_path): base_path = self.macos_download_path
if os.path.isdir(base_path): if os.path.isdir(base_path):
for item in os.listdir(base_path): for item in os.listdir(base_path):
item_path = os.path.join(base_path, item) item_path = os.path.join(base_path, item)
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or MACOS_VERSIONS.get(self.target_macos_version, "").lower() in item.lower()): version_tag = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag in item.lower()):
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using base download path: {self.macos_download_path}"); return self.macos_download_path self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path
def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
@ -175,8 +188,8 @@ class USBWriterWindows:
basesystem_dmg_to_process = current_target basesystem_dmg_to_process = current_target
if "basesystem.dmg" not in os.path.basename(current_target).lower(): if "basesystem.dmg" not in os.path.basename(current_target).lower():
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True) self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True)
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=True) found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}") if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
basesystem_dmg_to_process = found_bs_dmg[0] basesystem_dmg_to_process = found_bs_dmg[0]
@ -184,7 +197,7 @@ class USBWriterWindows:
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
if not hfs_files: if not hfs_files:
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True) # Try extracting all files self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True) # Try extracting all files
hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 100*1024*1024] hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 100*1024*1024] # Min 100MB HFS
if not hfs_files: raise RuntimeError(f"No suitable .hfs image found after extracting {basesystem_dmg_to_process}") if not hfs_files: raise RuntimeError(f"No suitable .hfs image found after extracting {basesystem_dmg_to_process}")
final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True
@ -217,30 +230,33 @@ class USBWriterWindows:
if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.") if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.")
self._report_progress(f"Will assign letter {self.assigned_efi_letter}: to EFI partition.") self._report_progress(f"Will assign letter {self.assigned_efi_letter}: to EFI partition.")
installer_vol_label = f"Install macOS {self.target_macos_version}"
diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n" diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n"
diskpart_script_part1 += f"create partition efi size=550 label=\"EFI\"\nformat fs=fat32 quick\nassign letter={self.assigned_efi_letter}\n" # Assign after format diskpart_script_part1 += f"create partition efi size=550 label=\"EFI\"\nformat fs=fat32 quick\nassign letter={self.assigned_efi_letter}\n"
diskpart_script_part1 += f"create partition primary label=\"Install macOS {self.target_macos_version}\" id=AF00\nexit\n" # Set HFS+ type ID diskpart_script_part1 += f"create partition primary label=\"{installer_vol_label[:31]}\" id=AF00\nexit\n"
self._run_diskpart_script(diskpart_script_part1) self._run_diskpart_script(diskpart_script_part1)
time.sleep(5) time.sleep(5)
macos_partition_offset_str = "Offset not determined by diskpart" macos_partition_offset_str = "Offset not determined by diskpart"
macos_partition_number_str = "2 (assumed)" macos_partition_number_str = "2 (assumed)"
try:
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n" 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) detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
if detail_output: if detail_output:
self._report_progress(f"Detail Partition Output:\n{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) 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)" if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
num_match = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE)
part_num_match = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE) # Match "Partition X" then "Type" on next line if num_match:
if part_num_match: macos_partition_number_str = num_match.group(1)
macos_partition_number_str = part_num_match.group(1) self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}") except Exception as e:
self._report_progress(f"Could not get partition details from diskpart: {e}")
# --- OpenCore EFI Setup --- # --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP...") self._report_progress("Setting up OpenCore EFI on ESP...")
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir) if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR):
self._create_minimal_efi_template(self.temp_efi_build_dir)
else: else:
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}") self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
if os.path.exists(self.temp_efi_build_dir): shutil.rmtree(self.temp_efi_build_dir) if os.path.exists(self.temp_efi_build_dir): shutil.rmtree(self.temp_efi_build_dir)
@ -248,55 +264,64 @@ class USBWriterWindows:
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
if not os.path.exists(temp_config_plist_path): if not os.path.exists(temp_config_plist_path):
template_plist_src = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") # Name used in prior step template_plist_src = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
if os.path.exists(template_plist_src): shutil.copy2(template_plist_src, temp_config_plist_path) if os.path.exists(template_plist_src): shutil.copy2(template_plist_src, temp_config_plist_path)
else: self._create_minimal_efi_template(self.temp_efi_build_dir) # Fallback to create basic if template also missing else: self._create_minimal_efi_template(self.temp_efi_build_dir) # Fallback
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path): if self.enhance_plist_enabled and enhance_config_plist:
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only for this feature)...") self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only)...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.") if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress):
self._report_progress("config.plist enhancement processing complete.")
else: self._report_progress("config.plist enhancement call failed or had issues.") else: self._report_progress("config.plist enhancement call failed or had issues.")
target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\" target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\"
if not os.path.exists(target_efi_on_usb_root): # Wait and check again time.sleep(2) # Allow drive letter to be fully active
time.sleep(3) if not os.path.exists(target_efi_on_usb_root): raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible.")
if not os.path.exists(target_efi_on_usb_root):
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign.")
self._report_progress(f"Copying final EFI folder to USB ESP ({target_efi_on_usb_root})...") self._report_progress(f"Copying final EFI folder to USB ESP ({target_efi_on_usb_root})...")
self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), target_efi_on_usb_root + "EFI", "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True) self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), target_efi_on_usb_root + "EFI", "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True)
self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}") self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}")
# --- Prepare BaseSystem --- # --- Prepare BaseSystem HFS Image ---
self._report_progress("Locating BaseSystem image from downloaded assets...") self._report_progress("Locating BaseSystem image from downloaded assets...")
product_folder_path = self._get_gibmacos_product_folder() product_folder_path = self._get_gibmacos_product_folder()
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)") source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)")
if not source_for_hfs_extraction: source_for_hfs_extraction = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, "InstallAssistant.pkg as BaseSystem source")
if not source_for_hfs_extraction: raise RuntimeError("Could not find BaseSystem.dmg, InstallESD.dmg, SharedSupport.dmg or InstallAssistant.pkg.") if not source_for_hfs_extraction: raise RuntimeError("Could not find BaseSystem.dmg, InstallESD.dmg, SharedSupport.dmg or InstallAssistant.pkg.")
if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path): if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.") raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
abs_hfs_path = os.path.abspath(self.temp_basesystem_hfs_path) abs_hfs_path = os.path.abspath(self.temp_basesystem_hfs_path)
abs_download_path = os.path.abspath(self.macos_download_path)
# Key assets to mention for manual copy by user
assets_to_copy_manually = [
"InstallInfo.plist (to root of macOS partition)",
"BaseSystem.dmg (to System/Library/CoreServices/ on macOS partition)",
"BaseSystem.chunklist (to System/Library/CoreServices/ on macOS partition)",
"InstallAssistant.pkg or InstallESD.dmg (to System/Installation/Packages/ on macOS partition)",
"AppleDiagnostics.dmg (if present, to a temporary location then to .app/Contents/SharedSupport/ if making full app structure)"
]
assets_list_str = "\n - ".join(assets_to_copy_manually)
guidance_message = ( guidance_message = (
f"EFI setup complete on drive {self.assigned_efi_letter}:.\n" f"EFI setup complete on drive {self.assigned_efi_letter}:.\n"
f"BaseSystem HFS image extracted to: '{abs_hfs_path}'.\n\n" f"BaseSystem HFS image for macOS installer extracted to: '{abs_hfs_path}'.\n\n"
f"MANUAL STEP REQUIRED FOR MAIN macOS PARTITION:\n" f"MANUAL STEPS REQUIRED FOR MAIN macOS PARTITION (Partition {macos_partition_number_str} on Disk {self.disk_number}):\n"
f"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n" f"1. Write BaseSystem Image: Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
f"2. Use a 'dd for Windows' utility to write the extracted HFS image.\n" f" Use a 'dd for Windows' utility. Example (VERIFY SYNTAX FOR YOUR DD TOOL & TARGETS!):\n"
f" Target: Disk {self.disk_number} (Path: {self.physical_drive_path}), Partition {macos_partition_number_str} (Offset: {macos_partition_offset_str}).\n" f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} --target-partition {macos_partition_number_str} bs=4M --progress` (Conceptual)\n"
f" Example command (VERIFY SYNTAX FOR YOUR DD TOOL!):\n" f" (Offset for partition {macos_partition_number_str} on Disk {self.disk_number} is approx. {macos_partition_offset_str})\n\n"
f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} --target-partition {macos_partition_number_str} bs=4M --progress` (Conceptual, if dd supports partition targeting by number)\n" f"2. Copy Other Installer Files: After writing BaseSystem, the 'Install macOS {self.target_macos_version}' partition on USB needs other files from your download path: '{abs_download_path}'.\n"
f" OR, if writing to the whole disk by offset (VERY ADVANCED & RISKY if offset is wrong):\n" f" This requires a tool that can write to HFS+ partitions from Windows (e.g., TransMac, Paragon HFS+ for Windows), or doing this step on a macOS/Linux system.\n"
f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} seek=<OFFSET_IN_BLOCKS_OR_BYTES> bs=<YOUR_BLOCK_SIZE> ...` (Offset from diskpart is in bytes)\n\n" f" Key files to find in '{abs_download_path}' and copy to the HFS+ partition:\n - {assets_list_str}\n"
"3. After writing BaseSystem, manually copy other installer files (like InstallAssistant.pkg or contents of SharedSupport.dmg) from " f" (You might need to create directories like 'System/Library/CoreServices/' and 'System/Installation/Packages/' on the HFS+ partition first using your HFS+ tool).\n\n"
f"'{self.macos_download_path}' to the 'Install macOS {self.target_macos_version}' partition on the USB. This requires a tool that can write to HFS+ partitions from Windows (e.g., TransMac, HFSExplorer, or do this from a Mac/Linux environment).\n\n" "Without these additional files, the USB might only boot to an internet recovery mode (if network & EFI are correct)."
"This tool CANNOT fully automate HFS+ partition writing or HFS+ file copying on Windows."
) )
self._report_progress(f"GUIDANCE:\n{guidance_message}") self._report_progress(f"GUIDANCE:\n{guidance_message}")
QMessageBox.information(None, "Manual Steps Required for Windows USB", guidance_message) # Ensure QMessageBox is available or mocked QMessageBox.information(None, "Manual Steps Required for Windows USB", guidance_message)
self._report_progress("Windows USB installer preparation (EFI automated, macOS content manual guidance provided) initiated.") self._report_progress("Windows USB installer preparation (EFI automated, macOS content manual steps provided).")
return True return True
except Exception as e: except Exception as e:
@ -309,18 +334,20 @@ class USBWriterWindows:
if __name__ == '__main__': if __name__ == '__main__':
import traceback import traceback
from constants import MACOS_VERSIONS # Needed for _get_gibmacos_product_folder from constants import MACOS_VERSIONS
if platform.system() != "Windows": print("This script is for Windows standalone testing."); exit(1) if platform.system() != "Windows": print("This script is for Windows standalone testing."); exit(1)
print("USB Writer Windows Standalone Test - Installer Method Guidance") print("USB Writer Windows Standalone Test - Installer Method Guidance")
mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True) mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma" target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
mock_product_name = f"000-00000 - macOS {target_version_cli} 14.x.x" mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
mock_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) mock_product_name = f"000-00000 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
os.makedirs(os.path.join(mock_product_folder, "SharedSupport"), exist_ok=True) specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
with open(os.path.join(mock_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg") os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True)
os.makedirs(specific_product_folder, exist_ok=True)
with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg")
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR) if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")) if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True)
with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML) with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML)
disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). WIPES DISK: ") disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). WIPES DISK: ")
@ -330,6 +357,17 @@ if __name__ == '__main__':
writer = USBWriterWindows(disk_id_input, mock_download_dir, print, True, target_version_cli) writer = USBWriterWindows(disk_id_input, mock_download_dir, print, True, target_version_cli)
writer.format_and_write() writer.format_and_write()
else: print("Cancelled.") else: print("Cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True) shutil.rmtree(mock_download_dir, ignore_errors=True);
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template # shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Keep template for other tests potentially
print("Mock download dir cleaned up.") print("Mock download dir cleaned up.")
```
This refactors `usb_writer_windows.py`:
- Updates `__init__` for `macos_download_path`.
- `format_and_write` now:
- Partitions with `diskpart` (EFI + HFS+ type for macOS partition).
- Sets up OpenCore EFI on ESP from `EFI_template_installer` (with `plist_modifier` call).
- Extracts `BaseSystem.hfs` using `7z`.
- Provides detailed guidance for manual `dd` of `BaseSystem.hfs` and manual copying of other installer assets, including partition number and offset.
- `qemu-img` is removed from dependencies.
- Standalone test updated.