mirror of
https://github.com/sickcodes/Docker-OSX.git
synced 2025-06-29 04:52:44 +02:00
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:
parent
4665531407
commit
91938925c1
@ -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>
|
||||
|
@ -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):
|
||||
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)}")
|
||||
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(
|
||||
command, check=check, capture_output=capture_output, text=True, timeout=timeout,
|
||||
shell=shell, cwd=working_dir,
|
||||
creationflags=0
|
||||
)
|
||||
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 '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise
|
||||
except FileNotFoundError: self._report_progress(f"Error: Command '{cmd_list[0]}' not found."); raise
|
||||
|
||||
def _cleanup_temp_files_and_dirs(self):
|
||||
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.")
|
||||
|
@ -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.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.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.")
|
||||
|
@ -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)
|
||||
def _find_gibmacos_asset(self, asset_name: str, product_folder_path: str | None = None, search_deep=True) -> str | None:
|
||||
search_locations = []
|
||||
if product_folder_path and os.path.isdir(product_folder_path):
|
||||
search_locations.extend([product_folder_path, os.path.join(product_folder_path, "SharedSupport")])
|
||||
|
||||
# Also search directly in macos_download_path and a potential "macOS Install Data" subdirectory
|
||||
search_locations.extend([self.macos_download_path, os.path.join(self.macos_download_path, "macOS Install Data")])
|
||||
|
||||
# If a version-specific folder exists at the root of macos_download_path (less common for gibMacOS structure)
|
||||
if os.path.isdir(self.macos_download_path):
|
||||
for item in os.listdir(self.macos_download_path):
|
||||
item_path = os.path.join(self.macos_download_path, item)
|
||||
if os.path.isdir(item_path) and self.target_macos_version.lower() in item.lower():
|
||||
search_locations.append(item_path)
|
||||
search_locations.append(os.path.join(item_path, "SharedSupport"))
|
||||
# Assuming first match is good enough for this heuristic
|
||||
break
|
||||
|
||||
# Deduplicate search locations while preserving order (Python 3.7+)
|
||||
search_locations = list(dict.fromkeys(search_locations))
|
||||
|
||||
for loc in search_locations:
|
||||
if not os.path.isdir(loc): continue
|
||||
|
||||
path = os.path.join(loc, asset_name)
|
||||
if os.path.exists(path):
|
||||
self._report_progress(f"Found '{asset_name}' at: {path}")
|
||||
return path
|
||||
|
||||
# Case-insensitive glob as fallback for direct name match
|
||||
# Create a pattern like "[bB][aA][sS][eE][sS][yY][sS][tT][eE][mM].[dD][mM][gG]"
|
||||
pattern_parts = [f"[{c.lower()}{c.upper()}]" if c.isalpha() else re.escape(c) for c in asset_name]
|
||||
insensitive_glob_pattern = "".join(pattern_parts)
|
||||
|
||||
found_files = glob.glob(os.path.join(loc, insensitive_glob_pattern), recursive=False)
|
||||
if found_files:
|
||||
found_files.sort(key=os.path.getsize, reverse=True)
|
||||
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
|
||||
self._report_progress(f"Found '{asset_name}' via case-insensitive glob at: {found_files[0]}")
|
||||
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)
|
||||
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 '{pattern}' via deep search at: {found_files_deep[0]}")
|
||||
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 matching patterns '{asset_patterns}' not found in {search_base}.")
|
||||
|
||||
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)
|
||||
# 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()
|
||||
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.")
|
||||
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.
|
||||
|
Loading…
Reference in New Issue
Block a user