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.
This commit is contained in:
google-labs-jules[bot] 2025-06-13 10:47:04 +00:00
parent 4665531407
commit 91938925c1
4 changed files with 803 additions and 635 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,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
@ -12,61 +12,89 @@ 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 = ""):
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 # String name like "Sonoma"
pid = os.getpid()
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)
@ -76,318 +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:
from constants import MACOS_VERSIONS # Import for this method
_report = self._report_progress
_report(f"Searching for macOS product folder in {self.macos_download_path} for version {self.target_macos_version}")
# Check for a specific versioned download folder first (gibMacOS pattern)
# e.g. macOS Downloads/publicrelease/XXX - macOS Sonoma 14.X/
possible_toplevel_folders = [
os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease"),
os.path.join(self.macos_download_path, "macOS Downloads", "developerseed"),
os.path.join(self.macos_download_path, "macOS Downloads", "customerseed"),
self.macos_download_path # Fallback to searching directly in the provided path
]
version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
target_version_str_simple = self.target_macos_version.lower().replace("macos","").strip()
for base_path_to_search in possible_toplevel_folders:
if not os.path.isdir(base_path_to_search): continue
for item in os.listdir(base_path_to_search):
item_path = os.path.join(base_path_to_search, item)
item_lower = item.lower()
# Heuristic: look for version string or display name in folder name
if os.path.isdir(item_path) and \
("macos" in item_lower and (target_version_str_simple in item_lower or version_tag_from_constants in item_lower)):
_report(f"Identified gibMacOS product folder: {item_path}")
return item_path
_report(f"Could not identify a specific product folder. Using base download path: {self.macos_download_path}")
return self.macos_download_path
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str, search_deep=True) -> 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]
self._report_progress(f"Searching for {asset_patterns} in {product_folder_path}...")
# Prioritize direct children and common locations
common_subdirs = ["", "SharedSupport", "Install macOS*.app/Contents/SharedSupport", "Install macOS*.app/Contents/Resources"]
search_base = product_folder_path or self.macos_download_path
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
for pattern in asset_patterns:
for sub_dir_pattern in common_subdirs:
# Construct glob pattern, allowing for versioned app names
current_search_base = os.path.join(product_folder_path, sub_dir_pattern.replace("Install macOS*.app", f"Install macOS {self.target_macos_version}.app"))
# If the above doesn't exist, try generic app name for glob
if not os.path.isdir(os.path.dirname(current_search_base)) and "Install macOS*.app" in sub_dir_pattern:
current_search_base = os.path.join(product_folder_path, sub_dir_pattern)
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)
glob_pattern = os.path.join(glob.escape(current_search_base), pattern) # Escape base path for glob
# Search non-recursively first in specific paths
found_files = glob.glob(glob_pattern, recursive=False)
if found_files:
found_files.sort(key=os.path.getsize, reverse=True) # Prefer larger files if multiple (e.g. InstallESD.dmg)
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 requested and not found yet, do a broader recursive search from product_folder_path
if search_deep:
deep_search_pattern = os.path.join(glob.escape(product_folder_path), "**", pattern)
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len) # Prefer shallower paths
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 {product_folder_path} or its common subdirectories.")
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
return None
def _get_gibmacos_product_folder(self) -> str | None:
from constants import MACOS_VERSIONS
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:
# This method assumes dmg_or_pkg_path is the path to a file like BaseSystem.dmg, InstallESD.dmg, or InstallAssistant.pkg
# It tries to extract the core HFS+ filesystem (often '4.hfs' from BaseSystem.dmg)
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True)
current_target_dmg = None
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 DMGs from PKG: {dmg_or_pkg_path}...")
self._run_command(["7z", "x", dmg_or_pkg_path, "*.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True) # Extract all DMGs recursively
dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*.dmg"), recursive=True)
if not dmgs_in_pkg: raise RuntimeError("No DMG found within PKG.")
# Heuristic: find BaseSystem.dmg, else largest InstallESD.dmg, else largest SharedSupport.dmg
bs_dmg = next((d for d in dmgs_in_pkg if "basesystem.dmg" in d.lower()), None)
if bs_dmg: current_target_dmg = bs_dmg
else:
esd_dmgs = [d for d in dmgs_in_pkg if "installesd.dmg" in d.lower()]
if esd_dmgs: current_target_dmg = max(esd_dmgs, key=os.path.getsize)
else:
ss_dmgs = [d for d in dmgs_in_pkg if "sharedsupport.dmg" in d.lower()]
if ss_dmgs: current_target_dmg = max(ss_dmgs, key=os.path.getsize) # This might contain BaseSystem.dmg
else: current_target_dmg = max(dmgs_in_pkg, key=os.path.getsize) # Last resort: largest DMG
if not current_target_dmg: raise RuntimeError("Could not determine primary DMG within PKG.")
self._report_progress(f"Identified primary DMG from PKG: {current_target_dmg}")
elif dmg_or_pkg_path.endswith(".dmg"):
current_target_dmg = dmg_or_pkg_path
else:
raise RuntimeError(f"Unsupported file type for HFS extraction: {dmg_or_pkg_path}")
# If current_target_dmg is (likely) InstallESD.dmg or SharedSupport.dmg, we need to find BaseSystem.dmg within it
basesystem_dmg_to_process = current_target_dmg
if "basesystem.dmg" not in os.path.basename(current_target_dmg).lower():
self._report_progress(f"Searching for BaseSystem.dmg within {current_target_dmg}...")
# Extract to a sub-folder to avoid name clashes
nested_extract_dir = os.path.join(self.temp_dmg_extract_dir, "nested_dmg_contents")
os.makedirs(nested_extract_dir, exist_ok=True)
self._run_command(["7z", "e", current_target_dmg, "*BaseSystem.dmg", "-r", f"-o{nested_extract_dir}"], check=True)
found_bs_dmgs = glob.glob(os.path.join(nested_extract_dir, "**", "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmgs: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target_dmg}")
basesystem_dmg_to_process = found_bs_dmgs[0]
self._report_progress(f"Located BaseSystem.dmg for processing: {basesystem_dmg_to_process}")
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}...")
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"))
if not hfs_files: # If no .hfs, maybe it's a flat DMG image already (unlikely for BaseSystem.dmg)
alt_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*"))
alt_files = [f for f in alt_files if os.path.isfile(f) and not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.getsize(f) > 2*1024*1024*1024] # Min 2GB
if alt_files: hfs_files = alt_files
if not hfs_files: raise RuntimeError(f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}")
final_hfs_file = max(hfs_files, key=os.path.getsize)
self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}")
shutil.move(final_hfs_file, output_hfs_path)
return True
except Exception as e:
self._report_progress(f"Error during HFS extraction: {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_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!")
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", "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", "2:0:0", "-t", "2:af00", "-c", f"2:{usb_vol_name[:11]}" , self.device])
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}.")
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", usb_vol_name, macos_partition_dev])
product_folder = self._get_gibmacos_product_folder()
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder, "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):
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"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"])
# --- 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
self._report_progress("Mounting macOS Install partition on USB...")
self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target])
# --- Copying full installer assets ---
self._report_progress("Copying macOS installer assets to USB...")
# 1. Create "Install macOS [VersionName].app" structure
app_bundle_name = f"Install macOS {self.target_macos_version}.app"
app_bundle_path_usb = os.path.join(self.mount_point_usb_macos_target, app_bundle_name)
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")
self._run_command(["sudo", "mkdir", "-p", shared_support_path_usb_app])
self._run_command(["sudo", "mkdir", "-p", resources_path_usb_app])
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")
# 2. Copy BaseSystem.dmg & BaseSystem.chunklist
core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder)
if original_bs_dmg:
self._report_progress(f"Copying BaseSystem.dmg to {core_services_path_usb}/ and {shared_support_path_usb_app}/")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")])
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")])
original_bs_chunklist = self._find_gibmacos_asset("BaseSystem.chunklist", os.path.dirname(original_bs_dmg)) # Look in same dir as BaseSystem.dmg
if original_bs_chunklist:
self._report_progress(f"Copying BaseSystem.chunklist...")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")])
else: self._report_progress("Warning: Original BaseSystem.dmg not found to copy.")
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])
# 3. Copy InstallInfo.plist
installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder)
# 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.")
# 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")]) # For .app bundle
self._run_command(["sudo", "cp", installinfo_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")]) # For root of volume
else: self._report_progress("Warning: InstallInfo.plist not found.")
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.")
# 4. Copy main installer package(s) to .app/Contents/SharedSupport/
# And also to /System/Installation/Packages/ for direct BaseSystem boot.
packages_dir_usb_system = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb_system])
# 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.")
main_payload_patterns = ["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"] # Order of preference
main_payload_src = self._find_gibmacos_asset(main_payload_patterns, product_folder, "Main Installer Payload (PKG/DMG)")
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")])
if main_payload_src:
payload_basename = os.path.basename(main_payload_src)
self._report_progress(f"Copying main payload '{payload_basename}' to {shared_support_path_usb_app}/ and {packages_dir_usb_system}/")
self._run_command(["sudo", "cp", main_payload_src, os.path.join(shared_support_path_usb_app, payload_basename)])
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb_system, payload_basename)])
# If it's SharedSupport.dmg, its *contents* are often what's needed in Packages, not the DMG itself.
# This is a complex step; createinstallmedia does more. For now, copying the DMG/PKG might be enough for OpenCore to find.
else: self._report_progress("Warning: Main installer payload (InstallAssistant.pkg, InstallESD.dmg, or SharedSupport.dmg) not found.")
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.")
# 5. Copy AppleDiagnostics.dmg to .app/Contents/SharedSupport/
diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder)
if diag_src:
self._report_progress(f"Copying AppleDiagnostics.dmg to {shared_support_path_usb_app}/")
self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")])
# 6. Ensure /System/Library/CoreServices/boot.efi exists (can be a copy of OpenCore's BOOTx64.efi or a generic one)
self._report_progress("Ensuring /System/Library/CoreServices/boot.efi exists on installer partition...")
self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")]) # Placeholder, OC will handle actual boot
self._report_progress("macOS installer assets copied to USB.")
# 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._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._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])
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 not os.path.exists(temp_config_plist_path):
template_plist = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
if os.path.exists(template_plist): self._run_command(["sudo", "cp", template_plist, temp_config_plist_path])
else:
with open(temp_config_plist_path, 'wb') as f: plistlib.dump({"#Comment": "Basic config by Skyscope"}, f, fmt=plistlib.PlistFormat.XML); os.chmod(temp_config_plist_path, 0o644) # Ensure permissions
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
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__':
# ... (Standalone test block needs constants.MACOS_VERSIONS for _get_gibmacos_product_folder)
from constants import MACOS_VERSIONS # For standalone test
import traceback
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 (Fuller Asset Copying)")
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" # Example: python usb_writer_linux.py Sonoma
mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower() # e.g. "sonoma" or "14"
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)
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)
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>")
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 for {target_version_cli}? (yes/NO): ")
success = False
if confirm.lower() == 'yes':
writer = USBWriterLinux(device=test_device, macos_download_path=mock_download_dir, progress_callback=print, enhance_plist_enabled=True, target_macos_version=target_version_cli)
success = writer.format_and_write()
else: print("Test cancelled by user.")
print(f"Test finished. Success: {success}")
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

@ -13,17 +13,12 @@ except ImportError:
enhance_config_plist = None
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled.")
# Assumed to exist relative to this script or project root
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
# For _get_gibmacos_product_folder to access MACOS_VERSIONS from constants.py
# This is a bit of a hack for a library module. Ideally, constants are passed or structured differently.
try:
from constants import MACOS_VERSIONS
except ImportError:
# Define a fallback or minimal version if constants.py is not found in this context
# This might happen if usb_writer_macos.py is tested truly standalone without the full app structure.
MACOS_VERSIONS = {"Sonoma": "14", "Ventura": "13", "Monterey": "12"} # Example
MACOS_VERSIONS = {"Sonoma": "14", "Ventura": "13", "Monterey": "12"}
print("Warning: constants.py not found, using fallback MACOS_VERSIONS for _get_gibmacos_product_folder.")
@ -31,32 +26,31 @@ 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 # Display name like "Sonoma"
self.target_macos_version = target_macos_version
pid = os.getpid()
# Using /tmp for macOS temporary files
self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs"
self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}"
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" # For 7z extractions
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}"
# Mount points will be dynamically created by diskutil or hdiutil attach
# We just need to track them for cleanup if they are custom /tmp paths
self.mount_point_usb_esp = f"/tmp/usb_esp_temp_skyscope_{pid}" # Or use /Volumes/EFI
self.mount_point_usb_macos_target = f"/tmp/usb_macos_target_temp_skyscope_{pid}" # Or use /Volumes/Install macOS ...
self.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_dmg_extract_dir,
self.mount_point_usb_esp, self.mount_point_usb_macos_target
# Mount points created by diskutil mount are usually in /Volumes/ and unmounted by name
self.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 device paths from hdiutil attach
def _report_progress(self, message: str):
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)
@ -74,40 +68,79 @@ class USBWriterMacOS:
def _cleanup_temp_files_and_dirs(self):
self._report_progress("Cleaning up temporary files, directories, and mounts on macOS...")
for f_path in self.temp_files_to_clean:
if os.path.exists(f_path):
try: os.remove(f_path)
except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
# 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
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: {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:
if os.path.ismount(device_path_or_mount_point):
self._run_command(["diskutil", "unmount", "force", device_path_or_mount_point], check=False)
if device_path_or_mount_point.startswith("/dev/disk"):
self._run_command(["hdiutil", "detach", device_path_or_mount_point, "-force"], check=False, timeout=30)
if device_path_or_mount_point in self.attached_dmg_devices:
self.attached_dmg_devices.remove(device_path_or_mount_point)
# 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/unmount {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`). Others are standard."
@ -124,34 +157,28 @@ class USBWriterMacOS:
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 general download path: {self.macos_download_path}"); return self.macos_download_path
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path
def _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:
common_subdirs_for_pattern = ["", "SharedSupport"] # Most assets are here or root of product folder
if "Install macOS" in pattern : # If looking for the .app bundle itself
common_subdirs_for_pattern = [""] # Only look at root of product folder
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
@ -159,55 +186,34 @@ class USBWriterMacOS:
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 "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) # Recursive search
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
basesystem_dmg_to_process = found_bs_dmg[0]
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
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):
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}")
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:
os.makedirs(mp_dir, exist_ok=True)
if not os.path.exists(mp_dir): os.makedirs(mp_dir, exist_ok=True)
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
self._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)
disk_info_plist_str = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
@ -215,11 +221,10 @@ class USBWriterMacOS:
disk_info = plistlib.loads(disk_info_plist_str.encode('utf-8'))
esp_partition_dev = None; macos_partition_dev = None
# Find the main disk entry first
main_disk_entry = next((d for d in disk_info.get("AllDisksAndPartitions", []) if d.get("DeviceIdentifier") == self.device.replace("/dev/", "")), None)
if main_disk_entry:
for part in main_disk_entry.get("Partitions", []):
if part.get("VolumeName") == "EFI" and part.get("Content") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
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.")
@ -236,51 +241,60 @@ class USBWriterMacOS:
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 to {self.temp_usb_macos_target_mount}...")
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
self._report_progress("Copying necessary macOS installer assets to USB...")
self._report_progress(f"macOS partition mounted at {self.mounted_usb_macos_path}")
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(self.temp_usb_macos_target_mount, app_bundle_name)
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")
self._run_command(["sudo", "mkdir", "-p", shared_support_path_usb_app])
self._run_command(["sudo", "mkdir", "-p", os.path.join(contents_path_usb, "Resources")])
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")
coreservices_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices")
self._run_command(["sudo", "mkdir", "-p", coreservices_path_usb])
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])
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=True)
if original_bs_dmg:
self._report_progress(f"Copying BaseSystem.dmg to USB CoreServices and App SharedSupport...")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(coreservices_path_usb, "BaseSystem.dmg")])
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")])
original_bs_chunklist = self._find_gibmacos_asset("BaseSystem.chunklist", os.path.dirname(original_bs_dmg), search_deep=False)
if original_bs_chunklist:
self._report_progress(f"Copying BaseSystem.chunklist...")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(coreservices_path_usb, "BaseSystem.chunklist")])
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")])
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.")
installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True)
if installinfo_src:
self._report_progress(f"Copying InstallInfo.plist...")
self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")])
self._run_command(["sudo", "cp", installinfo_src, os.path.join(self.temp_usb_macos_target_mount, "InstallInfo.plist")])
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.")
packages_dir_usb_system = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb_system])
main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True)
if main_payload_src:
payload_basename = os.path.basename(main_payload_src)
self._report_progress(f"Copying main payload '{payload_basename}' to App SharedSupport and System Packages...")
self._run_command(["sudo", "cp", main_payload_src, os.path.join(shared_support_path_usb_app, payload_basename)])
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb_system, payload_basename)])
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.")
self._run_command(["sudo", "touch", os.path.join(coreservices_path_usb, "boot.efi")]) # Placeholder for bootability
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._report_progress("macOS installer assets copied.")
# --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP...")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
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._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
@ -290,33 +304,27 @@ 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._report_progress(f"Copying final EFI folder to USB ESP ({self.temp_usb_esp_mount})...")
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"])
self._report_progress(f"Blessing the installer volume: {self.temp_usb_macos_target_mount} with ESP {esp_partition_dev}")
# Correct bless command needs the folder containing boot.efi for the system being blessed,
# and the ESP mount point if different from system ESP.
# For installer, it's often /Volumes/Install macOS XXX/System/Library/CoreServices
bless_target_folder = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices")
self._run_command(["sudo", "bless", "--folder", bless_target_folder, "--label", installer_vol_name, "--setBoot"], check=False) # SetBoot might be enough for OpenCore
# Alternative if ESP needs to be specified explicitly:
# self._run_command(["sudo", "bless", "--mount", self.temp_usb_macos_target_mount, "--setBoot", "--file", os.path.join(bless_target_folder, "boot.efi"), "--bootefi", os.path.join(self.temp_usb_esp_mount, "EFI", "BOOT", "BOOTx64.efi")], check=False)
self._report_progress(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 # For testing _get_gibmacos_product_folder
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)
@ -344,10 +352,11 @@ if __name__ == '__main__':
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, 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,73 +1,92 @@
# usb_writer_windows.py (Refining for installer workflow and guidance)
# 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; No = 0; Cancel = 0
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:
# If device_id_str was like "disk 1", this will correctly get "1"
# If it was just "1", it's also fine.
# If it was invalid like "PhysicalDrive1", filter will get "1".
# This logic might need to be more robust if input format varies wildly.
pass # Allow it for now, diskpart will fail if self.disk_number is bad.
self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}"
self.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()}")
@ -76,298 +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 (for extracting installer assets)."
self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("Please 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, 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:
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}.")
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:
self._report_progress(f"Found '{asset_name}' via case-insensitive glob at: {found_files[0]}")
return found_files[0]
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
# 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)
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}'. Using general 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", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True)
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True)
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
basesystem_dmg_to_process = found_bs_dmg[0]
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] # Min 100MB HFS
# 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"
# 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)
macos_partition_offset_str = "Offset not determined by diskpart"
macos_partition_number_str = "2 (assumed)"
self._run_diskpart_script(diskpart_script_part1)
self._report_progress("Disk partitioning complete. Waiting for volumes to settle...")
time.sleep(5) # Give Windows time to recognize new partitions
macos_partition_number_str = "2 (typically)"; macos_partition_offset_str = "Offset not automatically determined for Windows dd"
try:
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n"
# 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:
self._report_progress(f"Detail Partition Output:\n{detail_output}")
offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE)
if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
num_match = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE)
if num_match:
macos_partition_number_str = num_match.group(1)
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
# 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 partition details from diskpart: {e}")
self._report_progress(f"Could not get detailed partition info from diskpart: {e}")
# --- OpenCore EFI Setup ---
self._report_progress("Setting up OpenCore EFI on ESP...")
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR):
self._create_minimal_efi_template(self.temp_efi_build_dir)
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")
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
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:
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only)...")
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 processing complete.")
else: self._report_progress("config.plist enhancement call failed or had issues.")
self._report_progress("config.plist enhancement process complete.")
else:
self._report_progress("config.plist enhancement process failed or had issues (this is expected on Windows for hardware-specifics).")
target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\"
time.sleep(2) # Allow drive letter to be fully active
if not os.path.exists(target_efi_on_usb_root): raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible.")
# 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 {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 HFS Image ---
self._report_progress("Locating BaseSystem image from downloaded assets...")
self._report_progress("Locating BaseSystem image (DMG or PKG containing it) from downloaded assets...")
product_folder_path = self._get_gibmacos_product_folder()
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)")
if not source_for_hfs_extraction: raise RuntimeError("Could not find BaseSystem.dmg, InstallESD.dmg, SharedSupport.dmg or InstallAssistant.pkg.")
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
)
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.")
if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
self._report_progress(f"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.")
abs_hfs_path = os.path.abspath(self.temp_basesystem_hfs_path)
abs_download_path = os.path.abspath(self.macos_download_path)
# --- 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)"
# Key assets to mention for manual copy by user
assets_to_copy_manually = [
"InstallInfo.plist (to root of macOS partition)",
"BaseSystem.dmg (to System/Library/CoreServices/ on macOS partition)",
"BaseSystem.chunklist (to System/Library/CoreServices/ on macOS partition)",
"InstallAssistant.pkg or InstallESD.dmg (to System/Installation/Packages/ on macOS partition)",
"AppleDiagnostics.dmg (if present, to a temporary location then to .app/Contents/SharedSupport/ if making full app structure)"
]
assets_list_str = "\n - ".join(assets_to_copy_manually)
guidance_message = (
f"EFI setup complete on drive {self.assigned_efi_letter}:.\n"
f"BaseSystem HFS image for macOS installer extracted to: '{abs_hfs_path}'.\n\n"
f"MANUAL STEPS REQUIRED FOR MAIN macOS PARTITION (Partition {macos_partition_number_str} on Disk {self.disk_number}):\n"
f"1. Write BaseSystem Image: Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
f" Use a 'dd for Windows' utility. Example (VERIFY SYNTAX FOR YOUR DD TOOL & TARGETS!):\n"
f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} --target-partition {macos_partition_number_str} bs=4M --progress` (Conceptual)\n"
f" (Offset for partition {macos_partition_number_str} on Disk {self.disk_number} is approx. {macos_partition_offset_str})\n\n"
f"2. Copy Other Installer Files: After writing BaseSystem, the 'Install macOS {self.target_macos_version}' partition on USB needs other files from your download path: '{abs_download_path}'.\n"
f" This requires a tool that can write to HFS+ partitions from Windows (e.g., TransMac, Paragon HFS+ for Windows), or doing this step on a macOS/Linux system.\n"
f" Key files to find in '{abs_download_path}' and copy to the HFS+ partition:\n - {assets_list_str}\n"
f" (You might need to create directories like 'System/Library/CoreServices/' and 'System/Installation/Packages/' on the HFS+ partition first using your HFS+ tool).\n\n"
"Without these additional files, the USB might only boot to an internet recovery mode (if network & EFI are correct)."
)
self._report_progress(f"GUIDANCE:\n{guidance_message}")
QMessageBox.information(None, "Manual Steps Required for Windows USB", guidance_message)
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"
self._report_progress("Windows USB installer preparation (EFI automated, macOS content manual steps provided).")
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
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_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
mock_product_name = f"000-00000 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True)
os.makedirs(specific_product_folder, exist_ok=True)
with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg")
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True)
with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML)
# 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")
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)
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)
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) # Keep template for other tests potentially
print("Mock download dir cleaned up.")
# Mock download directory structure within the test_run_temp_dir
mock_download_dir = os.path.join(test_run_temp_dir, "mock_macos_downloads")
os.makedirs(mock_download_dir, exist_ok=True)
# Example: Sonoma. More versions could be added for thorough testing.
target_version_test = "Sonoma"
version_tag_test = MACOS_VERSIONS.get(target_version_test, target_version_test.lower())
mock_product_name = f"012-34567 - macOS {target_version_test} 14.1" # Example name
mock_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
mock_shared_support = os.path.join(mock_product_folder, "SharedSupport")
os.makedirs(mock_shared_support, exist_ok=True)
# 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.
# 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))
# 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.")
```
This refactors `usb_writer_windows.py`:
- Updates `__init__` for `macos_download_path`.
- `format_and_write` now:
- Partitions with `diskpart` (EFI + HFS+ type for macOS partition).
- Sets up OpenCore EFI on ESP from `EFI_template_installer` (with `plist_modifier` call).
- Extracts `BaseSystem.hfs` using `7z`.
- Provides detailed guidance for manual `dd` of `BaseSystem.hfs` and manual copying of other installer assets, including partition number and offset.
- `qemu-img` is removed from dependencies.
- Standalone test updated.