Compare commits

...

3 Commits

Author SHA1 Message Date
google-labs-jules[bot] 91938925c1 refactor!: Complete shift to gibMacOS installer workflow, update all USB writers, major plist enhancements, UI/UX improvements, and full README rework
This monolithic commit represents a comprehensive overhaul of the application,
transitioning from a Docker-OSX based system image creator to a sophisticated
macOS USB Installer creation tool using `corpnewt/gibMacOS.py`. It also
incorporates significant research and implementation for hardware compatibility,
especially for NVIDIA GPUs on newer macOS via OpenCore Legacy Patcher (OCLP)
preparation, and substantial UI/UX enhancements.

**Core Architectural Changes:**
1.  **Installer-Based Workflow with `gibMacOS`:**
    - `main_app.py`: Completely refactored. All Docker dependencies, UI components,
      and related logic have been removed.
    - I introduced a way to download official macOS installer assets
      directly from Apple via `gibMacOS.py`. The UI now reflects a two-step process:
      1. Download macOS Assets, 2. Create USB Installer.
    - The USB writing process now consumes `macos_download_path` from `gibMacOS`.

2.  **Platform-Specific USB Writer Modules (`usb_writer_*.py`) Refactored:**
    - **`usb_writer_linux.py`:** Creates a comprehensive macOS installer.
        - Uses `sgdisk` for GPT partitioning (EFI FAT32, Main HFS+).
        - Employs `7z` to extract BaseSystem HFS image from downloaded assets.
        - Writes BaseSystem image to USB via `dd`.
        - Copies essential installer files (`BaseSystem.dmg`/`.chunklist`,
          `InstallInfo.plist`, `InstallAssistant.pkg`/`InstallESD.dmg`,
          `AppleDiagnostics.dmg`, `boot.efi`) to standard locations within a
          created `Install macOS [VersionName].app` structure on the USB.
        - Sets up OpenCore EFI from `EFI_template_installer`, including
          conditional `config.plist` enhancement via `plist_modifier.py`.
        - Includes logic to emit determinate `rsync` progress (though UI display
          in `main_app.py` was blocked by difficulties).
    - **`usb_writer_macos.py`:** Mirrors Linux writer functionality using native
        macOS tools (`diskutil`, `hdiutil`, `7z`, `dd`, `rsync`/`cp`, `bless`).
        Creates a full installer with custom OpenCore EFI.
    - **`usb_writer_windows.py`:**
        - Automates EFI partition setup (`diskpart`) and OpenCore EFI placement
          (from template + `plist_modifier.py`, using `robocopy`).
        - Extracts BaseSystem HFS image using `7z`.
        - Provides detailed, enhanced guidance for you to manually:
            1.  Write the `BaseSystem.hfs` to the main USB partition using
                "dd for Windows" (includes disk number, path, partition info).
            2.  Copy other installer assets to the HFS+ partition using
                third-party tools or another OS.

3.  **`plist_modifier.py` (OpenCore `config.plist` Enhancement):**
    - Expanded hardware mappings for Intel Alder Lake iGPUs (including headless
      logic if dGPU detected), audio codecs (prioritizing detected names), and
      Ethernet kexts.
    - Refined NVIDIA GTX 970 (Maxwell) `boot-args` logic:
        - `nvda_drv=1` for High Sierra.
        - For Mojave+: `amfi_get_out_of_my_way=0x1` (OCLP prep), and `nv_disable=1`
          if an iGPU is present and primary; otherwise, no `nv_disable=1` to allow
          GTX 970 VESA boot.
    - Creates a `config.plist.backup` before modifications.

4.  **`linux_hardware_info.py` (Hardware Detection - Linux Host):**
    - Added `get_audio_codecs()` to detect audio codec names from `/proc/asound/`,
      improving `layout-id` accuracy for `plist_modifier.py`.

5.  **`EFI_template_installer`:**
    - `config-template.plist` significantly improved with robust, generic defaults
      for modern systems (Alder Lake friendly) and for `plist_modifier.py`.
    - Directory structure for kexts, drivers, ACPI defined with placeholders.

6.  **UI/UX Enhancements (`main_app.py`):**
    - Status bar features a QTimer-driven text-based spinner for active operations.
    - Implemented determinate `QProgressBar` for `gibMacOS` downloads.
    - Centralized UI state management (`_set_ui_busy`, `update_all_button_states`).
    - Improved lifecycle and error/completion signal handling.
    - Privilege checks implemented before USB writing.
    - Windows USB detection improved using PowerShell/WMI to populate a selectable list.

7.  **Documentation (`README.md`):**
    - Completely rewritten with "Skyscope" branding and project vision.
    - Details the new `gibMacOS`-based installer workflow.
    *   Explains the NVIDIA GPU support strategy (guiding you to OCLP for
        post-install acceleration on newer macOS).
    *   Comprehensive prerequisites (including `gibMacOS.py` setup, `7z`, platform
        tools like `hfsprogs` and `apfs-fuse` build info for Debian).
    *   Updated usage instructions and current limitations.
    *   Version updated to 1.1.0.

**Known Issue/Stuck Point:**
-   Persistent difficulties prevented the full integration of determinate `rsync`
    progress display in `main_app.py`. While `usb_writer_linux.py` emits the
    data, I could not reliably update `main_app.py` to use it for the progress bar.

This change represents a foundational shift to a more flexible and direct
method of macOS installer creation and incorporates many advanced configuration
and usability features.
2025-06-13 10:47:04 +00:00
google-labs-jules[bot] 4665531407 feat: Implement gibMacOS workflow, USB writers, plist modifier, UI/UX enhancements
This commit culminates a major refactoring to an installer-based workflow using
`gibMacOS` and includes significant enhancements to platform-specific USB
writers, `config.plist` modification, and UI/UX feedback.

**1. Core Workflow Change (Installer-Based via `gibMacOS`):**
    - `main_app.py`: I re-architected this to use `corpnewt/gibMacOS.py` for downloading
      official macOS installer assets. Docker dependencies and related UI/logic
      have been removed. I introduced a new approach for downloads using `gibMacOS`.
    - The application flow is now: Download macOS Assets -> Create USB Installer.

**2. Platform-Specific USB Writer Modules (`usb_writer_*.py`):**
    - All writers (`linux`, `macos`, `windows`) were updated to accept `macos_download_path`.
    - **`usb_writer_linux.py`:**
        - Creates macOS installer USB using `sgdisk`, `mkfs.hfsplus`, `7z` (for
          BaseSystem HFS extraction), `dd` (for BaseSystem write).
        - Copies key installer assets (`BaseSystem.dmg`/`.chunklist`, `InstallInfo.plist`,
          `InstallAssistant.pkg`, `AppleDiagnostics.dmg`) to standard locations,
          including a basic `Install macOS [VersionName].app` structure.
        - Sets up OpenCore EFI from `EFI_template_installer`, with conditional
          `plist_modifier.py` enhancements.
        - **`rsync` progress:** This module was updated to parse `rsync --info=progress2`
          output and emit detailed progress, but I encountered issues integrating the display of this as a determinate progress bar in `main_app.py`.
          Raw rsync log lines with progress will still appear.
    - **`usb_writer_macos.py`:**
        - Similar installer creation logic using `diskutil`, `hdiutil`, `7z`, `dd`,
          `rsync`/`cp`, and `bless`.
    - **`usb_writer_windows.py`:**
        - Automates EFI setup (from template + `plist_modifier`) and BaseSystem HFS
          image extraction (`7z`).
        - Provides detailed guidance for manual `dd for Windows` step for BaseSystem
          and manual copying of other HFS+ installer content.

**3. `plist_modifier.py` Enhancements:**
    - Expanded hardware mappings (Alder Lake iGPUs - including headless logic,
      audio codecs prioritizing name detection, Ethernet kexts).
    - Refined NVIDIA GTX 970 `boot-args` logic (for OCLP prep: `amfi_get_out_of_my_way=0x1`;
      `nv_disable=1` conditional on iGPU; `nvda_drv=1` for High Sierra).
    - Creates `config.plist.backup` before modifications.

**4. `linux_hardware_info.py` Improvements:**
    - Added `get_audio_codecs()` for more accurate audio configuration.

**5. `EFI_template_installer`:**
    - `config-template.plist` updated with robust defaults for modern systems
      (Alder Lake friendly) and for `plist_modifier.py`.
    - Placeholder files for kexts, drivers, ACPI established.

**6. UI/UX Enhancements (`main_app.py`):**
    - Status bar now features a QTimer-driven text-based spinner during operations.
    - Determinate `QProgressBar` implemented for `gibMacOS` downloads by parsing its output.
    - Centralized UI state management (`_set_ui_busy`, `update_all_button_states`).
    - Improved lifecycle management and error/completion handling.

**7. Documentation (`README.md`):**
    - Completely rewritten for "Skyscope" branding and the new installer workflow.
    *   Details NVIDIA GPU strategy (guidance towards post-install OCLP).
    *   Comprehensive prerequisites (including `gibMacOS.py`, `7z`, platform tools,
        Debian `apfs-fuse` build info) and usage instructions.

**Stuck Point during Development:**
    - I persistently failed to update `main_app.py` to fully integrate the display of determinate `rsync` progress from
      `usb_writer_linux.py`. The `usb_writer_linux.py` module *does* emit the
      necessary progress data, but the `main_app.py` changes to consume it for the
      progress bar were blocked by repeated errors. This means `rsync` progress
      will appear in logs but not as a filling bar.

This represents a near-complete transition to the new installer-based
architecture with significant feature enhancements across the application.
2025-06-13 09:52:34 +00:00
google-labs-jules[bot] d46413019e Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue. 2025-06-13 07:03:53 +00:00
5 changed files with 963 additions and 675 deletions

View File

@ -58,17 +58,11 @@
<array> <array>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>Lilu.kext</string><key>Comment</key><string>Lilu</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/Lilu</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>Lilu.kext</string><key>Comment</key><string>Lilu</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/Lilu</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>VirtualSMC.kext</string><key>Comment</key><string>VirtualSMC</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/VirtualSMC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>VirtualSMC.kext</string><key>Comment</key><string>VirtualSMC</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/VirtualSMC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>SMCProcessor.kext</string><key>Comment</key><string>SMCProcessor for CPU temp</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/SMCProcessor</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>SMCSuperIO.kext</string><key>Comment</key><string>SMCSuperIO for fan speeds</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/SMCSuperIO</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>WhateverGreen.kext</string><key>Comment</key><string>WhateverGreen for Graphics</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/WhateverGreen</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>WhateverGreen.kext</string><key>Comment</key><string>WhateverGreen for Graphics</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/WhateverGreen</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>AppleALC.kext</string><key>Comment</key><string>AppleALC for Audio</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/AppleALC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>AppleALC.kext</string><key>Comment</key><string>AppleALC for Audio</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/AppleALC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>IntelMausi.kext</string><key>Comment</key><string>Intel Ethernet</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/IntelMausi</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>IntelMausi.kext</string><key>Comment</key><string>Intel Ethernet</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/IntelMausi</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>RealtekRTL8111.kext</string><key>Comment</key><string>Realtek RTL8111</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/RealtekRTL8111</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>RealtekRTL8111.kext</string><key>Comment</key><string>Realtek RTL8111</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/RealtekRTL8111</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>LucyRTL8125Ethernet.kext</string><key>Comment</key><string>Realtek RTL8125 2.5GbE</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/LucyRTL8125Ethernet</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>LucyRTL8125Ethernet.kext</string><key>Comment</key><string>Realtek RTL8125 2.5GbE</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/LucyRTL8125Ethernet</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>NVMeFix.kext</string><key>Comment</key><string>NVMe Fixes</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/NVMeFix</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>CpuTopologyRebuild.kext</string><key>Comment</key><string>Alder Lake E-Core/P-Core fix</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/CpuTopologyRebuild</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>RestrictEvents.kext</string><key>Comment</key><string>Restrict unwanted events</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/RestrictEvents</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
</array> </array>
<key>Block</key><array/> <key>Block</key><array/>
<key>Emulate</key><dict><key>Cpuid1Data</key><data></data><key>Cpuid1Mask</key><data></data><key>DummyPowerManagement</key><false/><key>MaxKernel</key><string></string><key>MinKernel</key><string></string></dict> <key>Emulate</key><dict><key>Cpuid1Data</key><data></data><key>Cpuid1Mask</key><data></data><key>DummyPowerManagement</key><false/><key>MaxKernel</key><string></string><key>MinKernel</key><string></string></dict>
@ -101,9 +95,9 @@
</dict> </dict>
<key>Scheme</key><dict><key>CustomKernel</key><false/><key>FuzzyMatch</key><true/><key>KernelArch</key><string>Auto</string><key>KernelCache</key><string>Auto</string></dict> <key>Scheme</key><dict><key>CustomKernel</key><false/><key>FuzzyMatch</key><true/><key>KernelArch</key><string>Auto</string><key>KernelCache</key><string>Auto</string></dict>
</dict> </dict>
<key>Misc</key><dict><key>BlessOverride</key><array/><key>Boot</key><dict><key>ConsoleAttributes</key><integer>0</integer><key>HibernateMode</key><string>None</string><key>HibernateSkipsPicker</key><true/><key>HideAuxiliary</key><true/><key>LauncherOption</key><string>Disabled</string><key>LauncherPath</key><string>Default</string><key>PickerAttributes</key><integer>17</integer><key>PickerAudioAssist</key><false/><key>PickerMode</key><string>External</string><key>PickerVariant</key><string>Acidanthera\GoldenGate</string><key>PollAppleHotKeys</key><true/><key>ShowPicker</key><true/><key>TakeoffDelay</key><integer>0</integer><key>Timeout</key><integer>5</integer></dict><key>Debug</key><dict><key>AppleDebug</key><false/><key>ApplePanic</key><false/><key>DisableWatchDog</key><true/><key>DisplayDelay</key><integer>0</integer><key>DisplayLevel</key><integer>2147483650</integer><key>LogModules</key><string>*</string><key>SysReport</key><false/><key>Target</key><integer>0</integer></dict><key>Entries</key><array/><key>Security</key><dict><key>AllowSetDefault</key><true/><key>ApECID</key><integer>0</integer><key>AuthRestart</key><false/><key>BlacklistAppleUpdate</key><true/><key>DmgLoading</key><string>Signed</string><key>EnablePassword</key><false/><key>ExposeSensitiveData</key><integer>6</integer><key>HaltLevel</key><integer>2147483648</integer><key>PasswordHash</key><data></data><key>PasswordSalt</key><data></data><key>ScanPolicy</key><integer>0</integer><key>SecureBootModel</key><string>Disabled</string><key>Vault</key><string>Optional</string></dict><key>Serial</key><dict><key>Init</key><false/><key>Override</key><false/></dict><key>Tools</key><array/></dict> <key>PickerAudioAssist</key><false/><key>PickerMode</key><string>External</string><key>PickerVariant</key><string>Auto</string><key>PollAppleHotKeys</key><true/><key>ShowPicker</key><true/><key>TakeoffDelay</key><integer>0</integer><key>Timeout</key><integer>5</integer></dict><key>Debug</key><dict><key>AppleDebug</key><false/><key>ApplePanic</key><false/><key>DisableWatchDog</key><true/><key>DisplayDelay</key><integer>0</integer><key>DisplayLevel</key><integer>2147483650</integer><key>LogModules</key><string>*</string><key>SysReport</key><false/><key>Target</key><integer>0</integer></dict><key>Entries</key><array/><key>Security</key><dict><key>AllowSetDefault</key><true/><key>ApECID</key><integer>0</integer><key>AuthRestart</key><false/><key>BlacklistAppleUpdate</key><true/><key>DmgLoading</key><string>Signed</string><key>EnablePassword</key><false/><key>ExposeSensitiveData</key><integer>6</integer><key>HaltLevel</key><integer>2147483648</integer><key>PasswordHash</key><data></data><key>PasswordSalt</key><data></data><key>ScanPolicy</key><integer>0</integer><key>SecureBootModel</key><string>Disabled</string><key>Vault</key><string>Optional</string></dict><key>Serial</key><dict><key>Init</key><false/><key>Override</key><false/></dict><key>Tools</key><array/></dict>
<key>NVRAM</key><dict><key>Add</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><dict><key>DefaultBackgroundColor</key><data>AAAAAA==</data><key>UIScale</key><data>AQ==</data></dict><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><dict><key>SystemAudioVolume</key><data>Rg==</data><key>boot-args</key><string>-v keepsyms=1 debug=0x100 alcid=1</string><key>csr-active-config</key><data>AAAAAA==</data><key>prev-lang:kbd</key><data>ZW4tVVM6MA==</data><key>run-efi-updater</key><string>No</string></dict></dict><key>Delete</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><array><string>UIScale</string><string>DefaultBackgroundColor</string></array><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><array><string>boot-args</string><string>csr-active-config</string></array></dict><key>LegacyOverwrite</key><false/><key>LegacySchema</key><dict/><key>WriteFlash</key><true/></dict> <key>NVRAM</key><dict><key>Add</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><dict><key>DefaultBackgroundColor</key><data>AAAAAA==</data><key>UIScale</key><data>AQ==</data></dict><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><dict><key>SystemAudioVolume</key><data>Rg==</data><key>boot-args</key><string>-v keepsyms=1 debug=0x100 alcid=1</string><key>csr-active-config</key><data>AAAAAA==</data><key>prev-lang:kbd</key><string>en-US:0</string><key>run-efi-updater</key><string>No</string></dict></dict><key>Delete</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><array><string>UIScale</string><string>DefaultBackgroundColor</string></array><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><array><string>boot-args</string><string>csr-active-config</string></array></dict><key>LegacyOverwrite</key><false/><key>LegacySchema</key><dict/><key>WriteFlash</key><true/></dict>
<key>PlatformInfo</key><dict><key>Automatic</key><true/><key>CustomMemory</key><false/><key>Generic</key><dict><key>AdviseFeatures</key><false/><key>MLB</key><string>CHANGE_ME_MLB</string><key>MaxBIOSVersion</key><false/><key>ProcessorType</key><integer>0</integer><key>ROM</key><data>AAAAAA==</data><key>SpoofVendor</key><true/><key>SystemMemoryStatus</key><string>Auto</string><key>SystemProductName</key><string>iMacPro1,1</string><key>SystemSerialNumber</key><string>CHANGE_ME_SERIAL</string><key>SystemUUID</key><string>CHANGE_ME_UUID</string></dict><key>UpdateDataHub</key><true/><key>UpdateNVRAM</key><true/><key>UpdateSMBIOS</key><true/><key>UpdateSMBIOSMode</key><string>Create</string><key>UseRawUuidEncoding</key><false/></dict> <key>SystemProductName</key><string>iMacPro1,1</string><key>SystemSerialNumber</key><string>CHANGEME</string><key>SystemUUID</key><string>CHANGEME</string></dict><key>UpdateDataHub</key><true/><key>UpdateNVRAM</key><true/><key>UpdateSMBIOS</key><true/><key>UpdateSMBIOSMode</key><string>Create</string><key>UseRawUuidEncoding</key><false/></dict>
<key>UEFI</key><dict><key>APFS</key><dict><key>EnableJumpstart</key><true/><key>GlobalConnect</key><false/><key>HideVerbose</key><true/><key>JumpstartHotPlug</key><false/><key>MinDate</key><integer>0</integer><key>MinVersion</key><integer>0</integer></dict><key>AppleInput</key><dict><key>AppleEvent</key><string>Builtin</string><key>CustomDelays</key><false/><key>GraphicsInputMirroring</key><true/><key>KeyInitialDelay</key><integer>50</integer><key>KeySubsequentDelay</key><integer>5</integer><key>PointerSpeedDiv</key><integer>1</integer><key>PointerSpeedMul</key><integer>1</integer></dict><key>Audio</key><dict><key>AudioCodec</key><integer>0</integer><key>AudioDevice</key><string></string><key>AudioOutMask</key><integer>-1</integer><key>AudioSupport</key><false/><key>DisconnectHda</key><false/><key>MaximumGain</key><integer>-15</integer><key>MinimumAssistGain</key><integer>-30</integer><key>MinimumAudibleGain</key><integer>-55</integer><key>PlayChime</key><string>Auto</string><key>ResetTrafficClass</key><false/><key>SetupDelay</key><integer>0</integer></dict><key>ConnectDrivers</key><true/><key>Drivers</key><array><string>HfsPlus.efi</string><string>OpenRuntime.efi</string><string>OpenCanopy.efi</string><!-- Add OpenPartitionDxe.efi for some systems --></array><key>Input</key><dict><key>KeyFiltering</key><false/><key>KeyForgetThreshold</key><integer>5</integer><key>KeySupport</key><true/><key>KeySupportMode</key><string>Auto</string><key>KeySwap</key><false/><key>PointerSupport</key><false/><key>PointerSupportMode</key><string>ASUS</string><key>TimerResolution</key><integer>50000</integer></dict><key>Output</key><dict><key>ClearScreenOnModeSwitch</key><false/><key>ConsoleMode</key><string></string><key>DirectGopRendering</key><false/><key>ForceResolution</key><false/><key>GopPassThrough</key><string>Disabled</string><key>IgnoreTextInGraphics</key><false/><key>ProvideConsoleGop</key><true/><key>ReconnectGraphicsOnConnect</key><false/><key>ReconnectOnResChange</key><false/><key>ReplaceTabWithSpace</key><false/><key>Resolution</key><string>Max</string><key>SanitiseClearScreen</key><false/><key>TextRenderer</key><string>BuiltinGraphics</string><key>UIScale</key><integer>-1</integer><key>UgaPassThrough</key><false/></dict><key>ProtocolOverrides</key><dict/><key>Quirks</key><dict><key>ActivateHpetSupport</key><false/><key>DisableSecurityPolicy</key><false/><key>EnableVectorAcceleration</key><true/><key>EnableVmx</key><false/><key>ExitBootServicesDelay</key><integer>0</integer><key>ForceOcWriteFlash</key><false/><key>ForgeUefiSupport</key><false/><key>IgnoreInvalidFlexRatio</key><false/><key>ReleaseUsbOwnership</key><true/><!-- Often True for modern systems --> <key>ReloadOptionRoms</key><false/><key>RequestBootVarRouting</key><true/><key>ResizeGpuBars</key><integer>-1</integer><key>TscSyncTimeout</key><integer>0</integer><key>UnblockFsConnect</key><false/></dict><key>ReservedMemory</key><array/></dict> <key>UEFI</key><dict><key>APFS</key><dict><key>EnableJumpstart</key><true/><key>GlobalConnect</key><false/><key>HideVerbose</key><true/><key>JumpstartHotPlug</key><false/><key>MinDate</key><integer>-1</integer><key>MinVersion</key><integer>-1</integer></dict><key>AppleInput</key><dict><key>AppleEvent</key><string>Builtin</string><key>CustomDelays</key><false/><key>GraphicsInputMirroring</key><true/><key>KeyInitialDelay</key><integer>50</integer><key>KeySubsequentDelay</key><integer>5</integer><key>PointerSpeedDiv</key><integer>1</integer><key>PointerSpeedMul</key><integer>1</integer></dict><key>Audio</key><dict><key>AudioCodec</key><integer>0</integer><key>AudioDevice</key><string>PciRoot(0x0)/Pci(0x1b,0x0)</string><key>AudioOutMask</key><integer>1</integer><key>AudioSupport</key><true/><key>DisconnectHda</key><false/><key>MaximumGain</key><integer>-15</integer><key>MinimumAssistGain</key><integer>-30</integer><key>MinimumAudibleGain</key><integer>-55</integer><key>PlayChime</key><string>Auto</string><key>ResetTrafficClass</key><false/><key>SetupDelay</key><integer>0</integer></dict><key>ConnectDrivers</key><true/><key>Drivers</key><array><string>HfsPlus.efi</string><string>OpenRuntime.efi</string><string>OpenCanopy.efi</string></array><key>Input</key><dict><key>KeyFiltering</key><false/><key>KeyForgetThreshold</key><integer>5</integer><key>KeySupport</key><true/><key>KeySupportMode</key><string>Auto</string><key>KeySwap</key><false/><key>PointerSupport</key><false/><key>PointerSupportMode</key><string>ASUS</string><key>TimerResolution</key><integer>50000</integer></dict><key>Output</key><dict><key>ClearScreenOnModeSwitch</key><false/><key>ConsoleMode</key><string></string><key>DirectGopRendering</key><false/><key>ForceResolution</key><false/><key>GopPassThrough</key><string>Disabled</string><key>IgnoreTextInGraphics</key><false/><key>ProvideConsoleGop</key><true/><key>ReconnectGraphicsOnConnect</key><false/><key>ReconnectOnResChange</key><false/><key>ReplaceTabWithSpace</key><false/><key>Resolution</key><string>Max</string><key>SanitiseClearScreen</key><false/><key>TextRenderer</key><string>BuiltinGraphics</string><key>UIScale</key><integer>0</integer><key>UgaPassThrough</key><false/></dict><key>ProtocolOverrides</key><dict/><key>Quirks</key><dict><key>ActivateHpetSupport</key><false/><key>DisableSecurityPolicy</key><false/><key>EnableVectorAcceleration</key><true/><key>EnableVmx</key><false/><key>ExitBootServicesDelay</key><integer>0</integer><key>ForceOcWriteFlash</key><false/><key>ForgeUefiSupport</key><false/><key>IgnoreInvalidFlexRatio</key><false/><key>ReleaseUsbOwnership</key><true/><key>ReloadOptionRoms</key><false/><key>RequestBootVarRouting</key><true/><key>ResizeGpuBars</key><integer>-1</integer><key>TscSyncTimeout</key><integer>0</integer><key>UnblockFsConnect</key><false/></dict><key>ReservedMemory</key><array/></dict>
</dict> </dict>
</plist> </plist>

View File

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

View File

@ -1,4 +1,4 @@
# usb_writer_linux.py (Refined asset copying) # usb_writer_linux.py (Finalizing installer asset copying - refined)
import subprocess import subprocess
import os import os
import time import time
@ -6,66 +6,95 @@ import shutil
import glob import glob
import re import re
import plistlib import plistlib
import traceback
try: try:
from plist_modifier import enhance_config_plist from plist_modifier import enhance_config_plist
except ImportError: except ImportError:
enhance_config_plist = None enhance_config_plist = None
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled for USBWriterLinux.") # from constants import MACOS_VERSIONS # Imported in _get_gibmacos_product_folder
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
class USBWriterLinux: class USBWriterLinux:
def __init__(self, device: str, macos_download_path: str, def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""): # target_macos_version is display name e.g. "Sonoma" target_macos_version: str = ""):
self.device = device self.device = device; self.macos_download_path = macos_download_path
self.macos_download_path = macos_download_path self.progress_callback = progress_callback; self.enhance_plist_enabled = enhance_plist_enabled
self.progress_callback = progress_callback self.target_macos_version = target_macos_version; pid = os.getpid()
self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version
pid = os.getpid()
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs" self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
self.temp_efi_build_dir = f"temp_efi_build_{pid}" self.temp_efi_build_dir = f"temp_efi_build_{pid}"
self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For extracting HFS from DMG
self.mount_point_usb_esp = f"/mnt/usb_esp_temp_skyscope_{pid}" self.mount_point_usb_esp = f"/mnt/usb_esp_temp_skyscope_{pid}"
self.mount_point_usb_macos_target = f"/mnt/usb_macos_target_temp_skyscope_{pid}" self.mount_point_usb_macos_target = f"/mnt/usb_macos_target_temp_skyscope_{pid}"
self.temp_shared_support_mount = f"/mnt/shared_support_temp_{pid}"
self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # Added for _extract_hfs_from_dmg_or_pkg
self.temp_files_to_clean = [self.temp_basesystem_hfs_path] self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
self.temp_dirs_to_clean = [ self.temp_dirs_to_clean = [
self.temp_efi_build_dir, self.mount_point_usb_esp, self.temp_efi_build_dir, self.mount_point_usb_esp,
self.mount_point_usb_macos_target, self.temp_dmg_extract_dir self.mount_point_usb_macos_target, self.temp_shared_support_mount,
self.temp_dmg_extract_dir # Ensure this is cleaned
] ]
def _report_progress(self, message: str): def _report_progress(self, message: str, is_rsync_line: bool = False):
if is_rsync_line:
match = re.search(r"(\d+)%\s+", message)
if match:
try: percentage = int(match.group(1)); self.progress_callback(f"PROGRESS_VALUE:{percentage}")
except ValueError: pass
if self.progress_callback: self.progress_callback(message)
else: print(message)
else:
if self.progress_callback: self.progress_callback(message) if self.progress_callback: self.progress_callback(message)
else: print(message) else: print(message)
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None): def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None, stream_rsync_progress=False):
self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}") cmd_list = command if isinstance(command, list) else command.split()
is_rsync_progress_command = stream_rsync_progress and "rsync" in cmd_list[0 if cmd_list[0] != "sudo" else (1 if len(cmd_list) > 1 else 0)]
if is_rsync_progress_command:
effective_cmd_list = list(cmd_list)
rsync_idx = -1
for i, arg in enumerate(effective_cmd_list):
if "rsync" in arg: rsync_idx = i; break
if rsync_idx != -1:
conflicting_flags = ["-P", "--progress"]; effective_cmd_list = [arg for arg in effective_cmd_list if arg not in conflicting_flags]
actual_rsync_cmd_index_in_list = -1
for i, arg_part in enumerate(effective_cmd_list):
if "rsync" in os.path.basename(arg_part): actual_rsync_cmd_index_in_list = i; break
if actual_rsync_cmd_index_in_list != -1:
if "--info=progress2" not in effective_cmd_list: effective_cmd_list.insert(actual_rsync_cmd_index_in_list + 1, "--info=progress2")
if "--no-inc-recursive" not in effective_cmd_list : effective_cmd_list.insert(actual_rsync_cmd_index_in_list + 1, "--no-inc-recursive")
else: self._report_progress("Warning: rsync command part not found for progress flag insertion.")
self._report_progress(f"Executing (with progress streaming): {' '.join(effective_cmd_list)}")
process = subprocess.Popen(effective_cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True, cwd=working_dir)
stdout_lines, stderr_lines = [], []
if process.stdout:
for line in iter(process.stdout.readline, ''): line_strip = line.strip(); self._report_progress(line_strip, is_rsync_line=True); stdout_lines.append(line_strip)
process.stdout.close()
if process.stderr:
for line in iter(process.stderr.readline, ''): line_strip = line.strip(); self._report_progress(f"STDERR: {line_strip}"); stderr_lines.append(line_strip)
process.stderr.close()
return_code = process.wait(timeout=timeout);
if check and return_code != 0: raise subprocess.CalledProcessError(return_code, effective_cmd_list, output="\n".join(stdout_lines), stderr="\n".join(stderr_lines))
return subprocess.CompletedProcess(args=effective_cmd_list, returncode=return_code, stdout="\n".join(stdout_lines), stderr="\n".join(stderr_lines))
else:
self._report_progress(f"Executing: {' '.join(cmd_list)}")
try: try:
process = subprocess.run( process = subprocess.run(cmd_list, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=0)
command, check=check, capture_output=capture_output, text=True, timeout=timeout,
shell=shell, cwd=working_dir,
creationflags=0
)
if capture_output: if capture_output:
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}") if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}") if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
return process return process
except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise except FileNotFoundError: self._report_progress(f"Error: Command '{cmd_list[0]}' not found."); raise
def _cleanup_temp_files_and_dirs(self): def _cleanup_temp_files_and_dirs(self):
self._report_progress("Cleaning up temporary files and directories...") self._report_progress("Cleaning up...")
for mp in self.temp_dirs_to_clean: for mp in self.temp_dirs_to_clean:
if os.path.ismount(mp): if os.path.ismount(mp): self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15)
self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15)
for f_path in self.temp_files_to_clean: for f_path in self.temp_files_to_clean:
if os.path.exists(f_path): if os.path.exists(f_path):
try: self._run_command(["sudo", "rm", "-f", f_path], check=False) try: self._run_command(["sudo", "rm", "-f", f_path], check=False)
@ -75,278 +104,205 @@ class USBWriterLinux:
try: self._run_command(["sudo", "rm", "-rf", d_path], check=False) try: self._run_command(["sudo", "rm", "-rf", d_path], check=False)
except Exception as e: self._report_progress(f"Error removing temp dir {d_path}: {e}") except Exception as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
def check_dependencies(self): def check_dependencies(self): self._report_progress("Checking deps...");deps=["sgdisk","parted","mkfs.vfat","mkfs.hfsplus","7z","rsync","dd"];m=[d for d in deps if not shutil.which(d)]; assert not m, f"Missing: {', '.join(m)}. Install hfsprogs for mkfs.hfsplus, p7zip for 7z."; return True
self._report_progress("Checking dependencies (sgdisk, mkfs.vfat, mkfs.hfsplus, 7z, rsync, dd)...")
dependencies = ["sgdisk", "mkfs.vfat", "mkfs.hfsplus", "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)}. Please install them (e.g., hfsprogs, p7zip-full, gdisk)."
self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("All critical dependencies for Linux USB installer creation found.")
return True
def _get_gibmacos_product_folder(self) -> str: def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None:
"""Heuristically finds the main product folder within gibMacOS downloads."""
# gibMacOS often creates .../publicrelease/XXX - macOS [VersionName] [VersionNum]/
# We need to find this folder.
_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"
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))
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]
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."""
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
self._report_progress(f"Searching for {description} using patterns {asset_patterns} in {product_folder}...") 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: for pattern in asset_patterns:
# Search both in root of product_folder and common subdirs like "SharedSupport" or "*.app/Contents/SharedSupport" 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"]
search_glob_patterns = [ for sub_dir_pattern in common_subdirs_for_pattern:
os.path.join(product_folder, pattern), current_search_base = os.path.join(search_base, sub_dir_pattern)
os.path.join(product_folder, "**", pattern), # Recursive search # Escape special characters for glob, but allow wildcards in pattern itself
] # This simple escape might not be perfect for all glob patterns.
for glob_pattern in search_glob_patterns: glob_pattern = os.path.join(glob.escape(current_search_base), pattern)
found_files = glob.glob(glob_pattern, recursive=True)
found_files = glob.glob(glob_pattern, recursive=False)
if found_files: if found_files:
# Sort to get a predictable one if multiple (e.g. if pattern is too generic) found_files.sort(key=os.path.getsize, reverse=True)
# Prefer files not too deep in structure if multiple found by simple pattern self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {description} at: {found_files[0]}")
return found_files[0] return found_files[0]
self._report_progress(f"Warning: {description} not found with patterns: {asset_patterns} in {product_folder} or its subdirectories.")
if search_deep:
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
if found_files_deep:
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
return found_files_deep[0]
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
return None return None
def _extract_basesystem_hfs_from_source(self, source_dmg_path: str, output_hfs_path: str) -> bool: def _get_gibmacos_product_folder(self) -> str | None:
"""Extracts the primary HFS+ partition image (e.g., '4.hfs') from a source DMG (BaseSystem.dmg or InstallESD.dmg).""" from constants import MACOS_VERSIONS
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True) 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)
version_tag_from_constants = 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_from_constants 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}'. 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:
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path
try: try:
self._report_progress(f"Extracting HFS+ partition image from {source_dmg_path} into {self.temp_dmg_extract_dir}...") if dmg_or_pkg_path.endswith(".pkg"): self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True); dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert dmgs_in_pkg, "No DMG in PKG."; current_target = max(dmgs_in_pkg, key=os.path.getsize, default=dmgs_in_pkg[0]); assert current_target, "No primary DMG in PKG."; self._report_progress(f"Using DMG from PKG: {current_target}")
# 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar for BaseSystem) assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}"
# For InstallESD.dmg, it might be a different internal path or structure. basesystem_dmg_to_process = current_target
# Assuming the target is a standard BaseSystem.dmg or a DMG containing such structure. 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", "-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); assert found_bs_dmg, f"No BaseSystem.dmg from {current_target}"; basesystem_dmg_to_process = found_bs_dmg[0]
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*.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: self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True); 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)) > 2*1024*1024*1024] # Min 2GB HFS for BaseSystem
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")) assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}"
if not hfs_files: 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
# Fallback: try extracting * (if only one file inside a simple DMG, like some custom BaseSystem.dmg) except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
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: 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
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
return True
except Exception as e:
self._report_progress(f"Error during HFS extraction from DMG: {e}\n{traceback.format_exc()}")
return False
finally: finally:
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
def _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);[os.makedirs(os.path.join(oc_dir,s),exist_ok=True) for s in ["Drivers","Kexts","ACPI","Tools","Resources"]];open(os.path.join(efi_dir_path,"EFI","BOOT","BOOTx64.efi"),"w").close();open(os.path.join(oc_dir,"OpenCore.efi"),"w").close();bc={"#Comment":"Basic config","Misc":{"Security":{"ScanPolicy":0,"SecureBootModel":"Disabled"}},"PlatformInfo":{"Generic":{"MLB":"CHANGE_ME_MLB","SystemSerialNumber":"CHANGE_ME_SERIAL","SystemUUID":"CHANGE_ME_UUID","ROM":b"\0"*6}}};plistlib.dump(bc,open(os.path.join(oc_dir,"config.plist"),'wb'),fmt=plistlib.PlistFormat.XML)
def format_and_write(self) -> bool: def format_and_write(self) -> bool:
try: try:
self.check_dependencies() self.check_dependencies(); self._cleanup_temp_files_and_dirs();
self._cleanup_temp_files_and_dirs() for mp_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])
for mp in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]: self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!");
self._run_command(["sudo", "mkdir", "-p", mp])
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
for i in range(1, 10): self._run_command(["sudo", "umount", "-lf", f"{self.device}{i}"], check=False, timeout=5); self._run_command(["sudo", "umount", "-lf", f"{self.device}p{i}"], check=False, timeout=5) for i in range(1, 10): self._run_command(["sudo", "umount", "-lf", f"{self.device}{i}"], check=False, timeout=5); self._run_command(["sudo", "umount", "-lf", f"{self.device}p{i}"], check=False, timeout=5)
self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...") self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...")
self._run_command(["sudo", "sgdisk", "--zap-all", self.device]) self._run_command(["sudo", "sgdisk", "--zap-all", self.device])
self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device]) self._run_command(["sudo", "sgdisk", "-n", "0:0:+551MiB", "-t", "0:ef00", "-c", "0: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", "0:0:0", "-t", "0:af00", "-c", f"0:{usb_vol_name[:11]}" , self.device])
self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3) self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3)
esp_dev=f"{self.device}1" if os.path.exists(f"{self.device}1") else f"{self.device}p1"; macos_part=f"{self.device}2" if os.path.exists(f"{self.device}2") else f"{self.device}p2"; assert os.path.exists(esp_dev) and os.path.exists(macos_part), "Partitions not found."
self._report_progress(f"Formatting ESP {esp_dev}..."); self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_dev])
self._report_progress(f"Formatting macOS partition {macos_part}..."); self._run_command(["sudo", "mkfs.hfsplus", "-v", usb_vol_name, macos_part])
esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None) product_folder_path = self._get_gibmacos_product_folder()
macos_partition_dev = next((f"{self.device}{i}" for i in ["2", "p2"] if os.path.exists(f"{self.device}{i}")), None) basesystem_source_dmg_or_pkg = 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 (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not reliably determine partition names for {self.device}.") if not basesystem_source_dmg_or_pkg: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.")
if not self._extract_hfs_from_dmg_or_pkg(basesystem_source_dmg_or_pkg, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
self._report_progress(f"Writing BaseSystem to {macos_part}..."); self._run_command(["sudo","dd",f"if={self.temp_basesystem_hfs_path}",f"of={macos_part}","bs=4M","status=progress","oflag=sync"])
self._report_progress("Mounting macOS USB partition..."); self._run_command(["sudo","mount",macos_part,self.mount_point_usb_macos_target])
self._report_progress(f"Formatting ESP ({esp_partition_dev}) as FAT32...") # --- Finalizing macOS Installer Content on USB's HFS+ partition ---
self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_partition_dev]) self._report_progress("Finalizing macOS installer content on USB...")
self._report_progress(f"Formatting macOS Install partition ({macos_partition_dev}) as HFS+...") usb_target_root = self.mount_point_usb_macos_target
self._run_command(["sudo", "mkfs.hfsplus", "-v", f"Install macOS {self.target_macos_version}", macos_partition_dev])
# --- Prepare macOS Installer Content --- app_bundle_name = f"Install macOS {self.target_macos_version}.app"
product_folder = self._get_gibmacos_product_folder() app_bundle_path_usb = os.path.join(usb_target_root, 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") # For createinstallmedia structure
sys_install_pkgs_usb = os.path.join(usb_target_root, "System", "Installation", "Packages")
coreservices_path_usb = os.path.join(usb_target_root, "System", "Library", "CoreServices")
# Find BaseSystem.dmg (or equivalent like InstallESD.dmg if BaseSystem.dmg is not directly available) for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]:
# Some gibMacOS downloads might have InstallESD.dmg which contains BaseSystem.dmg. self._run_command(["sudo", "mkdir", "-p", p])
# Others might have BaseSystem.dmg directly.
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
self._report_progress("Extracting bootable HFS+ image from source DMG...") # Copy BaseSystem.dmg & BaseSystem.chunklist
if not self._extract_basesystem_hfs_from_source(source_for_hfs_extraction, self.temp_basesystem_hfs_path): bs_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=True)
raise RuntimeError("Failed to extract HFS+ image from source DMG.") bs_chunklist_src = self._find_gibmacos_asset("BaseSystem.chunklist", product_folder_path, search_deep=True)
if bs_dmg_src:
self._report_progress(f"Copying BaseSystem.dmg to USB CoreServices and App SharedSupport...")
self._run_command(["sudo", "cp", bs_dmg_src, os.path.join(coreservices_path_usb, "BaseSystem.dmg")])
self._run_command(["sudo", "cp", bs_dmg_src, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")])
if bs_chunklist_src:
self._report_progress(f"Copying BaseSystem.chunklist to USB CoreServices and App SharedSupport...")
self._run_command(["sudo", "cp", bs_chunklist_src, os.path.join(coreservices_path_usb, "BaseSystem.chunklist")])
self._run_command(["sudo", "cp", bs_chunklist_src, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")])
if not bs_dmg_src or not bs_chunklist_src: self._report_progress("Warning: BaseSystem.dmg or .chunklist not found in product folder.")
self._report_progress(f"Writing BaseSystem HFS+ image to {macos_partition_dev} using dd...") # Copy InstallInfo.plist
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"]) 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(usb_target_root, "InstallInfo.plist")])
else: self._report_progress("Warning: InstallInfo.plist (source) not found.")
self._report_progress("Mounting macOS Install partition on USB...") # Copy main installer package(s)
self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target]) main_pkg_src = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, search_deep=True) or self._find_gibmacos_asset("InstallESD.dmg", product_folder_path, search_deep=True)
if main_pkg_src:
pkg_basename = os.path.basename(main_pkg_src)
self._report_progress(f"Copying main payload '{pkg_basename}' to App SharedSupport and System Packages...")
self._run_command(["sudo", "cp", main_pkg_src, os.path.join(shared_support_path_usb_app, pkg_basename)])
self._run_command(["sudo", "cp", main_pkg_src, os.path.join(sys_install_pkgs_usb, pkg_basename)])
else: self._report_progress("Warning: Main installer package (InstallAssistant.pkg/InstallESD.dmg) not found.")
core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices") diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True)
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb]) if diag_src: self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")])
# Copy original BaseSystem.dmg and .chunklist from gibMacOS output template_boot_efi = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi")
original_bs_dmg = self._find_gibmacos_asset(["BaseSystem.dmg"], product_folder, "original BaseSystem.dmg") if os.path.exists(template_boot_efi) and os.path.getsize(template_boot_efi) > 0:
if original_bs_dmg: self._run_command(["sudo", "cp", template_boot_efi, os.path.join(coreservices_path_usb, "boot.efi")])
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg") else: self._report_progress(f"Warning: Template BOOTx64.efi for installer's boot.efi not found or empty.")
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")])
else: self._report_progress("Warning: Original BaseSystem.dmg not found in product folder to copy to CoreServices.")
install_info_src = self._find_gibmacos_asset(["InstallInfo.plist"], product_folder, "InstallInfo.plist") # Create .IAProductInfo (Simplified XML string to avoid f-string issues in tool call)
if install_info_src: ia_product_info_path = os.path.join(usb_target_root, ".IAProductInfo")
self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist") ia_content_xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plist version=\"1.0\"><dict><key>Product ID</key><string>com.apple.pkg.InstallAssistant</string><key>Product Path</key><string>" + app_bundle_name + "/Contents/SharedSupport/InstallAssistant.pkg</string></dict></plist>"
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")]) temp_ia_path = f"temp_iaproductinfo_{pid}.plist"
else: self._report_progress("Warning: InstallInfo.plist not found in product folder.") with open(temp_ia_path, "w") as f: f.write(ia_content_xml)
self._run_command(["sudo", "cp", temp_ia_path, ia_product_info_path])
# Copy Packages and other assets if os.path.exists(temp_ia_path): os.remove(temp_ia_path)
packages_target_path = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages") self._report_progress("Created .IAProductInfo.")
self._run_command(["sudo", "mkdir", "-p", packages_target_path]) self._report_progress("macOS installer assets fully copied to USB.")
# 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.")
# 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.
# --- OpenCore EFI Setup --- (same as before, but using self.temp_efi_build_dir)
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])
# --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP..."); self._run_command(["sudo", "mount", esp_dev, self.mount_point_usb_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)
else: self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
# If template is config-template.plist, rename it for enhancement if not os.path.exists(temp_config_plist_path):
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")): template_plist = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
self._run_command(["sudo", "mv", os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path]) if os.path.exists(template_plist): shutil.copy2(template_plist, temp_config_plist_path)
else: plistlib.dump({"#Comment": "Basic config by Skyscope"}, open(temp_config_plist_path, 'wb'), fmt=plistlib.PlistFormat.XML)
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path): if self.enhance_plist_enabled and enhance_config_plist:
self._report_progress("Attempting to enhance config.plist...") self._report_progress("Attempting to enhance config.plist...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement successful.") if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
else: self._report_progress("config.plist enhancement failed or had issues.") else: self._report_progress("config.plist enhancement call 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._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...")
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"]) self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"], stream_rsync_progress=True)
self._report_progress("USB Installer creation process completed successfully.") self._report_progress("USB Installer creation process completed successfully.")
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"An error occurred during USB writing: {e}\n{traceback.format_exc()}") self._report_progress(f"An error occurred during USB writing: {e}"); self._report_progress(traceback.format_exc())
return False return False
finally: finally:
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
if __name__ == '__main__': if __name__ == '__main__':
import traceback; from constants import MACOS_VERSIONS
if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1) if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1)
print("USB Writer Linux Standalone Test - Installer Method (Refined)") print("USB Writer Linux Standalone Test - Installer Method (Fuller Asset Copying Logic)")
mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
mock_download_dir = f"temp_macos_download_test_{os.getpid()}" target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
os.makedirs(mock_download_dir, exist_ok=True) 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"
# Create a more structured mock download similar to gibMacOS output specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
product_name_slug = f"000-00000 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.0" # Example os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True); os.makedirs(specific_product_folder, exist_ok=True)
specific_product_folder = os.path.join(mock_download_dir, "publicrelease", product_name_slug) with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
os.makedirs(specific_product_folder, exist_ok=True) 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)
# Mock BaseSystem.dmg (tiny, not functional, for path testing) with open(os.path.join(specific_product_folder, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024))
dummy_bs_dmg_path = os.path.join(specific_product_folder, "BaseSystem.dmg") with open(os.path.join(specific_product_folder, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024))
if not os.path.exists(dummy_bs_dmg_path): if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy 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)
# Mock BaseSystem.chunklist 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)
dummy_bs_chunklist_path = os.path.join(specific_product_folder, "BaseSystem.chunklist") with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("dummy bootx64.efi")
if not os.path.exists(dummy_bs_chunklist_path): print("\nAvailable block devices (be careful!):"); subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True)
with open(dummy_bs_chunklist_path, "w") as f: f.write("dummy chunklist")
# Mock InstallInfo.plist
dummy_installinfo_path = os.path.join(specific_product_folder, "InstallInfo.plist")
if not os.path.exists(dummy_installinfo_path):
with open(dummy_installinfo_path, "w") as f: plistlib.dump({"DummyInstallInfo": True}, f)
# Mock InstallAssistant.pkg (empty for now, just to test its presence)
dummy_pkg_path = os.path.join(specific_product_folder, "InstallAssistant.pkg")
if not os.path.exists(dummy_pkg_path):
with open(dummy_pkg_path, "wb") as f: f.write(os.urandom(1024))
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
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)
test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ") test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ")
if not test_device or not test_device.startswith("/dev/"): print("Invalid device."); shutil.rmtree(mock_download_dir); shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True); exit(1)
if not test_device or not test_device.startswith("/dev/"): if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes':
print("Invalid device. Exiting.") writer = USBWriterLinux(test_device, mock_download_dir, print, True, target_version_cli)
else: writer.format_and_write()
confirm = input(f"Are you absolutely sure you want to wipe {test_device} and create installer? (yes/NO): ") else: print("Test cancelled.")
success = False shutil.rmtree(mock_download_dir, ignore_errors=True);
if confirm.lower() == 'yes': # shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template dir for other tests
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"
)
success = writer.format_and_write()
else: print("Test cancelled by user.")
print(f"Test finished. Success: {success}")
if os.path.exists(mock_download_dir): shutil.rmtree(mock_download_dir, ignore_errors=True)
print("Mock download dir cleaned up.") print("Mock download dir cleaned up.")

View File

@ -15,38 +15,46 @@ except ImportError:
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
try:
from constants import MACOS_VERSIONS
except ImportError:
MACOS_VERSIONS = {"Sonoma": "14", "Ventura": "13", "Monterey": "12"}
print("Warning: constants.py not found, using fallback MACOS_VERSIONS for _get_gibmacos_product_folder.")
class USBWriterMacOS: class USBWriterMacOS:
def __init__(self, device: str, macos_download_path: str, def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""): target_macos_version: str = ""):
self.device = device # e.g., /dev/diskX self.device = device
self.macos_download_path = macos_download_path self.macos_download_path = macos_download_path
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version self.target_macos_version = target_macos_version
pid = os.getpid() pid = os.getpid()
self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs" # Use /tmp for macOS self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs"
self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}" self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}"
self.temp_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{pid}" # For source BaseSystem.dmg's EFI (if needed) self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}"
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.mounted_usb_esp_path = None # Will be like /Volumes/EFI
self.temp_usb_macos_target_mount = f"/tmp/usb_macos_target_temp_skyscope_{pid}" self.mounted_usb_macos_path = None # Will be like /Volumes/Install macOS ...
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" # For 7z extractions self.mounted_source_basesystem_path = f"/tmp/source_basesystem_mount_{pid}"
self.temp_files_to_clean = [self.temp_basesystem_hfs_path] self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
self.temp_dirs_to_clean = [ self.temp_dirs_to_clean = [
self.temp_efi_build_dir, self.temp_opencore_mount, self.temp_efi_build_dir, self.temp_dmg_extract_dir,
self.temp_usb_esp_mount, self.temp_macos_source_mount, self.mounted_source_basesystem_path
self.temp_usb_macos_target_mount, self.temp_dmg_extract_dir # Actual USB mount points (/Volumes/EFI, /Volumes/Install macOS...) are unmounted, not rmdir'd from here
] ]
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, is_rsync_line: bool = False):
# Simplified progress for macOS writer for now, can add rsync parsing later if needed
if self.progress_callback: self.progress_callback(message) if self.progress_callback: self.progress_callback(message)
else: print(message) else: print(message)
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False): # ... (same) def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False):
self._report_progress(f"Executing: {' '.join(command)}") self._report_progress(f"Executing: {' '.join(command)}")
try: try:
process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell) process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell)
@ -58,49 +66,84 @@ class USBWriterMacOS:
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0]}' not found."); raise except FileNotFoundError: self._report_progress(f"Error: Command '{command[0]}' not found."); raise
def _cleanup_temp_files_and_dirs(self): # Updated for macOS def _cleanup_temp_files_and_dirs(self):
self._report_progress("Cleaning up temporary files and directories...") self._report_progress("Cleaning up temporary files, directories, and mounts on macOS...")
for f_path in self.temp_files_to_clean:
if os.path.exists(f_path):
try: os.remove(f_path) # No sudo needed for /tmp files usually
except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
# Detach DMGs first # Unmount our specific /tmp mount points first
for dev_path in list(self.attached_dmg_devices): # Iterate copy if self.mounted_source_basesystem_path and os.path.ismount(self.mounted_source_basesystem_path):
self._unmount_path(self.mounted_source_basesystem_path, force=True)
# System mount points like /Volumes/EFI or /Volumes/Install macOS... are unmounted by diskutil unmountDisk or unmount
# We also add them to temp_dirs_to_clean if we used their dynamic path for rmdir later (but only if they were /tmp based)
for dev_path in list(self.attached_dmg_devices):
self._detach_dmg(dev_path) self._detach_dmg(dev_path)
self.attached_dmg_devices = [] self.attached_dmg_devices = []
for f_path in self.temp_files_to_clean:
if os.path.exists(f_path):
try: os.remove(f_path)
except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
for d_path in self.temp_dirs_to_clean: for d_path in self.temp_dirs_to_clean:
if os.path.ismount(d_path): if os.path.exists(d_path) and d_path.startswith("/tmp/"): # Only remove /tmp dirs we created
try: self._run_command(["diskutil", "unmount", "force", d_path], check=False, timeout=30)
except Exception: pass # Ignore if already unmounted or error
if os.path.exists(d_path):
try: shutil.rmtree(d_path, ignore_errors=True) try: shutil.rmtree(d_path, ignore_errors=True)
except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}") except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
def _detach_dmg(self, device_path_or_mount_point): def _unmount_path(self, mount_path_or_device, is_device=False, force=False):
if not device_path_or_mount_point: return target = mount_path_or_device
self._report_progress(f"Attempting to detach DMG associated with {device_path_or_mount_point}...") cmd_base = ["diskutil"]
try: action = "unmountDisk" if is_device else "unmount"
# hdiutil detach can take a device path or sometimes a mount path if it's unique enough cmd = cmd_base + ([action, "force", target] if force else [action, target])
# 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
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)
# Check if it's a valid target for unmount/unmountDisk
# For mount paths, check os.path.ismount. For devices, check if base device exists.
can_unmount = False
if is_device:
# Extract base disk identifier like /dev/diskX from /dev/diskXsY
base_device = re.match(r"(/dev/disk\d+)", target)
if base_device and os.path.exists(base_device.group(1)):
can_unmount = True
elif os.path.ismount(target):
can_unmount = True
if can_unmount:
self._report_progress(f"Attempting to {action} {'forcefully ' if force else ''}{target}...")
self._run_command(cmd, check=False, timeout=60) # Increased timeout for diskutil
else:
self._report_progress(f"Skipping unmount for {target}, not a valid mount point or device for this action.")
def _detach_dmg(self, device_path):
if not device_path or not device_path.startswith("/dev/disk"): return
self._report_progress(f"Attempting to detach DMG device {device_path}...")
try:
# Ensure it's actually a virtual disk from hdiutil
is_virtual_disk = False
try:
info_result = self._run_command(["diskutil", "info", "-plist", device_path], capture_output=True)
if info_result.returncode == 0 and info_result.stdout:
disk_info = plistlib.loads(info_result.stdout.encode('utf-8'))
if disk_info.get("VirtualOrPhysical") == "Virtual":
is_virtual_disk = True
except Exception: pass # Ignore parsing errors, proceed to detach attempt
if is_virtual_disk:
self._run_command(["hdiutil", "detach", device_path, "-force"], check=False, timeout=30)
else:
self._report_progress(f"{device_path} is not a virtual disk, or info check failed. Skipping direct hdiutil detach.")
if device_path in self.attached_dmg_devices:
self.attached_dmg_devices.remove(device_path)
except Exception as e: except Exception as e:
self._report_progress(f"Could not detach {device_path_or_mount_point}: {e}") self._report_progress(f"Could not detach {device_path}: {e}")
def check_dependencies(self): def check_dependencies(self):
self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd)...") self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd, bless)...")
dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"] dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd", "bless"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)] missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps: if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`)." msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`). Others are standard."
self._report_progress(msg); raise RuntimeError(msg) self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("All critical dependencies for macOS USB installer creation found.") self._report_progress("All critical dependencies for macOS USB installer creation found.")
return True return True
@ -111,149 +154,149 @@ class USBWriterMacOS:
if os.path.isdir(base_path): if os.path.isdir(base_path):
for item in os.listdir(base_path): for item in os.listdir(base_path):
item_path = os.path.join(base_path, item) item_path = os.path.join(base_path, item)
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or MACOS_VERSIONS.get(self.target_macos_version, "").lower() in item.lower()): # MACOS_VERSIONS needs to be accessible or passed if not global version_tag = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag in item.lower()):
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using base download path."); return self.macos_download_path self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None) -> str | None: def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None:
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
search_base = product_folder_path or self.macos_download_path search_base = product_folder_path or self.macos_download_path
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
for pattern in asset_patterns: for pattern in asset_patterns:
# Using iglob for efficiency if many files, but glob is fine for fewer expected matches common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"]
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True) 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: if found_files:
found_files.sort(key=lambda x: (x.count(os.sep), len(x))) found_files.sort(key=os.path.getsize, reverse=True)
self._report_progress(f"Found {pattern}: {found_files[0]}") self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
return found_files[0] return found_files[0]
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.") if search_deep:
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
if found_files_deep:
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
return found_files_deep[0]
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
return None return None
def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path
try: try:
if dmg_or_pkg_path.endswith(".pkg"): if dmg_or_pkg_path.endswith(".pkg"):
self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True) self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True); dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert dmgs_in_pkg, "No DMG in PKG."; current_target = max(dmgs_in_pkg, key=os.path.getsize, default=dmgs_in_pkg[0]); assert current_target, "No primary DMG in PKG."; self._report_progress(f"Using DMG from PKG: {current_target}")
dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}"
if not dmgs_in_pkg: raise RuntimeError("No DMG found in PKG.")
current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None) or dmgs_in_pkg[0]
if not current_target: raise RuntimeError("Could not determine primary DMG in PKG.")
self._report_progress(f"Using DMG from PKG: {current_target}")
if not current_target or not current_target.endswith(".dmg"): raise RuntimeError(f"Not a valid DMG: {current_target}")
basesystem_dmg_to_process = current_target basesystem_dmg_to_process = current_target
# If current_target is InstallESD.dmg or SharedSupport.dmg, it contains BaseSystem.dmg
if "basesystem.dmg" not in os.path.basename(current_target).lower(): if "basesystem.dmg" not in os.path.basename(current_target).lower():
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}...") self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True); found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True); assert found_bs_dmg, f"No BaseSystem.dmg from {current_target}"; basesystem_dmg_to_process = found_bs_dmg[0]
self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", 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"));
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=True) if not hfs_files: self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True); 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)) > 2*1024*1024*1024]
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}") assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}"
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)
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 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
except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
finally: finally:
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
def _create_minimal_efi_template(self, efi_dir_path):
def _create_minimal_efi_template(self, efi_dir_path): # Same as linux version 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);[os.makedirs(os.path.join(oc_dir,s),exist_ok=True) for s in ["Drivers","Kexts","ACPI","Tools","Resources"]];open(os.path.join(efi_dir_path,"EFI","BOOT","BOOTx64.efi"),"w").close();open(os.path.join(oc_dir,"OpenCore.efi"),"w").close();bc={"#Comment":"Basic config","Misc":{"Security":{"ScanPolicy":0,"SecureBootModel":"Disabled"}},"PlatformInfo":{"Generic":{"MLB":"CHANGE_ME_MLB","SystemSerialNumber":"CHANGE_ME_SERIAL","SystemUUID":"CHANGE_ME_UUID","ROM":b"\0"*6}}};plistlib.dump(bc,open(os.path.join(oc_dir,"config.plist"),'wb'),fmt=plistlib.PlistFormat.XML)
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)
with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("")
with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("")
basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}}
try:
with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML)
self._report_progress("Created basic placeholder config.plist.")
except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}")
def format_and_write(self) -> bool: def format_and_write(self) -> bool:
try: try:
self.check_dependencies() self.check_dependencies()
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
for mp_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) if not os.path.exists(mp_dir): os.makedirs(mp_dir, exist_ok=True)
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
self._run_command(["diskutil", "unmountDisk", "force", self.device], check=False, timeout=60); time.sleep(2) self._run_command(["diskutil", "unmountDisk", "force", self.device], check=False, timeout=60); time.sleep(2)
installer_vol_name = f"Install macOS {self.target_macos_version}" installer_vol_name = f"Install macOS {self.target_macos_version}"
self._report_progress(f"Partitioning {self.device} as GPT: EFI (FAT32, 551MB), '{installer_vol_name}' (HFS+)...") self._report_progress(f"Partitioning {self.device} for '{installer_vol_name}'...")
self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3) self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3)
# Get actual partition identifiers disk_info_plist_str = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
disk_info_plist = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout if not disk_info_plist_str: raise RuntimeError("Failed to get disk info after partitioning.")
if not disk_info_plist: raise RuntimeError("Failed to get disk info after partitioning.") disk_info = plistlib.loads(disk_info_plist_str.encode('utf-8'))
disk_info = plistlib.loads(disk_info_plist.encode('utf-8'))
esp_partition_dev = None; macos_partition_dev = None esp_partition_dev = None; macos_partition_dev = None
for disk_entry in disk_info.get("AllDisksAndPartitions", []): main_disk_entry = next((d for d in disk_info.get("AllDisksAndPartitions", []) if d.get("DeviceIdentifier") == self.device.replace("/dev/", "")), None)
if disk_entry.get("DeviceIdentifier") == self.device.replace("/dev/", ""): if main_disk_entry:
for part in disk_entry.get("Partitions", []): for part in main_disk_entry.get("Partitions", []):
if part.get("VolumeName") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}" if 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')}" elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}).")
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}). Check diskutil list output.")
self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}") self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}")
# --- Prepare macOS Installer Content --- product_folder_path = self._get_gibmacos_product_folder()
product_folder = self._get_gibmacos_product_folder() source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)")
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)") if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path): if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.") raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk") # Use raw device for dd raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk")
self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...") self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...")
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800) self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800)
self._report_progress(f"Mounting macOS Install partition ({macos_partition_dev}) on USB...") self.mounted_usb_macos_path = f"/Volumes/{installer_vol_name}"
if not os.path.ismount(self.mounted_usb_macos_path):
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev]) self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev])
self.mounted_usb_macos_path = self.temp_usb_macos_target_mount
core_services_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices") self._report_progress(f"macOS partition mounted at {self.mounted_usb_macos_path}")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder) usb_target_root = self.mounted_usb_macos_path
if original_bs_dmg: app_bundle_name = f"Install macOS {self.target_macos_version}.app"
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg") app_bundle_path_usb = os.path.join(usb_target_root, app_bundle_name)
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")]) contents_path_usb = os.path.join(app_bundle_path_usb, "Contents")
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist") shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport")
if os.path.exists(original_bs_chunklist): resources_path_usb_app = os.path.join(contents_path_usb, "Resources")
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist") sys_install_pkgs_usb = os.path.join(usb_target_root, "System", "Installation", "Packages")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")]) coreservices_path_usb = os.path.join(usb_target_root, "System", "Library", "CoreServices")
install_info_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder) for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]:
if install_info_src: self._run_command(["sudo", "mkdir", "-p", p])
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")])
packages_dir_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages") for f_name in ["BaseSystem.dmg", "BaseSystem.chunklist"]:
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb]) src_file = self._find_gibmacos_asset(f_name, product_folder_path, search_deep=True)
if src_file: self._run_command(["sudo", "cp", src_file, os.path.join(shared_support_path_usb_app, os.path.basename(src_file))]); self._run_command(["sudo", "cp", src_file, os.path.join(coreservices_path_usb, os.path.basename(src_file))])
else: self._report_progress(f"Warning: {f_name} not found.")
# Copy main installer package(s) or app contents. This is simplified. installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True)
# A real createinstallmedia copies the .app then uses it. We are building manually. if installinfo_src: 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(usb_target_root, "InstallInfo.plist")])
# We need to find the main payload: InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg content. else: self._report_progress("Warning: InstallInfo.plist not found.")
main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "Main Installer Payload (PKG/DMG)")
if main_payload_src: main_pkg_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True)
self._report_progress(f"Copying main payload {os.path.basename(main_payload_src)} to {packages_dir_usb}/") if main_pkg_src: pkg_basename = os.path.basename(main_pkg_src); self._run_command(["sudo", "cp", main_pkg_src, os.path.join(shared_support_path_usb_app, pkg_basename)]); self._run_command(["sudo", "cp", main_pkg_src, os.path.join(sys_install_pkgs_usb, pkg_basename)])
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb, os.path.basename(main_payload_src))]) else: self._report_progress("Warning: Main installer PKG/DMG not found.")
# 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. diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True)
else: self._report_progress("Warning: Main installer payload not found. Installer may be incomplete.") if diag_src: self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")])
template_boot_efi = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi")
if os.path.exists(template_boot_efi) and os.path.getsize(template_boot_efi) > 0: self._run_command(["sudo", "cp", template_boot_efi, os.path.join(coreservices_path_usb, "boot.efi")])
else: self._report_progress(f"Warning: Template BOOTx64.efi for installer's boot.efi not found or empty.")
ia_product_info_path = os.path.join(usb_target_root, ".IAProductInfo")
ia_content_xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plist version=\"1.0\"><dict><key>Product ID</key><string>com.apple.pkg.InstallAssistant</string><key>Product Path</key><string>" + app_bundle_name + "/Contents/SharedSupport/InstallAssistant.pkg</string></dict></plist>"
temp_ia_path = f"/tmp/temp_iaproductinfo_{pid}.plist"
with open(temp_ia_path, "w") as f: f.write(ia_content_xml)
self._run_command(["sudo", "cp", temp_ia_path, ia_product_info_path])
if os.path.exists(temp_ia_path): os.remove(temp_ia_path)
self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")])
self._report_progress("macOS installer assets copied.") self._report_progress("macOS installer assets copied.")
# --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP...") self._report_progress("Setting up OpenCore EFI on ESP...")
self.mounted_usb_esp_path = f"/Volumes/EFI" # Default mount path for ESP
if not os.path.ismount(self.mounted_usb_esp_path):
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
self.mounted_usb_esp_path = self.temp_usb_esp_mount
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir) if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir)
else: self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}"); self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) else: self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")): if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
@ -261,52 +304,59 @@ class USBWriterMacOS:
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path): if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only)...") self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only)...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.") if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement complete.")
else: self._report_progress("config.plist enhancement call failed or had issues.") else: self._report_progress("config.plist enhancement call failed or had issues.")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev]) self._report_progress(f"Copying final EFI folder to USB ESP ({self.mounted_usb_esp_path})...")
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.mounted_usb_esp_path}/EFI/"])
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"])
self._report_progress(f"Blessing the installer volume: {self.mounted_usb_macos_path}")
bless_target_folder = os.path.join(self.mounted_usb_macos_path, "System", "Library", "CoreServices")
self._run_command(["sudo", "bless", "--folder", bless_target_folder, "--label", installer_vol_name, "--setBoot"], check=False)
self._report_progress("USB Installer creation process completed successfully.") self._report_progress("USB Installer creation process completed successfully.")
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"An error occurred during USB writing on macOS: {e}\n{traceback.format_exc()}") self._report_progress(f"An error occurred during USB writing on macOS: {e}"); self._report_progress(traceback.format_exc())
return False return False
finally: finally:
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
if __name__ == '__main__': if __name__ == '__main__':
import traceback import traceback
from constants import MACOS_VERSIONS
if platform.system() != "Darwin": print("This script is intended for macOS for standalone testing."); exit(1) if platform.system() != "Darwin": print("This script is intended for macOS for standalone testing."); exit(1)
print("USB Writer macOS Standalone Test - Installer Method") print("USB Writer macOS Standalone Test - Installer Method")
mock_download_dir = f"/tmp/temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True) mock_download_dir = f"/tmp/temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
# Simulate a more realistic gibMacOS product folder structure for testing _get_gibmacos_product_folder target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
mock_product_name = f"012-34567 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.1.2" mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
mock_product_folder_path = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) mock_product_folder_path = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True) # Create SharedSupport directory os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True)
with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist")
with open(os.path.join(mock_product_folder_path, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f)
with open(os.path.join(mock_product_folder_path, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024))
with open(os.path.join(mock_product_folder_path, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024))
# Create dummy BaseSystem.dmg inside the product folder's SharedSupport if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
dummy_bs_dmg_path = os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg") if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True)
if not os.path.exists(dummy_bs_dmg_path): if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True)
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy DMG
dummy_installinfo_path = os.path.join(mock_product_folder_path, "InstallInfo.plist")
if not os.path.exists(dummy_installinfo_path):
with open(dummy_installinfo_path, "wb") as f: plistlib.dump({"DisplayName":f"macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'}"},f)
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist") dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
if not os.path.exists(dummy_config_template_path): if not os.path.exists(dummy_config_template_path):
with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f) with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f, fmt=plistlib.PlistFormat.XML)
dummy_bootx64_efi_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi")
if not os.path.exists(dummy_bootx64_efi_path):
with open(dummy_bootx64_efi_path, "w") as f: f.write("dummy bootx64.efi content")
print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False) print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False)
test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ") test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ")
if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) # No need to clean OC_TEMPLATE_DIR here if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1)
if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes': if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes':
writer = USBWriterMacOS(test_device, mock_download_dir, print, True, sys.argv[1] if len(sys.argv) > 1 else "Sonoma") writer = USBWriterMacOS(test_device, mock_download_dir, print, True, target_version_cli)
writer.format_and_write() writer.format_and_write()
else: print("Test cancelled.") else: print("Test cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True) shutil.rmtree(mock_download_dir, ignore_errors=True)
# Deliberately not cleaning OC_TEMPLATE_DIR in test, as it might be shared or pre-existing.
print("Mock download dir cleaned up.") print("Mock download dir cleaned up.")

View File

@ -1,71 +1,92 @@
# usb_writer_windows.py (Refactoring for Installer Workflow) # usb_writer_windows.py (Refining EFI setup and manual step guidance)
import subprocess import subprocess
import os import os
import time import time
import shutil import shutil
import re import re
import glob # For _find_gibmacos_asset import glob
import plistlib
import traceback import traceback
import sys # For checking psutil import import sys # Added for psutil check
# Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test
try: try:
from PyQt6.QtWidgets import QMessageBox # For user guidance from PyQt6.QtWidgets import QMessageBox
except ImportError: except ImportError:
class QMessageBox: # Mock for standalone testing # Mock QMessageBox for standalone testing or if PyQt6 is not available
class QMessageBox:
Information = 1 # Dummy enum value
Warning = 2 # Dummy enum value
Question = 3 # Dummy enum value
YesRole = 0 # Dummy role
NoRole = 1 # Dummy role
@staticmethod @staticmethod
def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'") def information(parent, title, message, buttons=None, defaultButton=None):
print(f"INFO (QMessageBox mock): Title='{title}', Message='{message}'")
return QMessageBox.Yes # Simulate a positive action if needed
@staticmethod @staticmethod
def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox def warning(parent, title, message, buttons=None, defaultButton=None):
Yes = 1 # Mock value print(f"WARNING (QMessageBox mock): Title='{title}', Message='{message}'")
No = 0 # Mock value return QMessageBox.Yes # Simulate a positive action
Cancel = 0 # Mock value @staticmethod
def critical(parent, title, message, buttons=None, defaultButton=None):
print(f"CRITICAL (QMessageBox mock): Title='{title}', Message='{message}'")
return QMessageBox.Yes # Simulate a positive action
# Add other static methods if your code uses them, e.g. question
@staticmethod
def question(parent, title, message, buttons=None, defaultButton=None):
print(f"QUESTION (QMessageBox mock): Title='{title}', Message='{message}'")
return QMessageBox.Yes # Simulate 'Yes' for testing
# Dummy button values if your code checks for specific button results
Yes = 0x00004000
No = 0x00010000
Cancel = 0x00400000
try: try:
from plist_modifier import enhance_config_plist from plist_modifier import enhance_config_plist
except ImportError: except ImportError:
enhance_config_plist = None print("Warning: plist_modifier not found. Enhancement will be skipped.")
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled.") def enhance_config_plist(plist_path, macos_version, progress_callback):
if progress_callback:
progress_callback("Skipping plist enhancement: plist_modifier not available.")
return False # Indicate failure or no action
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") # This path needs to be correct relative to where usb_writer_windows.py is, or use an absolute path strategy
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "EFI_template_installer")
class USBWriterWindows: class USBWriterWindows:
def __init__(self, device_id_str: str, macos_download_path: str, def __init__(self, device_id_str: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""): target_macos_version: str = ""):
# device_id_str is expected to be the disk number string from user, e.g., "1", "2" self.device_id_str = device_id_str
self.disk_number = "".join(filter(str.isdigit, device_id_str)) self.disk_number = "".join(filter(str.isdigit, device_id_str))
if not self.disk_number:
raise ValueError(f"Invalid device_id format: '{device_id_str}'. Must contain a disk number.")
self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}" self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}"
self.macos_download_path = macos_download_path self.macos_download_path = macos_download_path
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version self.target_macos_version = target_macos_version
pid = os.getpid() pid = os.getpid()
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs" # Use system temp for Windows more reliably
self.temp_efi_build_dir = f"temp_efi_build_{pid}" self.temp_dir_base = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_usb_temp_{pid}")
self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For 7z extractions self.temp_basesystem_hfs_path = os.path.join(self.temp_dir_base, f"temp_basesystem_{pid}.hfs")
self.temp_efi_build_dir = os.path.join(self.temp_dir_base, f"temp_efi_build_{pid}")
self.temp_dmg_extract_dir = os.path.join(self.temp_dir_base, f"temp_dmg_extract_{pid}")
self.temp_files_to_clean = [self.temp_basesystem_hfs_path] # Specific files outside temp_dir_base (if any)
self.temp_files_to_clean = [self.temp_basesystem_hfs_path] self.temp_dirs_to_clean = [self.temp_dir_base] # Base temp dir for this instance
self.temp_dirs_to_clean = [self.temp_efi_build_dir, self.temp_dmg_extract_dir]
self.assigned_efi_letter = None self.assigned_efi_letter = None
def _report_progress(self, message: str): def _report_progress(self, message: str):
if self.progress_callback: self.progress_callback(message) if self.progress_callback: self.progress_callback(message)
else: print(message) else: print(message)
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None): def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None, creationflags=0):
self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}") self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}")
try: try:
process = subprocess.run( process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=creationflags)
command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir,
creationflags=subprocess.CREATE_NO_WINDOW
)
if capture_output: if capture_output:
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}") if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}") if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
@ -74,262 +95,529 @@ class USBWriterWindows:
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise
def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None: def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None:
script_file_path = f"diskpart_script_{os.getpid()}.txt"; output_text = "" script_file_path = os.path.join(self.temp_dir_base, f"diskpart_script_{os.getpid()}.txt")
with open(script_file_path, "w") as f: f.write(script_content) os.makedirs(self.temp_dir_base, exist_ok=True)
output_text = None
try: try:
self._report_progress(f"Running diskpart script:\n{script_content}") self._report_progress(f"Running diskpart script:\n{script_content}")
process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False) with open(script_file_path, "w") as f: f.write(script_content)
# Use CREATE_NO_WINDOW for subprocess.run with diskpart
process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False, creationflags=subprocess.CREATE_NO_WINDOW)
output_text = (process.stdout or "") + "\n" + (process.stderr or "") output_text = (process.stdout or "") + "\n" + (process.stderr or "")
success_indicators = ["DiskPart successfully", "successfully completed", "succeeded in creating", "successfully formatted", "successfully assigned"]
has_success_indicator = any(indicator in output_text for indicator in success_indicators)
has_error_indicator = "Virtual Disk Service error" in output_text or "DiskPart has encountered an error" in output_text
if has_error_indicator:
self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}")
elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text :
self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}")
if capture_output_for_parse: return output_text if capture_output_for_parse: return output_text
finally: finally:
if os.path.exists(script_file_path): os.remove(script_file_path) if os.path.exists(script_file_path):
return output_text if capture_output_for_parse else None try: os.remove(script_file_path)
except OSError as e: self._report_progress(f"Warning: Could not remove temp diskpart script {script_file_path}: {e}")
return None # Explicitly return None if not capturing for parse or if it fails before return
def _cleanup_temp_files_and_dirs(self): def _cleanup_temp_files_and_dirs(self):
self._report_progress("Cleaning up...") self._report_progress("Cleaning up temporary files and directories on Windows...")
for f_path in self.temp_files_to_clean: for f_path in self.temp_files_to_clean:
if os.path.exists(f_path): if os.path.exists(f_path):
try: os.remove(f_path) try: os.remove(f_path)
except Exception as e: self._report_progress(f"Could not remove temp file {f_path}: {e}") except OSError as e: self._report_progress(f"Error removing file {f_path}: {e}")
for d_path in self.temp_dirs_to_clean:
for d_path in self.temp_dirs_to_clean: # self.temp_dir_base is the main one
if os.path.exists(d_path): if os.path.exists(d_path):
try: shutil.rmtree(d_path, ignore_errors=True) try: shutil.rmtree(d_path, ignore_errors=False) # Try with ignore_errors=False first
except Exception as e: self._report_progress(f"Could not remove temp dir {d_path}: {e}") except OSError as e:
self._report_progress(f"Error removing dir {d_path}: {e}. Attempting force remove.")
try: shutil.rmtree(d_path, ignore_errors=True) # Fallback to ignore_errors=True
except OSError as e_force: self._report_progress(f"Force remove for dir {d_path} also failed: {e_force}")
def _find_available_drive_letter(self) -> str | None: def _find_available_drive_letter(self) -> str | None:
import string; used_letters = set() import string
used_letters = set()
try: try:
if 'psutil' in sys.modules: # Check if psutil was imported by main app # Try to use psutil if available (e.g., when run from main_app.py)
partitions = sys.modules['psutil'].disk_partitions(all=True) if 'psutil' in sys.modules:
import psutil # Ensure it's imported here if check passes
partitions = psutil.disk_partitions(all=True)
for p in partitions: for p in partitions:
if p.mountpoint and len(p.mountpoint) >= 2 and p.mountpoint[1] == ':': # Check for "X:" if p.mountpoint and len(p.mountpoint) == 2 and p.mountpoint[1] == ':':
used_letters.add(p.mountpoint[0].upper()) used_letters.add(p.mountpoint[0].upper())
except Exception as e: else: # Fallback if psutil is not available (e.g. pure standalone script)
self._report_progress(f"Could not list used drive letters with psutil: {e}. Will try common letters.") self._report_progress("psutil not available, using limited drive letter detection.")
# Basic check, might not be exhaustive
for letter in string.ascii_uppercase[3:]: # D onwards
if os.path.exists(f"{letter}:\\"):
used_letters.add(letter)
except Exception as e:
self._report_progress(f"Error detecting used drive letters: {e}. Proceeding with caution.")
# Prefer letters from S onwards, less likely to conflict with user drives
for letter in "STUVWXYZGHIJKLMNOPQR": for letter in "STUVWXYZGHIJKLMNOPQR":
if letter not in used_letters and letter > 'D': # Avoid A, B, C, D if letter not in used_letters and letter > 'C': # Ensure it's not A, B, C
return letter return letter
return None return None
def check_dependencies(self): def check_dependencies(self):
self._report_progress("Checking dependencies (diskpart, robocopy, 7z, dd for Windows [manual check])...") self._report_progress("Checking dependencies (diskpart, robocopy, 7z, dd for Windows [manual check])...")
dependencies = ["diskpart", "robocopy", "7z"] dependencies = ["diskpart", "robocopy", "7z"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)] missing = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps: if missing:
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)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip) needs to be installed and its directory added to the system PATH."
self._report_progress(msg); raise RuntimeError(msg) self._report_progress(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.") raise RuntimeError(msg)
self._report_progress("Please ensure a 'dd for Windows' utility (e.g., from SUSE, Cygwin, or http://www.chrysocome.net/dd) is installed and accessible from your PATH for writing the main macOS BaseSystem image.")
return True return True
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None) -> str | None: def _find_gibmacos_asset(self, asset_name: str, product_folder_path: str | None = None, search_deep=True) -> str | None:
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] search_locations = []
search_base = product_folder_path or self.macos_download_path if product_folder_path and os.path.isdir(product_folder_path):
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") search_locations.extend([product_folder_path, os.path.join(product_folder_path, "SharedSupport")])
for pattern in asset_patterns:
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True) # Also search directly in macos_download_path and a potential "macOS Install Data" subdirectory
search_locations.extend([self.macos_download_path, os.path.join(self.macos_download_path, "macOS Install Data")])
# If a version-specific folder exists at the root of macos_download_path (less common for gibMacOS structure)
if os.path.isdir(self.macos_download_path):
for item in os.listdir(self.macos_download_path):
item_path = os.path.join(self.macos_download_path, item)
if os.path.isdir(item_path) and self.target_macos_version.lower() in item.lower():
search_locations.append(item_path)
search_locations.append(os.path.join(item_path, "SharedSupport"))
# Assuming first match is good enough for this heuristic
break
# Deduplicate search locations while preserving order (Python 3.7+)
search_locations = list(dict.fromkeys(search_locations))
for loc in search_locations:
if not os.path.isdir(loc): continue
path = os.path.join(loc, asset_name)
if os.path.exists(path):
self._report_progress(f"Found '{asset_name}' at: {path}")
return path
# Case-insensitive glob as fallback for direct name match
# Create a pattern like "[bB][aA][sS][eE][sS][yY][sS][tT][eE][mM].[dD][mM][gG]"
pattern_parts = [f"[{c.lower()}{c.upper()}]" if c.isalpha() else re.escape(c) for c in asset_name]
insensitive_glob_pattern = "".join(pattern_parts)
found_files = glob.glob(os.path.join(loc, insensitive_glob_pattern), recursive=False)
if found_files: if found_files:
found_files.sort(key=lambda x: (x.count(os.sep), len(x))) self._report_progress(f"Found '{asset_name}' via case-insensitive glob at: {found_files[0]}")
self._report_progress(f"Found {pattern}: {found_files[0]}")
return found_files[0] return found_files[0]
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.")
if search_deep:
self._report_progress(f"Asset '{asset_name}' not found in primary locations, starting deep search in {self.macos_download_path}...")
deep_search_pattern = os.path.join(self.macos_download_path, "**", asset_name)
# Sort by length to prefer shallower paths, then alphabetically
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=lambda p: (len(os.path.dirname(p)), p))
if found_files_deep:
self._report_progress(f"Found '{asset_name}' via deep search at: {found_files_deep[0]}")
return found_files_deep[0]
self._report_progress(f"Warning: Asset '{asset_name}' not found.")
return None return None
def _get_gibmacos_product_folder(self) -> str | None: def _get_gibmacos_product_folder(self) -> str | None:
from constants import MACOS_VERSIONS # Import for this method # constants.py should be in the same directory or Python path
try: from constants import MACOS_VERSIONS
except ImportError: MACOS_VERSIONS = {} ; self._report_progress("Warning: MACOS_VERSIONS from constants.py not loaded.")
# Standard gibMacOS download structure: macOS Downloads/publicrelease/012-34567 - macOS Sonoma 14.0
base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease") base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease")
if not os.path.isdir(base_path): base_path = self.macos_download_path if not os.path.isdir(base_path):
# Fallback if "macOS Downloads/publicrelease" is not present, use macos_download_path directly
base_path = self.macos_download_path
if os.path.isdir(base_path): if os.path.isdir(base_path):
potential_folders = []
for item in os.listdir(base_path): for item in os.listdir(base_path):
item_path = os.path.join(base_path, item) item_path = os.path.join(base_path, item)
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or MACOS_VERSIONS.get(self.target_macos_version, "").lower() in item.lower()): # Check if it's a directory and matches target_macos_version (name or tag)
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version.lower().replace(" ", ""))
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 if os.path.isdir(item_path) and \
(self.target_macos_version.lower() in item.lower() or \
version_tag_from_constants.lower() in item.lower().replace(" ", "")):
potential_folders.append(item_path)
if potential_folders:
# Sort by length (prefer shorter, more direct matches) or other heuristics if needed
best_match = min(potential_folders, key=len)
self._report_progress(f"Identified gibMacOS product folder: {best_match}")
return best_match
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 # Fallback to the root download path
def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path temp_extract_dir = self.temp_dmg_extract_dir
os.makedirs(temp_extract_dir, exist_ok=True)
current_target = dmg_or_pkg_path
try: try:
if dmg_or_pkg_path.endswith(".pkg"): if not os.path.exists(current_target):
self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True) self._report_progress(f"Error: Input file for HFS extraction does not exist: {current_target}"); return False
dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg"));
if not dmgs_in_pkg: raise RuntimeError("No DMG found in PKG.") # Step 1: If it's a PKG, extract DMGs from it.
current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None) or dmgs_in_pkg[0] if dmg_or_pkg_path.lower().endswith(".pkg"):
if not current_target: raise RuntimeError("Could not determine primary DMG in PKG.") self._report_progress(f"Extracting DMG(s) from PKG: {current_target} using 7z...")
# Using 'e' to extract flat, '-txar' for PKG/XAR format.
self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{temp_extract_dir}", "-y"], check=True)
dmgs_in_pkg = glob.glob(os.path.join(temp_extract_dir, "*.dmg"))
if not dmgs_in_pkg: self._report_progress(f"No DMG files found after extracting PKG: {current_target}"); return False
# Select the largest DMG, assuming it's the main one.
current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None)
if not current_target: self._report_progress("Failed to select a DMG from PKG contents."); return False
self._report_progress(f"Using DMG from PKG: {current_target}") self._report_progress(f"Using DMG from PKG: {current_target}")
if not current_target or not current_target.endswith(".dmg"): raise RuntimeError(f"Not a valid DMG: {current_target}")
# Step 2: Ensure we have a DMG file.
if not current_target or not current_target.lower().endswith(".dmg"):
self._report_progress(f"Not a valid DMG file for HFS extraction: {current_target}"); return False
basesystem_dmg_to_process = current_target basesystem_dmg_to_process = current_target
# Step 3: If the DMG is not BaseSystem.dmg, try to extract BaseSystem.dmg from it.
# This handles cases like SharedSupport.dmg containing BaseSystem.dmg.
if "basesystem.dmg" not in os.path.basename(current_target).lower(): if "basesystem.dmg" not in os.path.basename(current_target).lower():
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True) self._report_progress(f"Extracting BaseSystem.dmg from container DMG: {current_target} using 7z...")
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=True) # Extract recursively, looking for any path that includes BaseSystem.dmg
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}") self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{temp_extract_dir}", "-y"], check=True)
basesystem_dmg_to_process = found_bs_dmg[0] found_bs_dmg_list = glob.glob(os.path.join(temp_extract_dir, "**", "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmg_list: self._report_progress(f"No BaseSystem.dmg found within {current_target}"); return False
basesystem_dmg_to_process = max(found_bs_dmg_list, key=os.path.getsize, default=None) # Largest if multiple
if not basesystem_dmg_to_process: self._report_progress("Failed to select BaseSystem.dmg from container."); return False
self._report_progress(f"Processing extracted BaseSystem.dmg: {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) # Step 4: Extract HFS partition image from BaseSystem.dmg.
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process} using 7z...")
if not hfs_files: # Using 'e' to extract flat, '-tdmg' for DMG format. Looking for '*.hfs' or specific partition files.
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True) # Try extracting all files # Common HFS file names inside BaseSystem.dmg are like '2.hfs' or similar.
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] # Sometimes they don't have .hfs extension, 7z might list them by index.
# We will try to extract any .hfs file.
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{temp_extract_dir}", "-y"], check=True)
hfs_files = glob.glob(os.path.join(temp_extract_dir, "*.hfs"))
if not hfs_files: raise RuntimeError(f"No suitable .hfs image found after extracting {basesystem_dmg_to_process}") if not hfs_files: # If no .hfs, try extracting by common partition indices if 7z supports listing them for DMG
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 self._report_progress("No direct '*.hfs' found. Attempting extraction of common HFS partition by index (e.g., '2', '3')...")
except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False # This is more complex as 7z CLI might not easily allow extracting by index directly without listing first.
finally: # For now, we rely on .hfs existing. If this fails, user might need to extract manually with 7z GUI.
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) # A more robust solution would involve listing contents and then extracting the correct file.
self._report_progress("Extraction by index is not implemented. Please ensure BaseSystem.dmg contains a directly extractable .hfs file.")
return False
def _create_minimal_efi_template(self, efi_dir_path): if not hfs_files: self._report_progress(f"No HFS files found after extracting DMG: {basesystem_dmg_to_process}"); return False
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) final_hfs_file = max(hfs_files, key=os.path.getsize, default=None) # Largest HFS file
for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True) if not final_hfs_file: self._report_progress("Failed to select HFS file."); return False
with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("")
with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("") self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}")
basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}} shutil.move(final_hfs_file, output_hfs_path)
return True
except Exception as e:
self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
def _create_minimal_efi_template_content(self, efi_dir_path_root):
self._report_progress(f"Minimal EFI template directory '{OC_TEMPLATE_DIR}' not found or is empty. Creating basic structure at {efi_dir_path_root}")
efi_path = os.path.join(efi_dir_path_root, "EFI")
oc_dir = os.path.join(efi_path, "OC")
os.makedirs(os.path.join(efi_path, "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)
# Create dummy BOOTx64.efi and OpenCore.efi
with open(os.path.join(efi_path, "BOOT", "BOOTx64.efi"), "w") as f: f.write("Minimal Boot")
with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("Minimal OC")
# Create a very basic config.plist
basic_config = {
"#WARNING": "This is a minimal config.plist. Replace with a full one for booting macOS!",
"Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}},
"PlatformInfo": {"Generic": {"MLB": "CHANGE_ME_MLB", "SystemSerialNumber": "CHANGE_ME_SERIAL", "SystemUUID": "CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}},
"NVRAM": {"Add": {"4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14": {"DefaultBackgroundColor": "00000000", "UIScale": "01"}}}, # Basic NVRAM
"UEFI": {"Drivers": ["OpenRuntime.efi"], "Input": {"KeySupport": True}} # Example
}
config_plist_path = os.path.join(oc_dir, "config.plist")
try: try:
with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML) with open(config_plist_path, 'wb') as fp:
self._report_progress("Created basic placeholder config.plist.") plistlib.dump(basic_config, fp, fmt=plistlib.PlistFormat.XML)
except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}") self._report_progress(f"Created minimal config.plist at {config_plist_path}")
except Exception as e:
self._report_progress(f"Error creating minimal config.plist: {e}")
def format_and_write(self) -> bool: def format_and_write(self) -> bool:
try: try:
self.check_dependencies() self.check_dependencies()
self._cleanup_temp_files_and_dirs() if os.path.exists(self.temp_dir_base):
os.makedirs(self.temp_efi_build_dir, exist_ok=True) self._report_progress(f"Cleaning up existing temp base directory: {self.temp_dir_base}")
shutil.rmtree(self.temp_dir_base, ignore_errors=True)
os.makedirs(self.temp_dir_base, exist_ok=True)
os.makedirs(self.temp_efi_build_dir, exist_ok=True) # For building EFI contents before copy
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True) # For 7z extractions
self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!") self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!")
# Optional: Add a QMessageBox.question here for final confirmation in GUI mode
self.assigned_efi_letter = self._find_available_drive_letter() self.assigned_efi_letter = self._find_available_drive_letter()
if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.") if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.")
self._report_progress(f"Will assign letter {self.assigned_efi_letter}: to EFI partition.") self._report_progress(f"Will attempt to assign letter {self.assigned_efi_letter}: to EFI partition.")
installer_vol_label = f"Install macOS {self.target_macos_version}"
# Ensure label for diskpart is max 32 chars for FAT32. "Install macOS Monterey" is 23 chars.
diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n" diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n"
diskpart_script_part1 += f"create partition efi size=550 label=\"EFI\"\nformat fs=fat32 quick\nassign letter={self.assigned_efi_letter}\n" # Assign after format # Create EFI (ESP) partition, 550MB is generous and common
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\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n"
# Create main macOS partition (HFS+). Let diskpart use remaining space.
# AF00 is Apple HFS+ type GUID. For APFS, it's 7C3457EF-0000-11AA-AA11-00306543ECAC
# We create as HFS+ because BaseSystem is HFS+. Installer will convert if needed.
diskpart_script_part1 += f"create partition primary label=\"{installer_vol_label[:31]}\" id=AF00\nexit\n"
self._run_diskpart_script(diskpart_script_part1) self._run_diskpart_script(diskpart_script_part1)
time.sleep(5) self._report_progress("Disk partitioning complete. Waiting for volumes to settle...")
time.sleep(5) # Give Windows time to recognize new partitions
macos_partition_offset_str = "Offset not determined by diskpart" macos_partition_number_str = "2 (typically)"; macos_partition_offset_str = "Offset not automatically determined for Windows dd"
macos_partition_number_str = "2 (assumed)" try:
# Attempt to get partition details. This is informational.
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n" diskpart_script_detail = f"select disk {self.disk_number}\nlist partition\nexit\n"
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True) detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
if detail_output: if detail_output:
self._report_progress(f"Detail Partition Output:\n{detail_output}") # Try to find Partition 2, assuming it's our target HFS+ partition
offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE) part_match = re.search(r"Partition 2\s+Primary\s+\d+\s+[GMK]B\s+(\d+)\s+[GMK]B", detail_output, re.IGNORECASE)
if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)" if part_match:
macos_partition_offset_str = f"{part_match.group(1)} MB (approx. from start of disk for Partition 2)"
else: # Fallback if specific regex fails
self._report_progress("Could not parse partition 2 offset, using generic message.")
except Exception as e:
self._report_progress(f"Could not get detailed partition info from diskpart: {e}")
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}")
# --- OpenCore EFI Setup --- # --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP...") self._report_progress("Setting up OpenCore EFI on ESP...")
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir) if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR):
self._report_progress(f"EFI_template_installer at '{OC_TEMPLATE_DIR}' is missing or empty.")
self._create_minimal_efi_template_content(self.temp_efi_build_dir) # Create in temp_efi_build_dir
else: else:
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}") self._report_progress(f"Copying 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)
shutil.copytree(OC_TEMPLATE_DIR, self.temp_efi_build_dir, dirs_exist_ok=True) shutil.copytree(OC_TEMPLATE_DIR, self.temp_efi_build_dir, dirs_exist_ok=True)
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
if not os.path.exists(temp_config_plist_path): if not os.path.exists(temp_config_plist_path):
template_plist_src = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") # Name used in prior step template_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
if os.path.exists(template_plist_src): shutil.copy2(template_plist_src, temp_config_plist_path) if os.path.exists(template_plist_path):
else: self._create_minimal_efi_template(self.temp_efi_build_dir) # Fallback to create basic if template also missing self._report_progress(f"Using template config: {template_plist_path}")
shutil.copy2(template_plist_path, temp_config_plist_path)
else:
self._report_progress("No config.plist or config-template.plist found in EFI template. Creating a minimal one.")
plistlib.dump({"#Comment": "Minimal config by Skyscope - REPLACE ME", "PlatformInfo": {"Generic": {"MLB": "CHANGE_ME"}}},
open(temp_config_plist_path, 'wb'), fmt=plistlib.PlistFormat.XML)
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path): if self.enhance_plist_enabled and enhance_config_plist: # Check if function exists
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only for this feature)...") self._report_progress("Attempting to enhance config.plist (note: hardware detection for enhancement is primarily Linux-based)...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.") if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress):
else: self._report_progress("config.plist enhancement call failed or had issues.") self._report_progress("config.plist enhancement process complete.")
else:
self._report_progress("config.plist enhancement process failed or had issues (this is expected on Windows for hardware-specifics).")
target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\" target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\"
if not os.path.exists(target_efi_on_usb_root): # Wait and check again # Ensure the assigned drive letter is actually available before robocopy
time.sleep(3)
if not os.path.exists(target_efi_on_usb_root): 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(3) # Extra wait
if not os.path.exists(target_efi_on_usb_root):
raise RuntimeError(f"EFI partition {target_efi_on_usb_root} not accessible after formatting and assignment.")
self._report_progress(f"Copying final EFI folder to USB ESP ({target_efi_on_usb_root})...") self._report_progress(f"Copying final EFI folder from {os.path.join(self.temp_efi_build_dir, 'EFI')} to USB ESP ({target_efi_on_usb_root}EFI)...")
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) # Using robocopy: /E for subdirs (incl. empty), /S for non-empty, /NFL no file list, /NDL no dir list, /NJH no job header, /NJS no job summary, /NC no class, /NS no size, /NP no progress
# /MT:8 for multithreading (default is 8, can be 1-128)
self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), os.path.join(target_efi_on_usb_root, "EFI"), "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/MT:8", "/R:3", "/W:5"], check=True)
self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}") self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}")
# --- Prepare BaseSystem --- # --- Prepare BaseSystem HFS Image ---
self._report_progress("Locating BaseSystem image from downloaded assets...") self._report_progress("Locating BaseSystem image (DMG or PKG containing it) from downloaded assets...")
product_folder_path = self._get_gibmacos_product_folder() product_folder_path = self._get_gibmacos_product_folder()
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)") basesystem_source_dmg_or_pkg = (
if not source_for_hfs_extraction: source_for_hfs_extraction = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, "InstallAssistant.pkg as BaseSystem source") self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path) or
if not source_for_hfs_extraction: raise RuntimeError("Could not find BaseSystem.dmg, InstallESD.dmg, SharedSupport.dmg or InstallAssistant.pkg.") self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path) or # Common for newer macOS
self._find_gibmacos_asset("SharedSupport.dmg", product_folder_path) # Older fallback
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)
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."
) )
self._report_progress(f"GUIDANCE:\n{guidance_message}") if not basesystem_source_dmg_or_pkg:
QMessageBox.information(None, "Manual Steps Required for Windows USB", guidance_message) # Ensure QMessageBox is available or mocked # Last resort: search for any large PKG file as it might be the installer
if product_folder_path:
pkgs = glob.glob(os.path.join(product_folder_path, "*.pkg")) + glob.glob(os.path.join(product_folder_path, "SharedSupport", "*.pkg"))
if pkgs: basesystem_source_dmg_or_pkg = max(pkgs, key=os.path.getsize, default=None)
if not basesystem_source_dmg_or_pkg:
raise RuntimeError("Could not find BaseSystem.dmg, InstallAssistant.pkg, or SharedSupport.dmg in expected locations.")
self._report_progress("Windows USB installer preparation (EFI automated, macOS content manual guidance provided) initiated.") self._report_progress(f"Selected source for HFS extraction: {basesystem_source_dmg_or_pkg}")
if not self._extract_hfs_from_dmg_or_pkg(basesystem_source_dmg_or_pkg, self.temp_basesystem_hfs_path):
raise RuntimeError(f"Failed to extract HFS+ image from '{basesystem_source_dmg_or_pkg}'. Check 7z output above.")
# --- Guidance for Manual Steps ---
abs_hfs_path_win = os.path.abspath(self.temp_basesystem_hfs_path).replace("/", "\\")
abs_download_path_win = os.path.abspath(self.macos_download_path).replace("/", "\\")
physical_drive_path_win = self.physical_drive_path # Already has escaped backslashes for \\.\
# Try to find specific assets for better guidance
install_info_plist_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=False) or "InstallInfo.plist (find in product folder)"
basesystem_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=False) or "BaseSystem.dmg"
basesystem_chunklist_src = self._find_gibmacos_asset("BaseSystem.chunklist", product_folder_path, search_deep=False) or "BaseSystem.chunklist"
main_installer_pkg_src = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, search_deep=False) or \
self._find_gibmacos_asset("InstallESD.dmg", product_folder_path, search_deep=False) or \
"InstallAssistant.pkg OR InstallESD.dmg (main installer package)"
apple_diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=False) or "AppleDiagnostics.dmg (if present)"
guidance_message = (
f"AUTOMATED EFI SETUP COMPLETE on drive {self.assigned_efi_letter}: (USB partition 1).\n"
f"TEMPORARY BaseSystem HFS image prepared at: '{abs_hfs_path_win}'.\n\n"
f"MANUAL STEPS REQUIRED FOR MAIN macOS PARTITION (USB partition {macos_partition_number_str} - '{installer_vol_label}'):\n"
f"TARGET DISK: Disk {self.disk_number} ({physical_drive_path_win})\n"
f"TARGET PARTITION FOR HFS+ CONTENT: Partition {macos_partition_number_str} (Offset from disk start: {macos_partition_offset_str}).\n\n"
f"1. WRITE BaseSystem IMAGE:\n"
f" You MUST use a 'dd for Windows' utility. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
f" Example command (VERIFY SYNTAX & TARGETS for YOUR dd tool! Incorrect use can WIPE OTHER DRIVES!):\n"
f" `dd if=\"{abs_hfs_path_win}\" of={physical_drive_path_win} bs=8M --progress` (if targeting whole disk with offset for partition 2)\n"
f" OR (if your dd supports writing directly to a partition by its number/offset, less common for \\\\.\\PhysicalDrive targets):\n"
f" `dd if=\"{abs_hfs_path_win}\" of=\\\\?\\Volume{{GUID_OF_PARTITION_2}}\ bs=8M --progress` (more complex to get GUID)\n"
f" It's often SAFER to write to the whole physical drive path ({physical_drive_path_win}) if your `dd` version calculates offsets correctly or if you specify the exact starting sector/byte offset for partition 2.\n"
f" The BaseSystem HFS image is approx. {os.path.getsize(self.temp_basesystem_hfs_path)/(1024*1024):.2f} MB.\n\n"
f"2. COPY OTHER INSTALLER FILES (CRITICAL FOR OFFLINE INSTALLER):\n"
f" After `dd`-ing BaseSystem.hfs, the '{installer_vol_label}' partition on the USB needs more files from your download path: '{abs_download_path_win}'.\n"
f" This requires a tool that can WRITE to HFS+ partitions from Windows (e.g., TransMac, Paragon HFS+ for Windows, HFSExplorer with write capabilities if any), OR perform this step on macOS/Linux.\n\n"
f" KEY FILES/FOLDERS TO COPY from '{abs_download_path_win}' (likely within a subfolder named like '{os.path.basename(product_folder_path if product_folder_path else '')}') to the ROOT of the '{installer_vol_label}' USB partition:\n"
f" a. Create folder: `Install macOS {self.target_macos_version}.app` (this is a directory)\n"
f" b. Copy '{os.path.basename(install_info_plist_src)}' to the root of '{installer_vol_label}' partition.\n"
f" c. Copy '{os.path.basename(basesystem_dmg_src)}' AND '{os.path.basename(basesystem_chunklist_src)}' into: `System/Library/CoreServices/` (on '{installer_vol_label}')\n"
f" d. Copy '{os.path.basename(main_installer_pkg_src)}' into: `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/`\n"
f" (Alternatively, for older macOS, sometimes into: `System/Installation/Packages/`)\n"
f" e. Copy '{os.path.basename(apple_diag_src)}' (if found) into: `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/` (or a similar recovery/diagnostics path if known for your version).\n"
f" f. Ensure `boot.efi` (from the OpenCore EFI, often copied from `usr/standalone/i386/boot.efi` inside BaseSystem.dmg or similar) is placed at `System/Library/CoreServices/boot.efi` on the '{installer_vol_label}' partition. (Your EFI setup on partition 1 handles OpenCore booting, this is for the macOS installer itself).\n\n"
f"3. (Optional but Recommended) Create `.IAProductInfo` file at the root of the '{installer_vol_label}' partition. This file is a symlink to `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/InstallInfo.plist` in real installers. On Windows, you may need to copy the `InstallInfo.plist` to this location as well if symlinks are hard.\n\n"
"IMPORTANT:\n"
"- Without step 2 (copying additional assets), the USB will likely NOT work as a full offline installer and may only offer Internet Recovery (if OpenCore is correctly configured for network access).\n"
"- The temporary BaseSystem HFS image at '{abs_hfs_path_win}' will be DELETED when you close this program or this message.\n"
)
self._report_progress(f"GUIDANCE FOR MANUAL STEPS:\n{guidance_message}")
# Use the QMessageBox mock or actual if available
QMessageBox.information(None, f"Manual Steps Required for Windows USB - {self.target_macos_version}", guidance_message)
self._report_progress("Windows USB installer preparation (EFI automated, macOS content requires manual steps as detailed).")
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"Error during Windows USB writing: {e}"); self._report_progress(traceback.format_exc()) self._report_progress(f"FATAL ERROR during Windows USB writing: {e}"); self._report_progress(traceback.format_exc())
# Show error in QMessageBox as well if possible
QMessageBox.critical(None, "USB Writing Failed", f"An error occurred: {e}\n\n{traceback.format_exc()}")
return False return False
finally: finally:
if self.assigned_efi_letter: if self.assigned_efi_letter:
self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit") self._report_progress(f"Attempting to remove drive letter assignment for {self.assigned_efi_letter}:")
# Run silently, don't check for errors as it's cleanup
self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit", capture_output_for_parse=False)
# Cleanup of self.temp_dir_base will handle all sub-temp-dirs and files within it.
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
self._report_progress("Temporary files cleanup attempted.")
# Standalone test block
if __name__ == '__main__': if __name__ == '__main__':
import traceback import platform
from constants import MACOS_VERSIONS # Needed for _get_gibmacos_product_folder if platform.system() != "Windows":
if platform.system() != "Windows": print("This script is for Windows standalone testing."); exit(1) print("This script's standalone test mode is intended for Windows.")
# sys.exit(1) # Use sys.exit for proper exit codes
print("USB Writer Windows Standalone Test - Installer Method Guidance") print("USB Writer Windows Standalone Test - Installer Method Guidance")
mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma" # Mock constants if not available (e.g. running totally standalone)
mock_product_name = f"000-00000 - macOS {target_version_cli} 14.x.x" try: from constants import MACOS_VERSIONS
except ImportError: MACOS_VERSIONS = {"Sonoma": "sonoma", "Ventura": "ventura"} ; print("Mocked MACOS_VERSIONS")
pid_test = os.getpid()
# Create a unique temp directory for this test run to avoid conflicts
# Place it in user's Temp for better behavior on Windows
test_run_temp_dir = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_test_run_{pid_test}")
os.makedirs(test_run_temp_dir, exist_ok=True)
# Mock download directory structure within the test_run_temp_dir
mock_download_dir = os.path.join(test_run_temp_dir, "mock_macos_downloads")
os.makedirs(mock_download_dir, exist_ok=True)
# Example: Sonoma. More versions could be added for thorough testing.
target_version_test = "Sonoma"
version_tag_test = MACOS_VERSIONS.get(target_version_test, target_version_test.lower())
mock_product_name = f"012-34567 - macOS {target_version_test} 14.1" # Example name
mock_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) 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) mock_shared_support = os.path.join(mock_product_folder, "SharedSupport")
with open(os.path.join(mock_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg") os.makedirs(mock_shared_support, exist_ok=True)
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR) # Create dummy files that would be found by _find_gibmacos_asset and _extract_hfs_from_dmg_or_pkg
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")) # 1. Dummy InstallAssistant.pkg (which contains BaseSystem.dmg)
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) dummy_pkg_path = os.path.join(mock_product_folder, "InstallAssistant.pkg")
with open(dummy_pkg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy PKG
# For the _extract_hfs_from_dmg_or_pkg to work with 7z, it needs a real archive.
# This test won't actually run 7z unless 7z is installed and the dummy files are valid archives.
# The focus here is testing the script logic, not 7z itself.
# So, we'll also create a dummy extracted BaseSystem.hfs for the guidance part.
disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). WIPES DISK: ") # 2. Dummy files for the guidance message (these would normally be in mock_product_folder or mock_shared_support)
if not disk_id_input.isdigit(): print("Invalid disk number."); exit(1) with open(os.path.join(mock_product_folder, "InstallInfo.plist"), "w") as f: f.write("<plist><dict></dict></plist>")
with open(os.path.join(mock_shared_support, "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(5*1024*1024)) # Dummy DMG
with open(os.path.join(mock_shared_support, "BaseSystem.chunklist"), "w") as f: f.write("chunklist content")
# AppleDiagnostics.dmg is optional
with open(os.path.join(mock_shared_support, "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1*1024*1024))
if input(f"Sure to wipe disk {disk_id_input}? (yes/NO): ").lower() == 'yes':
writer = USBWriterWindows(disk_id_input, mock_download_dir, print, True, target_version_cli) # Ensure OC_TEMPLATE_DIR (EFI_template_installer) exists for the test or use the minimal creation.
# Relative path from usb_writer_windows.py to EFI_template_installer
abs_oc_template_dir = OC_TEMPLATE_DIR
if not os.path.exists(abs_oc_template_dir):
print(f"Warning: Test OC_TEMPLATE_DIR '{abs_oc_template_dir}' not found. Minimal EFI will be created by script if needed.")
# Optionally, create a dummy one for test if you want to test the copy logic:
# os.makedirs(os.path.join(abs_oc_template_dir, "EFI", "OC"), exist_ok=True)
# with open(os.path.join(abs_oc_template_dir, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"TestTemplate":True}, f)
else:
print(f"Using existing OC_TEMPLATE_DIR for test: {abs_oc_template_dir}")
disk_id_input = input("Enter target PHYSICAL DISK NUMBER for test (e.g., '1' for PhysicalDrive1). WARNING: THIS DISK WILL BE MODIFIED/WIPED by diskpart. BE ABSOLUTELY SURE. Enter 'skip' to not run diskpart stage: ")
if disk_id_input.lower() == 'skip':
print("Skipping disk operations. Guidance message will be shown with placeholder disk info.")
# Create a writer instance with a dummy disk ID for logic testing without diskpart
writer = USBWriterWindows("disk 0", mock_download_dir, print, True, target_version_test)
# We need to manually create a dummy temp_basesystem.hfs for the guidance message part
os.makedirs(writer.temp_dir_base, exist_ok=True)
with open(writer.temp_basesystem_hfs_path, "wb") as f: f.write(os.urandom(1024*1024)) # 1MB dummy HFS
# Manually call parts of format_and_write that don't involve diskpart
writer.check_dependencies() # Still check other deps
# Simulate EFI setup success for guidance
writer.assigned_efi_letter = "X"
# ... then generate and show guidance (this part is inside format_and_write)
# This is a bit clunky for 'skip' mode. Full format_and_write is better if safe.
print("Test in 'skip' mode is limited. Full test requires a dedicated test disk.")
elif not disk_id_input.isdigit():
print("Invalid disk number.")
else:
actual_disk_id_str = f"\\\\.\\PhysicalDrive{disk_id_input}" # Match format used by class
confirm = input(f"ARE YOU ABSOLUTELY SURE you want to test on {actual_disk_id_str}? This involves running 'diskpart clean'. Type 'YESIDO' to confirm: ")
if confirm == 'YESIDO':
writer = USBWriterWindows(actual_disk_id_str, mock_download_dir, print, True, target_version_test)
try:
writer.format_and_write() writer.format_and_write()
else: print("Cancelled.") print(f"Test run completed. Check disk {disk_id_input} and console output.")
shutil.rmtree(mock_download_dir, ignore_errors=True) except Exception as e:
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template print(f"Test run failed: {e}")
print("Mock download dir cleaned up.") traceback.print_exc()
else:
print("Test cancelled by user.")
# Cleanup the test run's unique temp directory
print(f"Cleaning up test run temp directory: {test_run_temp_dir}")
shutil.rmtree(test_run_temp_dir, ignore_errors=True)
print("Standalone test finished.")
```