mirror of
https://github.com/sickcodes/Docker-OSX.git
synced 2025-06-21 17:12:47 +02:00
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:
parent
5d0e2da88d
commit
d46413019e
98
README.md
98
README.md
@ -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)
|
||||
|
@ -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}")
|
||||
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]
|
||||
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
|
||||
|
||||
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."""
|
||||
_report(f"Could not identify a specific product folder. Using base 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, 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}")
|
||||
|
@ -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)
|
||||
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 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:
|
||||
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)
|
||||
if found_files:
|
||||
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
|
||||
self._report_progress(f"Found {pattern}: {found_files[0]}")
|
||||
return found_files[0]
|
||||
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.")
|
||||
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=os.path.getsize, reverse=True)
|
||||
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
|
||||
return found_files[0]
|
||||
|
||||
if search_deep:
|
||||
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
|
||||
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
|
||||
if found_files_deep:
|
||||
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
|
||||
return found_files_deep[0]
|
||||
|
||||
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
|
||||
return None
|
||||
|
||||
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')}"
|
||||
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}).")
|
||||
# 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}). 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)
|
||||
|
@ -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)
|
||||
if found_files:
|
||||
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
|
||||
self._report_progress(f"Found {pattern}: {found_files[0]}")
|
||||
return found_files[0]
|
||||
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.")
|
||||
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=os.path.getsize, reverse=True)
|
||||
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
|
||||
return found_files[0]
|
||||
if search_deep:
|
||||
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
|
||||
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
|
||||
if found_files_deep:
|
||||
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
|
||||
return found_files_deep[0]
|
||||
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
|
||||
return None
|
||||
|
||||
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)"
|
||||
|
||||
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)
|
||||
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
|
||||
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)"
|
||||
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.
|
||||
|
Loading…
Reference in New Issue
Block a user