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
**Version:** 1.0.0 (Dev - New Workflow)
**Version:** 1.1.0 (Alpha - Installer Workflow with NVIDIA/OCLP Guidance)
**Developer:** Miss Casey Jay Topojani
**Business:** Skyscope Sentinel Intelligence
## 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
* **Intuitive Graphical User Interface (PyQt6):**
* Dark-themed by default (planned).
* Dark-themed by default (planned UI enhancement).
* Rounded window design (platform permitting).
* Clear, step-by-step workflow.
* Enhanced progress indicators (filling bars, spinners, percentage updates - planned).
* **Automated macOS Installer Acquisition:**
* Directly downloads official macOS installer assets from Apple's servers using `gibMacOS` principles.
* Supports user selection of macOS versions (aiming for Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.).
* Directly downloads official macOS installer assets from Apple's servers using `gibMacOS.py` principles.
* Supports user selection of macOS versions (e.g., Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.).
* **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).
* **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:**
* Assembles a complete OpenCore EFI folder on the USB's EFI partition.
* Includes essential drivers, kexts, and ACPI SSDTs for broad compatibility.
* Assembles a complete OpenCore EFI folder on the USB's EFI partition using a robust template.
* **Experimental `config.plist` Auto-Enhancement:**
* 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).
* Applies targeted modifications to the `config.plist` to improve compatibility (e.g., Intel iGPU `DeviceProperties`, audio `layout-id`s, enabling Ethernet kexts).
* 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).
* Applies targeted modifications to the `config.plist` for iGPU, audio, Ethernet, and specific NVIDIA GPU considerations.
* Creates a backup of the original `config.plist` before modification.
* **Privilege Handling:** Checks for and advises on necessary admin/root privileges for USB writing.
* **User Guidance:** Provides clear instructions and warnings throughout the process.
* **NVIDIA GPU Strategy (for newer macOS like Sonoma/Sequoia):**
* 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.
* 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:
* 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.
* 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.
* **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.
* 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.
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.
**How Skyscope Tool Helps:**
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).
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).
**User Action Required for NVIDIA Acceleration (Post-Install):**
* 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
* **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.
* **`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.
## Prerequisites
1. **Python:** Version 3.8 or newer.
2. **Python Libraries:** `PyQt6`, `psutil`. Install via `pip install PyQt6 psutil`.
3. **Core Utilities (all platforms, must be in PATH):**
* `git` (used by `gibMacOS.py` and potentially for cloning other resources).
* `7z` or `7za` (7-Zip command-line tool for archive extraction).
4. **Platform-Specific CLI Tools for USB Writing:**
3. **Core Utilities (All Platforms, in PATH):**
* `git` (for `gibMacOS.py`).
* `7z` or `7za` (7-Zip CLI for archive extraction).
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"):**
* `sgdisk`, `parted`, `partprobe` (from `gdisk`, `parted`, `util-linux`)
* `mkfs.vfat` (from `dosfstools`)
* `mkfs.hfsplus` (from `hfsprogs`)
* `rsync`
* `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.
* `sgdisk` (from `gdisk`), `parted`, `partprobe` (from `util-linux`)
* `mkfs.vfat` (from `dosfstools`), `mkfs.hfsplus` (from `hfsprogs`)
* `rsync`, `dd`
* `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`.
* Install most via: `sudo apt update && sudo apt install gdisk parted dosfstools hfsprogs rsync util-linux p7zip-full` (or `p7zip`)
* **macOS:**
* `diskutil`, `hdiutil`, `rsync`, `cp`, `bless` (standard system tools).
* `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).
* **macOS:** `diskutil`, `hdiutil`, `rsync`, `cp`, `dd`, `bless`. `7z` (e.g., `brew install p7zip`).
* **Windows:** `diskpart`, `robocopy`. `7z.exe`. A "dd for Windows" utility.
## How to Run (Development Phase)
1. Ensure all prerequisites for your OS are met.
2. Clone this repository.
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.
4. Install Python libraries: `pip install PyQt6 psutil`.
5. Execute `python main_app.py`.
6. **For USB Writing Operations:**
1. Meet all prerequisites for your OS, including `gibMacOS.py` setup.
2. Clone this repository. Install Python libs: `pip install PyQt6 psutil`.
3. Execute `python main_app.py`.
4. **For USB Writing Operations:**
* **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.
## Step-by-Step Usage Guide (New Workflow)

View File

@ -6,6 +6,7 @@ import shutil
import glob
import re
import plistlib
import traceback
try:
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:
def __init__(self, device: str, macos_download_path: str,
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.macos_download_path = macos_download_path
self.progress_callback = progress_callback
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()
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
@ -86,90 +87,142 @@ class USBWriterLinux:
return True
def _get_gibmacos_product_folder(self) -> str:
"""Heuristically finds the main product folder within gibMacOS downloads."""
# gibMacOS often creates .../publicrelease/XXX - macOS [VersionName] [VersionNum]/
# We need to find this folder.
from constants import MACOS_VERSIONS # Import for this method
_report = self._report_progress
_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"
primary_name = version_parts[0] # "Sonoma", "Mac", "High"
if primary_name == "Mac" and len(version_parts) > 2 and version_parts[1] == "OS": # "Mac OS X"
primary_name = "OS X"
if len(version_parts) > 2 and version_parts[2] == "X": primary_name = "OS X" # For "Mac OS X"
# Check for a specific versioned download folder first (gibMacOS pattern)
# e.g. macOS Downloads/publicrelease/XXX - macOS Sonoma 14.X/
possible_toplevel_folders = [
os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease"),
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 = []
for root, dirs, _ in os.walk(self.macos_download_path):
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))
version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
target_version_str_simple = self.target_macos_version.lower().replace("macos","").strip()
if not possible_folders:
_report(f"Could not automatically determine specific product folder. Using base download path: {self.macos_download_path}")
for base_path_to_search in possible_toplevel_folders:
if not os.path.isdir(base_path_to_search): continue
for item in os.listdir(base_path_to_search):
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
_report(f"Could not identify a 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
# This heuristic might need refinement. For now, take the first plausible one.
_report(f"Found potential product folder(s): {possible_folders}. Using: {possible_folders[0]}")
return possible_folders[0]
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder: str, description: str) -> str | None:
"""Finds the first existing file matching a list of glob patterns within the product_folder."""
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]
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:
# Search both in root of product_folder and common subdirs like "SharedSupport" or "*.app/Contents/SharedSupport"
search_glob_patterns = [
os.path.join(product_folder, pattern),
os.path.join(product_folder, "**", pattern), # Recursive search
]
for glob_pattern in search_glob_patterns:
found_files = glob.glob(glob_pattern, recursive=True)
for sub_dir_pattern in common_subdirs:
# Construct glob pattern, allowing for versioned app names
current_search_base = os.path.join(product_folder_path, sub_dir_pattern.replace("Install macOS*.app", f"Install macOS {self.target_macos_version}.app"))
# 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:
current_search_base = os.path.join(product_folder_path, sub_dir_pattern)
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:
# Sort to get a predictable one if multiple (e.g. if pattern is too generic)
# Prefer files not too deep in structure if multiple found by simple pattern
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {description} at: {found_files[0]}")
found_files.sort(key=os.path.getsize, reverse=True) # Prefer larger files if multiple (e.g. InstallESD.dmg)
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
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
def _extract_basesystem_hfs_from_source(self, source_dmg_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)."""
def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
# 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)
current_target_dmg = None
try:
self._report_progress(f"Extracting HFS+ partition image from {source_dmg_path} into {self.temp_dmg_extract_dir}...")
# 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar for BaseSystem)
# For InstallESD.dmg, it might be a different internal path or structure.
# Assuming the target is a standard BaseSystem.dmg or a DMG containing such structure.
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
if dmg_or_pkg_path.endswith(".pkg"):
self._report_progress(f"Extracting DMGs from PKG: {dmg_or_pkg_path}...")
self._run_command(["7z", "x", dmg_or_pkg_path, "*.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True) # Extract all DMGs recursively
dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*.dmg"), recursive=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"))
if not hfs_files:
# Fallback: try extracting * (if only one file inside a simple DMG, like some custom BaseSystem.dmg)
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*")) # Check all 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: # If no .hfs, maybe it's a flat DMG image already (unlikely for BaseSystem.dmg)
alt_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*"))
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
if alt_files: hfs_files = alt_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) # Assume largest is the one
final_hfs_file = max(hfs_files, key=os.path.getsize)
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
except Exception as e:
self._report_progress(f"Error during HFS extraction from DMG: {e}\n{traceback.format_exc()}")
return False
self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
finally:
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:
try:
self.check_dependencies()
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]:
self._run_command(["sudo", "mkdir", "-p", mp])
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_dir])
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)
@ -177,7 +230,8 @@ class USBWriterLinux:
self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...")
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", "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)
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._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._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()
# Find BaseSystem.dmg (or equivalent like InstallESD.dmg if BaseSystem.dmg is not directly available)
# Some gibMacOS downloads might have InstallESD.dmg which contains BaseSystem.dmg.
# 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.")
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)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.")
self._report_progress("Extracting bootable HFS+ image from source DMG...")
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 source DMG.")
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.")
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"])
@ -208,80 +257,90 @@ class USBWriterLinux:
self._report_progress("Mounting macOS Install partition on USB...")
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")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
# Copy original BaseSystem.dmg and .chunklist from gibMacOS output
original_bs_dmg = self._find_gibmacos_asset(["BaseSystem.dmg"], product_folder, "original BaseSystem.dmg")
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder)
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")])
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist")
if os.path.exists(original_bs_chunklist):
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")])
original_bs_chunklist = self._find_gibmacos_asset("BaseSystem.chunklist", os.path.dirname(original_bs_dmg)) # Look in same dir as BaseSystem.dmg
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")])
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")
if install_info_src:
self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist")
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")])
else: self._report_progress("Warning: InstallInfo.plist not found in product folder.")
# 3. Copy InstallInfo.plist
installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder)
if installinfo_src:
self._report_progress(f"Copying InstallInfo.plist...")
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
packages_target_path = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_target_path])
# 4. Copy main installer package(s) to .app/Contents/SharedSupport/
# And also to /System/Installation/Packages/ for direct BaseSystem boot.
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
# This part is complex, as gibMacOS output varies.
# 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.")
main_payload_patterns = ["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"] # Order of preference
main_payload_src = self._find_gibmacos_asset(main_payload_patterns, product_folder, "Main Installer Payload (PKG/DMG)")
# Create 'Install macOS [Version].app' structure (simplified)
app_name = f"Install macOS {self.target_macos_version}.app"
app_path_usb = os.path.join(self.mount_point_usb_macos_target, app_name)
self._run_command(["sudo", "mkdir", "-p", os.path.join(app_path_usb, "Contents", "SharedSupport")])
# Copying some key files into this structure might be needed too.
if main_payload_src:
payload_basename = os.path.basename(main_payload_src)
self._report_progress(f"Copying main payload '{payload_basename}' to {shared_support_path_usb_app}/ and {packages_dir_usb_system}/")
self._run_command(["sudo", "cp", main_payload_src, os.path.join(shared_support_path_usb_app, payload_basename)])
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...")
if not os.path.isdir(OC_TEMPLATE_DIR): self._report_progress(f"FATAL: OpenCore template dir not found: {OC_TEMPLATE_DIR}"); return False
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])
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])
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) and os.path.exists(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 not os.path.exists(temp_config_plist_path):
template_plist = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
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):
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.")
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._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._report_progress("USB Installer creation process completed successfully.")
return True
except Exception as e:
self._report_progress(f"An error occurred during USB writing: {e}\n{traceback.format_exc()}")
return False
@ -289,36 +348,25 @@ class USBWriterLinux:
self._cleanup_temp_files_and_dirs()
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)
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()}"
os.makedirs(mock_download_dir, exist_ok=True)
# Create a more structured mock download similar to gibMacOS output
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)
mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower() # e.g. "sonoma" or "14"
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)
os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True)
os.makedirs(specific_product_folder, exist_ok=True)
# Mock BaseSystem.dmg (tiny, not functional, for path testing)
dummy_bs_dmg_path = os.path.join(specific_product_folder, "BaseSystem.dmg")
if not os.path.exists(dummy_bs_dmg_path):
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy
# 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))
with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist")
with open(os.path.join(specific_product_folder, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f)
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))
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):
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!):")
subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True)
print("\nAvailable block devices (be careful!):"); 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: ")
if not test_device or not test_device.startswith("/dev/"):
print("Invalid device. Exiting.")
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
if confirm.lower() == 'yes':
writer = USBWriterLinux(
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"
)
writer = USBWriterLinux(device=test_device, macos_download_path=mock_download_dir, progress_callback=print, enhance_plist_enabled=True, target_macos_version=target_version_cli)
success = writer.format_and_write()
else: print("Test cancelled by user.")
print(f"Test finished. Success: {success}")

View File

@ -13,8 +13,20 @@ except ImportError:
enhance_config_plist = None
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")
# 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:
def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False,
@ -23,30 +35,32 @@ class USBWriterMacOS:
self.macos_download_path = macos_download_path
self.progress_callback = progress_callback
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()
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_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
# 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_dirs_to_clean = [
self.temp_efi_build_dir, self.temp_opencore_mount,
self.temp_usb_esp_mount, self.temp_macos_source_mount,
self.temp_usb_macos_target_mount, self.temp_dmg_extract_dir
self.temp_efi_build_dir, self.temp_dmg_extract_dir,
self.mount_point_usb_esp, self.mount_point_usb_macos_target
# 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)
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)}")
try:
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 FileNotFoundError: self._report_progress(f"Error: Command '{command[0]}' not found."); raise
def _cleanup_temp_files_and_dirs(self): # Updated for macOS
self._report_progress("Cleaning up temporary files and directories...")
def _cleanup_temp_files_and_dirs(self):
self._report_progress("Cleaning up temporary files, directories, and mounts on macOS...")
for f_path in self.temp_files_to_clean:
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}")
# Detach DMGs first
for dev_path in list(self.attached_dmg_devices): # Iterate copy
for dev_path in list(self.attached_dmg_devices):
self._detach_dmg(dev_path)
self.attached_dmg_devices = []
for d_path in self.temp_dirs_to_clean:
if os.path.ismount(d_path):
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):
try: shutil.rmtree(d_path, ignore_errors=True)
except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
def _detach_dmg(self, device_path_or_mount_point):
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:
# hdiutil detach can take a device path or sometimes a mount path if it's unique enough
# Using -force to ensure it detaches even if volumes are "busy" (after unmount attempts)
if os.path.ismount(device_path_or_mount_point):
self._run_command(["diskutil", "unmount", "force", device_path_or_mount_point], check=False)
if device_path_or_mount_point.startswith("/dev/disk"):
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: # Check if it was in our list
if device_path_or_mount_point in self.attached_dmg_devices:
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:
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):
@ -100,7 +110,7 @@ class USBWriterMacOS:
dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
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("All critical dependencies for macOS USB installer creation found.")
return True
@ -111,22 +121,38 @@ class USBWriterMacOS:
if os.path.isdir(base_path):
for item in os.listdir(base_path):
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"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]
search_base = product_folder_path or self.macos_download_path
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
for pattern in asset_patterns:
# Using iglob for efficiency if many files, but glob is fine for fewer expected matches
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True)
common_subdirs_for_pattern = ["", "SharedSupport"] # Most assets are here or root of product folder
if "Install macOS" in pattern : # If looking for the .app bundle itself
common_subdirs_for_pattern = [""] # Only look at root of product folder
for sub_dir_pattern in common_subdirs_for_pattern:
current_search_base = os.path.join(search_base, sub_dir_pattern)
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=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {pattern}: {found_files[0]}")
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]
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.")
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
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}")
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():
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)
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=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) # Recursive search
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}")
basesystem_dmg_to_process = found_bs_dmg[0]
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._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"));
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
@ -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)
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}")
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)
@ -177,7 +200,7 @@ class USBWriterMacOS:
try:
self.check_dependencies()
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)
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._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 = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
if not disk_info_plist: raise RuntimeError("Failed to get disk info after partitioning.")
disk_info = plistlib.loads(disk_info_plist.encode('utf-8'))
disk_info_plist_str = 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.")
disk_info = plistlib.loads(disk_info_plist_str.encode('utf-8'))
esp_partition_dev = None; macos_partition_dev = None
for disk_entry in disk_info.get("AllDisksAndPartitions", []):
if disk_entry.get("DeviceIdentifier") == self.device.replace("/dev/", ""):
for part in disk_entry.get("Partitions", []):
if part.get("VolumeName") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
# Find the main disk entry first
main_disk_entry = next((d for d in disk_info.get("AllDisksAndPartitions", []) if d.get("DeviceIdentifier") == self.device.replace("/dev/", "")), None)
if main_disk_entry:
for part in main_disk_entry.get("Partitions", []):
if part.get("VolumeName") == "EFI" and part.get("Content") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
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}).")
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}")
# --- Prepare macOS Installer Content ---
product_folder = self._get_gibmacos_product_folder()
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.")
product_folder_path = 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)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG 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):
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._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])
core_services_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
self._report_progress("Copying necessary macOS installer assets to 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:
self._report_progress(f"Copying {original_bs_dmg} to {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")
if os.path.exists(original_bs_chunklist):
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
self._report_progress(f"Copying BaseSystem.dmg to USB CoreServices and App SharedSupport...")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(coreservices_path_usb, "BaseSystem.dmg")])
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")])
original_bs_chunklist = self._find_gibmacos_asset("BaseSystem.chunklist", os.path.dirname(original_bs_dmg), search_deep=False)
if original_bs_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)
if install_info_src:
self._report_progress(f"Copying InstallInfo.plist to {self.temp_usb_macos_target_mount}/InstallInfo.plist")
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.temp_usb_macos_target_mount, "InstallInfo.plist")])
installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True)
if installinfo_src:
self._report_progress(f"Copying 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")
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb])
# 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)")
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_system])
main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True)
if main_payload_src:
self._report_progress(f"Copying main payload {os.path.basename(main_payload_src)} to {packages_dir_usb}/")
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb, os.path.basename(main_payload_src))])
# If it's SharedSupport.dmg, its contents might be what's needed in Packages or elsewhere.
# If InstallAssistant.pkg, it might need to be placed at root or specific app structure.
else: self._report_progress("Warning: Main installer payload not found. Installer may be incomplete.")
payload_basename = os.path.basename(main_payload_src)
self._report_progress(f"Copying main payload '{payload_basename}' to App SharedSupport and System Packages...")
self._run_command(["sudo", "cp", main_payload_src, os.path.join(shared_support_path_usb_app, payload_basename)])
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb_system, payload_basename)])
self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")])
self._report_progress("macOS installer assets copied.")
self._run_command(["sudo", "touch", os.path.join(coreservices_path_usb, "boot.efi")]) # Placeholder for bootability
# --- OpenCore EFI Setup ---
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)
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")
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.")
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._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.")
return True
except Exception as e:
@ -278,34 +316,37 @@ class USBWriterMacOS:
if __name__ == '__main__':
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)
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)
# Simulate a more realistic gibMacOS product folder structure for testing _get_gibmacos_product_folder
mock_product_name = f"012-34567 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.1.2"
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
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)
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
dummy_bs_dmg_path = os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg")
if not os.path.exists(dummy_bs_dmg_path):
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"))
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"), exist_ok=True)
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)
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
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)
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 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()
else: print("Test cancelled.")
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 os
import time
@ -17,9 +17,7 @@ except ImportError:
def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'")
@staticmethod
def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox
Yes = 1 # Mock value
No = 0 # Mock value
Cancel = 0 # Mock value
Yes = 1; No = 0; Cancel = 0
try:
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"
self.disk_number = "".join(filter(str.isdigit, device_id_str))
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}"
@ -131,34 +133,45 @@ class USBWriterWindows:
dependencies = ["diskpart", "robocopy", "7z"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
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("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
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]
search_base = product_folder_path or self.macos_download_path
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
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"]
for sub_dir_pattern in common_subdirs_for_pattern:
current_search_base = os.path.join(search_base, sub_dir_pattern)
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=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {pattern}: {found_files[0]}")
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]
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.")
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
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")
if not os.path.isdir(base_path): base_path = self.macos_download_path
if os.path.isdir(base_path):
for item in os.listdir(base_path):
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"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:
@ -175,8 +188,8 @@ class USBWriterWindows:
basesystem_dmg_to_process = current_target
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)
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=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)
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
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"));
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
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}")
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.")
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"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 primary label=\"Install macOS {self.target_macos_version}\" id=AF00\nexit\n" # Set HFS+ type ID
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=\"{installer_vol_label[:31]}\" id=AF00\nexit\n"
self._run_diskpart_script(diskpart_script_part1)
time.sleep(5)
macos_partition_offset_str = "Offset not determined by diskpart"
macos_partition_number_str = "2 (assumed)"
try:
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n"
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
if detail_output:
self._report_progress(f"Detail Partition Output:\n{detail_output}")
offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE)
if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
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 part_num_match:
macos_partition_number_str = part_num_match.group(1)
num_match = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE)
if num_match:
macos_partition_number_str = num_match.group(1)
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 ---
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:
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)
@ -248,55 +264,64 @@ class USBWriterWindows:
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):
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)
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):
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only for this feature)...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
if self.enhance_plist_enabled and enhance_config_plist:
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.")
else: self._report_progress("config.plist enhancement call failed or had issues.")
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(3)
if not os.path.exists(target_efi_on_usb_root):
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign.")
time.sleep(2) # Allow drive letter to be fully active
if not os.path.exists(target_efi_on_usb_root): raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible.")
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._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...")
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)")
if not source_for_hfs_extraction: source_for_hfs_extraction = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, "InstallAssistant.pkg as BaseSystem source")
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: 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):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
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 = (
f"EFI setup complete on drive {self.assigned_efi_letter}:.\n"
f"BaseSystem HFS image extracted to: '{abs_hfs_path}'.\n\n"
f"MANUAL STEP REQUIRED FOR MAIN macOS PARTITION:\n"
f"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
f"2. Use a 'dd for Windows' utility to write the extracted HFS image.\n"
f" Target: Disk {self.disk_number} (Path: {self.physical_drive_path}), Partition {macos_partition_number_str} (Offset: {macos_partition_offset_str}).\n"
f" Example command (VERIFY SYNTAX FOR YOUR DD TOOL!):\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" OR, if writing to the whole disk by offset (VERY ADVANCED & RISKY if offset is wrong):\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"
"3. After writing BaseSystem, manually copy other installer files (like InstallAssistant.pkg or contents of SharedSupport.dmg) from "
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"
"This tool CANNOT fully automate HFS+ partition writing or HFS+ file copying on Windows."
f"BaseSystem HFS image for macOS installer extracted to: '{abs_hfs_path}'.\n\n"
f"MANUAL STEPS REQUIRED FOR MAIN macOS PARTITION (Partition {macos_partition_number_str} on Disk {self.disk_number}):\n"
f"1. Write BaseSystem Image: Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
f" Use a 'dd for Windows' utility. Example (VERIFY SYNTAX FOR YOUR DD TOOL & TARGETS!):\n"
f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} --target-partition {macos_partition_number_str} bs=4M --progress` (Conceptual)\n"
f" (Offset for partition {macos_partition_number_str} on Disk {self.disk_number} is approx. {macos_partition_offset_str})\n\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" 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" Key files to find in '{abs_download_path}' and copy to the HFS+ partition:\n - {assets_list_str}\n"
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"
"Without these additional files, the USB might only boot to an internet recovery mode (if network & EFI are correct)."
)
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
except Exception as e:
@ -309,18 +334,20 @@ class USBWriterWindows:
if __name__ == '__main__':
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)
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)
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_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
os.makedirs(os.path.join(mock_product_folder, "SharedSupport"), exist_ok=True)
with open(os.path.join(mock_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg")
mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
mock_product_name = f"000-00000 - 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)
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(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
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"), 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)
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.format_and_write()
else: print("Cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True)
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template
shutil.rmtree(mock_download_dir, ignore_errors=True);
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Keep template for other tests potentially
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.