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>
<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>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>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>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>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><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>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>
<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>
@ -101,9 +95,9 @@
</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>
<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>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>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>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>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><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>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>-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>
</plist>

View File

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

View File

@ -1,4 +1,4 @@
# usb_writer_linux.py (Refined asset copying)
# usb_writer_linux.py (Finalizing installer asset copying - refined)
import subprocess
import os
import time
@ -6,66 +6,95 @@ import shutil
import glob
import re
import plistlib
import traceback
try:
from plist_modifier import enhance_config_plist
except ImportError:
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")
class USBWriterLinux:
def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""): # target_macos_version is display name e.g. "Sonoma"
self.device = device
self.macos_download_path = macos_download_path
self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version
pid = os.getpid()
target_macos_version: str = ""):
self.device = device; self.macos_download_path = macos_download_path
self.progress_callback = progress_callback; self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version; pid = os.getpid()
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
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_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_dirs_to_clean = [
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):
if self.progress_callback: self.progress_callback(message)
else: print(message)
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)
else: print(message)
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None):
self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}")
try:
process = subprocess.run(
command, check=check, capture_output=capture_output, text=True, timeout=timeout,
shell=shell, cwd=working_dir,
creationflags=0
)
if capture_output:
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
return process
except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None, stream_rsync_progress=False):
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:
process = subprocess.run(cmd_list, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=0)
if capture_output:
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
return process
except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
except FileNotFoundError: self._report_progress(f"Error: Command '{cmd_list[0]}' not found."); raise
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:
if os.path.ismount(mp):
self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15)
if os.path.ismount(mp): self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15)
for f_path in self.temp_files_to_clean:
if os.path.exists(f_path):
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)
except Exception as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
def check_dependencies(self):
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 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
def _get_gibmacos_product_folder(self) -> str:
"""Heuristically finds the main product folder within gibMacOS downloads."""
# gibMacOS often creates .../publicrelease/XXX - macOS [VersionName] [VersionNum]/
# We need to find this folder.
_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."""
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]
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:
# Search both in root of product_folder and common subdirs like "SharedSupport" or "*.app/Contents/SharedSupport"
search_glob_patterns = [
os.path.join(product_folder, pattern),
os.path.join(product_folder, "**", pattern), # Recursive search
]
for glob_pattern in search_glob_patterns:
found_files = glob.glob(glob_pattern, recursive=True)
common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"]
for sub_dir_pattern in common_subdirs_for_pattern:
current_search_base = os.path.join(search_base, sub_dir_pattern)
# Escape special characters for glob, but allow wildcards in pattern itself
# This simple escape might not be perfect for all glob patterns.
glob_pattern = os.path.join(glob.escape(current_search_base), pattern)
found_files = glob.glob(glob_pattern, recursive=False)
if found_files:
# Sort to get a predictable one if multiple (e.g. if pattern is too generic)
# Prefer files not too deep in structure if multiple found by simple pattern
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {description} at: {found_files[0]}")
found_files.sort(key=os.path.getsize, reverse=True)
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
return found_files[0]
self._report_progress(f"Warning: {description} not found with patterns: {asset_patterns} in {product_folder} or its subdirectories.")
if search_deep:
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
if found_files_deep:
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
return found_files_deep[0]
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
return None
def _extract_basesystem_hfs_from_source(self, source_dmg_path: str, output_hfs_path: str) -> bool:
"""Extracts the primary HFS+ partition image (e.g., '4.hfs') from a source DMG (BaseSystem.dmg or InstallESD.dmg)."""
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True)
def _get_gibmacos_product_folder(self) -> str | None:
from constants import MACOS_VERSIONS
base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease")
if not os.path.isdir(base_path): base_path = self.macos_download_path
if os.path.isdir(base_path):
for item in os.listdir(base_path):
item_path = os.path.join(base_path, item)
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:
self._report_progress(f"Extracting HFS+ partition image from {source_dmg_path} into {self.temp_dmg_extract_dir}...")
# 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar for BaseSystem)
# For InstallESD.dmg, it might be a different internal path or structure.
# Assuming the target is a standard BaseSystem.dmg or a DMG containing such structure.
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"))
if not hfs_files:
# Fallback: try extracting * (if only one file inside a simple DMG, like some custom BaseSystem.dmg)
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*")) # Check all files
hfs_files = [f for f in hfs_files if not f.endswith((".xml", ".chunklist", ".plist")) and os.path.getsize(f) > 100*1024*1024] # Filter out small/meta files
if not hfs_files: 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
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}")
assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}"
basesystem_dmg_to_process = current_target
if "basesystem.dmg" not in os.path.basename(current_target).lower(): self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-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._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
assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}"
final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True
except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
finally:
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
def _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:
try:
self.check_dependencies()
self._cleanup_temp_files_and_dirs()
for mp in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]:
self._run_command(["sudo", "mkdir", "-p", mp])
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
self.check_dependencies(); 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])
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)
self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...")
self._run_command(["sudo", "sgdisk", "--zap-all", self.device])
self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device])
self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", f"2:Install macOS {self.target_macos_version}", self.device])
self._run_command(["sudo", "sgdisk", "-n", "0:0:+551MiB", "-t", "0:ef00", "-c", "0:EFI", 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)
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)
macos_partition_dev = next((f"{self.device}{i}" for i in ["2", "p2"] if os.path.exists(f"{self.device}{i}")), None)
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not reliably determine partition names for {self.device}.")
product_folder_path = self._get_gibmacos_product_folder()
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 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...")
self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_partition_dev])
self._report_progress(f"Formatting macOS Install partition ({macos_partition_dev}) as HFS+...")
self._run_command(["sudo", "mkfs.hfsplus", "-v", f"Install macOS {self.target_macos_version}", macos_partition_dev])
# --- Finalizing macOS Installer Content on USB's HFS+ partition ---
self._report_progress("Finalizing macOS installer content on USB...")
usb_target_root = self.mount_point_usb_macos_target
# --- Prepare macOS Installer Content ---
product_folder = self._get_gibmacos_product_folder()
app_bundle_name = f"Install macOS {self.target_macos_version}.app"
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)
# Some gibMacOS downloads might have InstallESD.dmg which contains BaseSystem.dmg.
# Others might have BaseSystem.dmg directly.
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]:
self._run_command(["sudo", "mkdir", "-p", p])
self._report_progress("Extracting bootable HFS+ image from source DMG...")
if not self._extract_basesystem_hfs_from_source(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from source DMG.")
# Copy BaseSystem.dmg & BaseSystem.chunklist
bs_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=True)
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...")
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"])
# Copy InstallInfo.plist
installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True)
if installinfo_src:
self._report_progress(f"Copying InstallInfo.plist...")
self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")])
self._run_command(["sudo", "cp", installinfo_src, os.path.join(usb_target_root, "InstallInfo.plist")])
else: self._report_progress("Warning: InstallInfo.plist (source) not found.")
self._report_progress("Mounting macOS Install partition on USB...")
self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target])
# Copy main installer package(s)
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")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True)
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
original_bs_dmg = self._find_gibmacos_asset(["BaseSystem.dmg"], product_folder, "original BaseSystem.dmg")
if original_bs_dmg:
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")])
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist")
if os.path.exists(original_bs_chunklist):
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
else: self._report_progress("Warning: Original BaseSystem.dmg not found in product folder to copy to CoreServices.")
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.")
install_info_src = self._find_gibmacos_asset(["InstallInfo.plist"], product_folder, "InstallInfo.plist")
if install_info_src:
self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist")
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")])
else: self._report_progress("Warning: InstallInfo.plist not found in product folder.")
# Copy Packages and other assets
packages_target_path = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_target_path])
# 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])
# Create .IAProductInfo (Simplified XML string to avoid f-string issues in tool call)
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"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._report_progress("Created .IAProductInfo.")
self._report_progress("macOS installer assets fully copied to USB.")
# --- 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")
# If template is config-template.plist, rename it for enhancement
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
self._run_command(["sudo", "mv", os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path])
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
if not os.path.exists(temp_config_plist_path):
template_plist = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
if os.path.exists(template_plist): 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:
self._report_progress("Attempting to enhance config.plist...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement successful.")
else: self._report_progress("config.plist enhancement failed or had issues.")
self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp])
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
else: self._report_progress("config.plist enhancement call failed or had issues.")
self._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.")
return True
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
finally:
self._cleanup_temp_files_and_dirs()
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)
print("USB Writer Linux Standalone Test - Installer Method (Refined)")
mock_download_dir = f"temp_macos_download_test_{os.getpid()}"
os.makedirs(mock_download_dir, exist_ok=True)
# Create a more structured mock download similar to gibMacOS output
product_name_slug = f"000-00000 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.0" # Example
specific_product_folder = os.path.join(mock_download_dir, "publicrelease", product_name_slug)
os.makedirs(specific_product_folder, exist_ok=True)
# Mock BaseSystem.dmg (tiny, not functional, for path testing)
dummy_bs_dmg_path = os.path.join(specific_product_folder, "BaseSystem.dmg")
if not os.path.exists(dummy_bs_dmg_path):
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy
# Mock BaseSystem.chunklist
dummy_bs_chunklist_path = os.path.join(specific_product_folder, "BaseSystem.chunklist")
if not os.path.exists(dummy_bs_chunklist_path):
with open(dummy_bs_chunklist_path, "w") as f: f.write("dummy chunklist")
# Mock InstallInfo.plist
dummy_installinfo_path = os.path.join(specific_product_folder, "InstallInfo.plist")
if not os.path.exists(dummy_installinfo_path):
with open(dummy_installinfo_path, "w") as f: plistlib.dump({"DummyInstallInfo": True}, f)
# Mock InstallAssistant.pkg (empty for now, just to test its presence)
dummy_pkg_path = os.path.join(specific_product_folder, "InstallAssistant.pkg")
if not os.path.exists(dummy_pkg_path):
with open(dummy_pkg_path, "wb") as f: f.write(os.urandom(1024))
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)
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)
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True); os.makedirs(specific_product_folder, exist_ok=True)
with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist")
with open(os.path.join(specific_product_folder, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f)
with open(os.path.join(specific_product_folder, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024))
with open(os.path.join(specific_product_folder, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024))
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True)
with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML)
with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("dummy bootx64.efi")
print("\nAvailable block devices (be careful!):"); subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True)
test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ")
if not test_device or not test_device.startswith("/dev/"):
print("Invalid device. Exiting.")
else:
confirm = input(f"Are you absolutely sure you want to wipe {test_device} and create installer? (yes/NO): ")
success = False
if confirm.lower() == 'yes':
writer = USBWriterLinux(
device=test_device,
macos_download_path=mock_download_dir, # Pass base download dir
progress_callback=print,
enhance_plist_enabled=True,
target_macos_version=sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
)
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)
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 input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes':
writer = USBWriterLinux(test_device, mock_download_dir, print, True, target_version_cli)
writer.format_and_write()
else: print("Test cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True);
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template dir for other tests
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")
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:
def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""):
self.device = device # e.g., /dev/diskX
self.device = device
self.macos_download_path = macos_download_path
self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version
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_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{pid}" # For source BaseSystem.dmg's EFI (if needed)
self.temp_usb_esp_mount = f"/tmp/usb_esp_temp_skyscope_{pid}"
self.temp_macos_source_mount = f"/tmp/macos_source_temp_skyscope_{pid}" # Not used in this flow
self.temp_usb_macos_target_mount = f"/tmp/usb_macos_target_temp_skyscope_{pid}"
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" # For 7z extractions
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}"
self.mounted_usb_esp_path = None # Will be like /Volumes/EFI
self.mounted_usb_macos_path = None # Will be like /Volumes/Install macOS ...
self.mounted_source_basesystem_path = f"/tmp/source_basesystem_mount_{pid}"
self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
self.temp_dirs_to_clean = [
self.temp_efi_build_dir, self.temp_opencore_mount,
self.temp_usb_esp_mount, self.temp_macos_source_mount,
self.temp_usb_macos_target_mount, self.temp_dmg_extract_dir
self.temp_efi_build_dir, self.temp_dmg_extract_dir,
self.mounted_source_basesystem_path
# 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)
else: print(message)
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False): # ... (same)
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False):
self._report_progress(f"Executing: {' '.join(command)}")
try:
process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell)
@ -58,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 FileNotFoundError: self._report_progress(f"Error: Command '{command[0]}' not found."); raise
def _cleanup_temp_files_and_dirs(self): # Updated for macOS
self._report_progress("Cleaning up temporary files and directories...")
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}")
def _cleanup_temp_files_and_dirs(self):
self._report_progress("Cleaning up temporary files, directories, and mounts on macOS...")
# Detach DMGs first
for dev_path in list(self.attached_dmg_devices): # Iterate copy
# Unmount our specific /tmp mount points first
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.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:
if os.path.ismount(d_path):
try: self._run_command(["diskutil", "unmount", "force", d_path], check=False, timeout=30)
except Exception: pass # Ignore if already unmounted or error
if os.path.exists(d_path):
if os.path.exists(d_path) and d_path.startswith("/tmp/"): # Only remove /tmp dirs we created
try: shutil.rmtree(d_path, ignore_errors=True)
except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
def _detach_dmg(self, device_path_or_mount_point):
if not device_path_or_mount_point: return
self._report_progress(f"Attempting to detach DMG associated with {device_path_or_mount_point}...")
try:
# hdiutil detach can take a device path or sometimes a mount path if it's unique enough
# Using -force to ensure it detaches even if volumes are "busy" (after unmount attempts)
self._run_command(["hdiutil", "detach", device_path_or_mount_point, "-force"], check=False, timeout=30)
if device_path_or_mount_point in self.attached_dmg_devices: # Check if it was in our list
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)
def _unmount_path(self, mount_path_or_device, is_device=False, force=False):
target = mount_path_or_device
cmd_base = ["diskutil"]
action = "unmountDisk" if is_device else "unmount"
cmd = cmd_base + ([action, "force", target] if force else [action, target])
# 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:
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):
self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd)...")
dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"]
self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd, bless)...")
dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd", "bless"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`)."
msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`). Others are standard."
self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("All critical dependencies for macOS USB installer creation found.")
return True
@ -111,149 +154,149 @@ class USBWriterMacOS:
if os.path.isdir(base_path):
for item in os.listdir(base_path):
item_path = os.path.join(base_path, item)
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or MACOS_VERSIONS.get(self.target_macos_version, "").lower() in item.lower()): # MACOS_VERSIONS needs to be accessible or passed if not global
version_tag = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag in item.lower()):
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using base download path."); return self.macos_download_path
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None) -> str | None:
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None:
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
search_base = product_folder_path or self.macos_download_path
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
for pattern in asset_patterns:
# Using iglob for efficiency if many files, but glob is fine for fewer expected matches
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True)
if found_files:
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {pattern}: {found_files[0]}")
return found_files[0]
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.")
common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"]
for sub_dir_pattern in common_subdirs_for_pattern:
current_search_base = os.path.join(search_base, sub_dir_pattern)
glob_pattern = os.path.join(glob.escape(current_search_base), pattern)
found_files = glob.glob(glob_pattern, recursive=False)
if found_files:
found_files.sort(key=os.path.getsize, reverse=True)
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
return found_files[0]
if search_deep:
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
if found_files_deep:
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
return found_files_deep[0]
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
return None
def _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:
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"));
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}")
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}")
assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}"
basesystem_dmg_to_process = current_target
# If current_target is InstallESD.dmg or SharedSupport.dmg, it contains BaseSystem.dmg
if "basesystem.dmg" not in os.path.basename(current_target).lower():
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}...")
self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True)
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
basesystem_dmg_to_process = found_bs_dmg[0]
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}...")
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
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}")
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._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]
assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}"
final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True
except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
finally:
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
def _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)
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 _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:
try:
self.check_dependencies()
self._cleanup_temp_files_and_dirs()
for mp_dir in self.temp_dirs_to_clean: # Use full list from constructor
os.makedirs(mp_dir, exist_ok=True)
for mp_dir in self.temp_dirs_to_clean:
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._run_command(["diskutil", "unmountDisk", "force", self.device], check=False, timeout=60); time.sleep(2)
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)
# Get actual partition identifiers
disk_info_plist = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
if not disk_info_plist: raise RuntimeError("Failed to get disk info after partitioning.")
disk_info = plistlib.loads(disk_info_plist.encode('utf-8'))
disk_info_plist_str = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
if not disk_info_plist_str: raise RuntimeError("Failed to get disk info after partitioning.")
disk_info = plistlib.loads(disk_info_plist_str.encode('utf-8'))
esp_partition_dev = None; macos_partition_dev = None
for disk_entry in disk_info.get("AllDisksAndPartitions", []):
if disk_entry.get("DeviceIdentifier") == self.device.replace("/dev/", ""):
for part in disk_entry.get("Partitions", []):
if part.get("VolumeName") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}).")
main_disk_entry = next((d for d in disk_info.get("AllDisksAndPartitions", []) if d.get("DeviceIdentifier") == self.device.replace("/dev/", "")), None)
if main_disk_entry:
for part in main_disk_entry.get("Partitions", []):
if part.get("Content") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}). Check diskutil list output.")
self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}")
# --- Prepare macOS Installer Content ---
product_folder = self._get_gibmacos_product_folder()
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
product_folder_path = self._get_gibmacos_product_folder()
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.")
if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk") # Use raw device for dd
raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk")
self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...")
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800)
self._report_progress(f"Mounting macOS Install partition ({macos_partition_dev}) on USB...")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev])
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.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._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
self._report_progress(f"macOS partition mounted at {self.mounted_usb_macos_path}")
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder)
if original_bs_dmg:
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg")
self._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")])
usb_target_root = self.mounted_usb_macos_path
app_bundle_name = f"Install macOS {self.target_macos_version}.app"
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")
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")
install_info_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder)
if install_info_src:
self._report_progress(f"Copying InstallInfo.plist to {self.temp_usb_macos_target_mount}/InstallInfo.plist")
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.temp_usb_macos_target_mount, "InstallInfo.plist")])
for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]:
self._run_command(["sudo", "mkdir", "-p", p])
packages_dir_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb])
for f_name in ["BaseSystem.dmg", "BaseSystem.chunklist"]:
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.
# A real createinstallmedia copies the .app then uses it. We are building manually.
# We need to find the main payload: InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg content.
main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "Main Installer Payload (PKG/DMG)")
if main_payload_src:
self._report_progress(f"Copying main payload {os.path.basename(main_payload_src)} to {packages_dir_usb}/")
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb, os.path.basename(main_payload_src))])
# If it's SharedSupport.dmg, its contents might be what's needed in Packages or elsewhere.
# If InstallAssistant.pkg, it might need to be placed at root or specific app structure.
else: self._report_progress("Warning: Main installer payload not found. Installer may be incomplete.")
installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True)
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")])
else: self._report_progress("Warning: InstallInfo.plist not found.")
main_pkg_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True)
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)])
else: self._report_progress("Warning: Main installer PKG/DMG not found.")
diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True)
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.")
# --- OpenCore EFI Setup ---
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)
else: self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}"); self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
else: self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
@ -261,52 +304,59 @@ class USBWriterMacOS:
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)...")
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.")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
self._report_progress(f"Copying final EFI folder to USB ESP ({self.temp_usb_esp_mount})...")
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"])
self._report_progress(f"Copying final EFI folder to USB ESP ({self.mounted_usb_esp_path})...")
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mounted_usb_esp_path}/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.")
return True
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
finally:
self._cleanup_temp_files_and_dirs()
if __name__ == '__main__':
import traceback
from constants import MACOS_VERSIONS
if platform.system() != "Darwin": print("This script is intended for macOS for standalone testing."); exit(1)
print("USB Writer macOS Standalone Test - Installer Method")
mock_download_dir = f"/tmp/temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
# Simulate a more realistic gibMacOS product folder structure for testing _get_gibmacos_product_folder
mock_product_name = f"012-34567 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.1.2"
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
mock_product_folder_path = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True) # Create SharedSupport directory
os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True)
with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist")
with open(os.path.join(mock_product_folder_path, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f)
with open(os.path.join(mock_product_folder_path, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024))
with open(os.path.join(mock_product_folder_path, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024))
# Create dummy BaseSystem.dmg inside the product folder's SharedSupport
dummy_bs_dmg_path = os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg")
if not os.path.exists(dummy_bs_dmg_path):
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy DMG
dummy_installinfo_path = os.path.join(mock_product_folder_path, "InstallInfo.plist")
if not os.path.exists(dummy_installinfo_path):
with open(dummy_installinfo_path, "wb") as f: plistlib.dump({"DisplayName":f"macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'}"},f)
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True)
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
if not os.path.exists(dummy_config_template_path):
with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f)
with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f, fmt=plistlib.PlistFormat.XML)
dummy_bootx64_efi_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi")
if not os.path.exists(dummy_bootx64_efi_path):
with open(dummy_bootx64_efi_path, "w") as f: f.write("dummy bootx64.efi content")
print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False)
test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ")
if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) # No need to clean OC_TEMPLATE_DIR here
if 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':
writer = USBWriterMacOS(test_device, mock_download_dir, print, True, sys.argv[1] if len(sys.argv) > 1 else "Sonoma")
writer = USBWriterMacOS(test_device, mock_download_dir, print, True, target_version_cli)
writer.format_and_write()
else: print("Test cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True)
# Deliberately not cleaning OC_TEMPLATE_DIR in test, as it might be shared or pre-existing.
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 os
import time
import shutil
import re
import glob # For _find_gibmacos_asset
import glob
import plistlib
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:
from PyQt6.QtWidgets import QMessageBox # For user guidance
from PyQt6.QtWidgets import QMessageBox
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
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
def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox
Yes = 1 # Mock value
No = 0 # Mock value
Cancel = 0 # Mock value
def warning(parent, title, message, buttons=None, defaultButton=None):
print(f"WARNING (QMessageBox mock): Title='{title}', Message='{message}'")
return QMessageBox.Yes # Simulate a positive action
@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:
from plist_modifier import enhance_config_plist
except ImportError:
enhance_config_plist = None
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled.")
print("Warning: plist_modifier not found. Enhancement will be skipped.")
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:
def __init__(self, device_id_str: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False,
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))
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.macos_download_path = macos_download_path
self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version
pid = os.getpid()
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
self.temp_efi_build_dir = f"temp_efi_build_{pid}"
self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For 7z extractions
# Use system temp for Windows more reliably
self.temp_dir_base = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_usb_temp_{pid}")
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]
self.temp_dirs_to_clean = [self.temp_efi_build_dir, self.temp_dmg_extract_dir]
self.temp_files_to_clean = [self.temp_basesystem_hfs_path] # Specific files outside temp_dir_base (if any)
self.temp_dirs_to_clean = [self.temp_dir_base] # Base temp dir for this instance
self.assigned_efi_letter = None
def _report_progress(self, message: str):
if self.progress_callback: self.progress_callback(message)
else: print(message)
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None):
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)}")
try:
process = subprocess.run(
command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir,
creationflags=subprocess.CREATE_NO_WINDOW
)
process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=creationflags)
if capture_output:
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
@ -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 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:
script_file_path = f"diskpart_script_{os.getpid()}.txt"; output_text = ""
with open(script_file_path, "w") as f: f.write(script_content)
script_file_path = os.path.join(self.temp_dir_base, f"diskpart_script_{os.getpid()}.txt")
os.makedirs(self.temp_dir_base, exist_ok=True)
output_text = None
try:
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 "")
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
finally:
if os.path.exists(script_file_path): os.remove(script_file_path)
return output_text if capture_output_for_parse else None
if os.path.exists(script_file_path):
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):
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:
if os.path.exists(f_path):
try: os.remove(f_path)
except Exception as e: self._report_progress(f"Could not remove temp file {f_path}: {e}")
for d_path in self.temp_dirs_to_clean:
except OSError as e: self._report_progress(f"Error removing file {f_path}: {e}")
for d_path in self.temp_dirs_to_clean: # self.temp_dir_base is the main one
if os.path.exists(d_path):
try: shutil.rmtree(d_path, ignore_errors=True)
except Exception as e: self._report_progress(f"Could not remove temp dir {d_path}: {e}")
try: shutil.rmtree(d_path, ignore_errors=False) # Try with ignore_errors=False first
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:
import string; used_letters = set()
import string
used_letters = set()
try:
if 'psutil' in sys.modules: # Check if psutil was imported by main app
partitions = sys.modules['psutil'].disk_partitions(all=True)
# Try to use psutil if available (e.g., when run from main_app.py)
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:
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())
except Exception as e:
self._report_progress(f"Could not list used drive letters with psutil: {e}. Will try common letters.")
else: # Fallback if psutil is not available (e.g. pure standalone script)
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":
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 None
def check_dependencies(self):
self._report_progress("Checking dependencies (diskpart, robocopy, 7z, dd for Windows [manual check])...")
dependencies = ["diskpart", "robocopy", "7z"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip CLI) needs to be installed and in PATH."
self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("Base dependencies found. Ensure a 'dd for Windows' utility is installed and in your PATH for writing the main macOS BaseSystem image.")
missing = [dep for dep in dependencies if not shutil.which(dep)]
if missing:
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("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
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None) -> str | None:
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
search_base = product_folder_path or self.macos_download_path
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
for pattern in asset_patterns:
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True)
def _find_gibmacos_asset(self, asset_name: str, product_folder_path: str | None = None, search_deep=True) -> str | None:
search_locations = []
if product_folder_path and os.path.isdir(product_folder_path):
search_locations.extend([product_folder_path, os.path.join(product_folder_path, "SharedSupport")])
# 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:
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
self._report_progress(f"Found {pattern}: {found_files[0]}")
self._report_progress(f"Found '{asset_name}' via case-insensitive glob at: {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
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")
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):
potential_folders = []
for item in os.listdir(base_path):
item_path = os.path.join(base_path, item)
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or MACOS_VERSIONS.get(self.target_macos_version, "").lower() in item.lower()):
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using base download path: {self.macos_download_path}"); return self.macos_download_path
# Check if it's a directory and matches target_macos_version (name or tag)
version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version.lower().replace(" ", ""))
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:
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:
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"));
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.")
if not os.path.exists(current_target):
self._report_progress(f"Error: Input file for HFS extraction does not exist: {current_target}"); return False
# Step 1: If it's a PKG, extract DMGs from it.
if dmg_or_pkg_path.lower().endswith(".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}")
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
# 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():
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True)
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
basesystem_dmg_to_process = found_bs_dmg[0]
self._report_progress(f"Extracting BaseSystem.dmg from container DMG: {current_target} using 7z...")
# Extract recursively, looking for any path that includes BaseSystem.dmg
self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{temp_extract_dir}", "-y"], check=True)
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)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
if not hfs_files:
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True) # Try extracting all files
hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 100*1024*1024]
# Step 4: Extract HFS partition image from BaseSystem.dmg.
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process} using 7z...")
# Using 'e' to extract flat, '-tdmg' for DMG format. Looking for '*.hfs' or specific partition files.
# Common HFS file names inside BaseSystem.dmg are like '2.hfs' or similar.
# 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}")
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
finally:
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
if not hfs_files: # If no .hfs, try extracting by common partition indices if 7z supports listing them for DMG
self._report_progress("No direct '*.hfs' found. Attempting extraction of common HFS partition by index (e.g., '2', '3')...")
# This is more complex as 7z CLI might not easily allow extracting by index directly without listing first.
# For now, we rely on .hfs existing. If this fails, user might need to extract manually with 7z GUI.
# 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):
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"}}}
if not hfs_files: self._report_progress(f"No HFS files found after extracting DMG: {basesystem_dmg_to_process}"); return False
final_hfs_file = max(hfs_files, key=os.path.getsize, default=None) # Largest HFS file
if not final_hfs_file: self._report_progress("Failed to select HFS file."); return False
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
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:
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}")
with open(config_plist_path, 'wb') as fp:
plistlib.dump(basic_config, fp, fmt=plistlib.PlistFormat.XML)
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:
try:
self.check_dependencies()
self._cleanup_temp_files_and_dirs()
os.makedirs(self.temp_efi_build_dir, exist_ok=True)
if os.path.exists(self.temp_dir_base):
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!")
# Optional: Add a QMessageBox.question here for final confirmation in GUI mode
self.assigned_efi_letter = self._find_available_drive_letter()
if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.")
self._report_progress(f"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"create partition efi size=550 label=\"EFI\"\nformat fs=fat32 quick\nassign letter={self.assigned_efi_letter}\n" # Assign after format
diskpart_script_part1 += f"create partition primary label=\"Install macOS {self.target_macos_version}\" id=AF00\nexit\n" # Set HFS+ type ID
# Create EFI (ESP) partition, 550MB is generous and common
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)
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 (assumed)"
macos_partition_number_str = "2 (typically)"; macos_partition_offset_str = "Offset not automatically determined for Windows dd"
try:
# Attempt to get partition details. This is informational.
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)
if detail_output:
# Try to find Partition 2, assuming it's our target HFS+ partition
part_match = re.search(r"Partition 2\s+Primary\s+\d+\s+[GMK]B\s+(\d+)\s+[GMK]B", detail_output, re.IGNORECASE)
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}")
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n"
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
if detail_output:
self._report_progress(f"Detail Partition Output:\n{detail_output}")
offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE)
if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
part_num_match = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE) # Match "Partition X" then "Type" on next line
if part_num_match:
macos_partition_number_str = part_num_match.group(1)
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
# --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP...")
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir)
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR):
self._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:
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
if os.path.exists(self.temp_efi_build_dir): shutil.rmtree(self.temp_efi_build_dir)
self._report_progress(f"Copying EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
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")
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
if os.path.exists(template_plist_src): shutil.copy2(template_plist_src, temp_config_plist_path)
else: self._create_minimal_efi_template(self.temp_efi_build_dir) # Fallback to create basic if template also missing
template_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
if os.path.exists(template_plist_path):
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):
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only for this feature)...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
else: self._report_progress("config.plist enhancement call failed or had issues.")
if self.enhance_plist_enabled and enhance_config_plist: # Check if function exists
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 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}:\\"
if not os.path.exists(target_efi_on_usb_root): # Wait and check again
time.sleep(3)
# Ensure the assigned drive letter is actually available before robocopy
if not os.path.exists(target_efi_on_usb_root):
time.sleep(3) # Extra wait
if not os.path.exists(target_efi_on_usb_root):
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign.")
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._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), target_efi_on_usb_root + "EFI", "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True)
self._report_progress(f"Copying final EFI folder from {os.path.join(self.temp_efi_build_dir, 'EFI')} to USB ESP ({target_efi_on_usb_root}EFI)...")
# 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}")
# --- Prepare BaseSystem ---
self._report_progress("Locating BaseSystem image from downloaded assets...")
# --- Prepare BaseSystem HFS Image ---
self._report_progress("Locating BaseSystem image (DMG or PKG containing it) from downloaded assets...")
product_folder_path = self._get_gibmacos_product_folder()
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
if not source_for_hfs_extraction: source_for_hfs_extraction = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, "InstallAssistant.pkg as BaseSystem source")
if not source_for_hfs_extraction: raise RuntimeError("Could not find BaseSystem.dmg, InstallESD.dmg, SharedSupport.dmg or InstallAssistant.pkg.")
if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
abs_hfs_path = os.path.abspath(self.temp_basesystem_hfs_path)
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."
basesystem_source_dmg_or_pkg = (
self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path) or
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
)
self._report_progress(f"GUIDANCE:\n{guidance_message}")
QMessageBox.information(None, "Manual Steps Required for Windows USB", guidance_message) # Ensure QMessageBox is available or mocked
if not basesystem_source_dmg_or_pkg:
# 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
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
finally:
if self.assigned_efi_letter:
self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit")
self._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._report_progress("Temporary files cleanup attempted.")
# Standalone test block
if __name__ == '__main__':
import traceback
from constants import MACOS_VERSIONS # Needed for _get_gibmacos_product_folder
if platform.system() != "Windows": print("This script is for Windows standalone testing."); exit(1)
import platform
if platform.system() != "Windows":
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")
mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
mock_product_name = f"000-00000 - macOS {target_version_cli} 14.x.x"
# Mock constants if not available (e.g. running totally standalone)
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)
os.makedirs(os.path.join(mock_product_folder, "SharedSupport"), exist_ok=True)
with open(os.path.join(mock_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg")
mock_shared_support = os.path.join(mock_product_folder, "SharedSupport")
os.makedirs(mock_shared_support, exist_ok=True)
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"))
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)
# Create dummy files that would be found by _find_gibmacos_asset and _extract_hfs_from_dmg_or_pkg
# 1. Dummy InstallAssistant.pkg (which contains BaseSystem.dmg)
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: ")
if not disk_id_input.isdigit(): print("Invalid disk number."); exit(1)
# 2. Dummy files for the guidance message (these would normally be in mock_product_folder or mock_shared_support)
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)
writer.format_and_write()
else: print("Cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True)
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template
print("Mock download dir cleaned up.")
# 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()
print(f"Test run completed. Check disk {disk_id_input} and console output.")
except Exception as e:
print(f"Test run failed: {e}")
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.")
```