Compare commits
3 Commits
5d0e2da88d
...
91938925c1
| Author | SHA1 | Date |
|---|---|---|
|
|
91938925c1 | |
|
|
4665531407 | |
|
|
d46413019e |
|
|
@ -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>
|
||||
|
|
|
|||
98
README.md
98
README.md
|
|
@ -1,96 +1,96 @@
|
|||
# Skyscope macOS on PC USB Creator Tool
|
||||
|
||||
**Version:** 1.0.0 (Dev - New Workflow)
|
||||
**Version:** 1.1.0 (Alpha - Installer Workflow with NVIDIA/OCLP Guidance)
|
||||
**Developer:** Miss Casey Jay Topojani
|
||||
**Business:** Skyscope Sentinel Intelligence
|
||||
|
||||
## Vision: Your Effortless Bridge to macOS on PC
|
||||
|
||||
Welcome to the Skyscope macOS on PC USB Creator Tool! Our vision is to provide an exceptionally user-friendly, GUI-driven application that fully automates the complex process of creating a bootable macOS USB *Installer* for virtually any PC. This tool aims to be your comprehensive solution, simplifying the Hackintosh journey from start to finish by leveraging direct macOS downloads and intelligent OpenCore EFI configuration.
|
||||
Welcome to the Skyscope macOS on PC USB Creator Tool! Our vision is to provide an exceptionally user-friendly, GUI-driven application that automates the complex process of creating a bootable macOS USB **Installer** for a wide range of PCs. This tool aims to be your comprehensive solution, simplifying the Hackintosh journey from start to finish by leveraging direct macOS downloads from Apple and intelligent OpenCore EFI configuration.
|
||||
|
||||
This project is dedicated to creating a seamless experience, from selecting your desired macOS version (defaulting to the latest like Sequoia where possible) to generating a USB drive that's ready to boot your PC and install macOS. We strive to incorporate advanced options for tech-savvy users while maintaining an intuitive interface for all.
|
||||
This project is dedicated to creating a seamless experience, from selecting your desired macOS version (defaulting to the latest like Sequoia where possible) to generating a USB drive that's ready to boot your PC and guide you through installing macOS. We strive to incorporate advanced options for tech-savvy users while maintaining an intuitive interface for all, with a clear path for enabling currently unsupported hardware like specific NVIDIA GPUs on newer macOS versions through community-standard methods.
|
||||
|
||||
## Core Features
|
||||
|
||||
* **Intuitive Graphical User Interface (PyQt6):**
|
||||
* Dark-themed by default (planned).
|
||||
* Dark-themed by default (planned UI enhancement).
|
||||
* Rounded window design (platform permitting).
|
||||
* Clear, step-by-step workflow.
|
||||
* Enhanced progress indicators (filling bars, spinners, percentage updates - planned).
|
||||
* **Automated macOS Installer Acquisition:**
|
||||
* Directly downloads official macOS installer assets from Apple's servers using `gibMacOS` principles.
|
||||
* Supports user selection of macOS versions (aiming for Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.).
|
||||
* Directly downloads official macOS installer assets from Apple's servers using `gibMacOS.py` principles.
|
||||
* Supports user selection of macOS versions (e.g., Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.).
|
||||
* **Automated USB Installer Creation:**
|
||||
* **Cross-Platform USB Detection:** Identifies suitable USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows).
|
||||
* **Automated Partitioning:** Creates GUID Partition Table (GPT), an EFI System Partition (FAT32, ~300-550MB), and a main macOS Installer partition (HFS+).
|
||||
* **macOS Installer Layout:** Automatically extracts and lays out downloaded macOS assets (BaseSystem, installer packages, etc.) onto the USB to create a bootable macOS installer volume.
|
||||
* **macOS Installer Layout (Linux & macOS):** Automatically extracts and lays out downloaded macOS assets (BaseSystem, key support files, and installer packages) onto the USB to create a bootable macOS installer volume.
|
||||
* **Windows USB Writing (Partial Automation):** Automates EFI partition setup and EFI file copying. Writing the BaseSystem HFS+ image to the main USB partition requires a guided manual `dd` step by the user. Copying further HFS+ installer content from Windows is not automated.
|
||||
* **Intelligent OpenCore EFI Setup:**
|
||||
* Assembles a complete OpenCore EFI folder on the USB's EFI partition.
|
||||
* Includes essential drivers, kexts, and ACPI SSDTs for broad compatibility.
|
||||
* Assembles a complete OpenCore EFI folder on the USB's EFI partition using a robust template.
|
||||
* **Experimental `config.plist` Auto-Enhancement:**
|
||||
* If enabled by the user (and running the tool on a Linux host for hardware detection):
|
||||
* Gathers host hardware information (iGPU, dGPU, Audio, Ethernet, CPU).
|
||||
* Applies targeted modifications to the `config.plist` to improve compatibility (e.g., Intel iGPU `DeviceProperties`, audio `layout-id`s, enabling Ethernet kexts).
|
||||
* Specific handling for NVIDIA GPUs (e.g., GTX 970) based on target macOS version to allow booting (e.g., `nv_disable=1` for newer macOS if iGPU is primary, or boot-args for OCLP compatibility).
|
||||
* Applies targeted modifications to the `config.plist` for iGPU, audio, Ethernet, and specific NVIDIA GPU considerations.
|
||||
* Creates a backup of the original `config.plist` before modification.
|
||||
* **Privilege Handling:** Checks for and advises on necessary admin/root privileges for USB writing.
|
||||
* **User Guidance:** Provides clear instructions and warnings throughout the process.
|
||||
* **NVIDIA GPU Strategy (for newer macOS like Sonoma/Sequoia):**
|
||||
* The tool configures the `config.plist` to ensure bootability with NVIDIA Maxwell/Pascal GPUs (like GTX 970).
|
||||
* If an Intel iGPU is present and usable, it will be prioritized for display, and `nv_disable=1` will be set for the NVIDIA card.
|
||||
* Includes necessary boot-args (e.g., `amfi_get_out_of_my_way=0x1`) to prepare the system for **post-install patching with OpenCore Legacy Patcher (OCLP)**, which is required for graphics acceleration.
|
||||
* **Privilege Checking:** Warns if administrative/root privileges are needed for USB writing and are not detected.
|
||||
|
||||
## NVIDIA GPU Support Strategy (e.g., GTX 970 on newer macOS)
|
||||
## NVIDIA GPU Support on Newer macOS (Mojave+): The OCLP Path
|
||||
|
||||
* **Installer Phase:** This tool will configure the OpenCore EFI on the USB installer to allow your system to boot with your NVIDIA card.
|
||||
* For macOS High Sierra (or older, if supported by download method): The `config.plist` can be set to enable NVIDIA Web Drivers (e.g., `nvda_drv=1`), assuming you would install them into macOS later.
|
||||
* For macOS Mojave and newer (Sonoma, Sequoia, etc.) where native NVIDIA drivers are absent:
|
||||
* If your system has an Intel iGPU, this tool will aim to configure the iGPU as primary and add `nv_disable=1` to `boot-args` for the NVIDIA card.
|
||||
* If the NVIDIA card is your only graphics output, `nv_disable=1` will not be set, allowing macOS to boot with basic display (no acceleration) from your NVIDIA card.
|
||||
* The `config.plist` will include boot arguments like `amfi_get_out_of_my_way=0x1` to prepare the system for potential use with OpenCore Legacy Patcher.
|
||||
* **Post-macOS Installation (User Action for Acceleration):**
|
||||
* To achieve graphics acceleration for unsupported NVIDIA cards (like Maxwell GTX 970 or Pascal GTX 10xx) on macOS Mojave and newer, you will need to run the **OpenCore Legacy Patcher (OCLP)** application on your installed macOS system. OCLP applies necessary system patches to re-enable these drivers.
|
||||
* This tool prepares the USB installer to be compatible with an OCLP workflow but **does not perform the root volume patching itself.**
|
||||
* **CUDA Support:** CUDA is dependent on NVIDIA's official driver stack, which is not available for newer macOS versions. Therefore, CUDA support is generally not achievable on macOS Mojave+ for NVIDIA cards.
|
||||
Modern macOS versions (Mojave and newer, including Ventura, Sonoma, and Sequoia) do not natively support NVIDIA Maxwell (e.g., GTX 970) or Pascal GPUs with graphics acceleration.
|
||||
|
||||
**How Skyscope Tool Helps:**
|
||||
|
||||
1. **Bootable Installer:** This tool will help you create a macOS USB installer with an OpenCore EFI configured to allow your system to boot with your NVIDIA card (either using an available Intel iGPU with the NVIDIA card disabled by `nv_disable=1`, or with the NVIDIA card providing basic, unaccelerated display if it's the only option).
|
||||
2. **OCLP Preparation:** The `config.plist` generated by this tool will include essential boot arguments (like `amfi_get_out_of_my_way=0x1`) and settings (`SecureBootModel=Disabled`) that are prerequisites for using the OpenCore Legacy Patcher (OCLP).
|
||||
|
||||
**User Action Required for NVIDIA Acceleration (Post-Install):**
|
||||
|
||||
* After you have installed macOS onto your PC's internal drive using the USB created by this tool, you **must run the OpenCore Legacy Patcher application from within your new macOS installation.**
|
||||
* OCLP will then apply the necessary system patches to the installed macOS system to enable graphics acceleration for your unsupported NVIDIA card.
|
||||
* This tool **does not** perform these system patches itself. It prepares your installer and EFI to be compatible with the OCLP process.
|
||||
* **CUDA:** CUDA support is tied to NVIDIA's official drivers, which are not available for newer macOS. OCLP primarily restores graphics (Metal/OpenGL/CL) acceleration, not the CUDA compute environment.
|
||||
|
||||
For macOS High Sierra or older, this tool can set `nvda_drv=1` if you intend to install NVIDIA Web Drivers (which you must source and install separately).
|
||||
|
||||
## Current Status & Known Limitations
|
||||
|
||||
* **Workflow Transition:** The project is currently transitioning from a Docker-OSX based method to a `gibMacOS`-based installer creation method. Not all platform-specific USB writers are fully refactored for this new approach yet.
|
||||
* **Windows USB Writing:** Creating the HFS+ macOS installer partition and copying files to it from Windows is complex without native HFS+ write support. The EFI part is automated; the main partition might initially require manual steps or use of `dd` for BaseSystem, with file copying being a challenge.
|
||||
* **`config.plist` Enhancement is Experimental:** Hardware detection for this feature is currently Linux-host only. The range of hardware automatically configured is limited to common setups.
|
||||
* **Universal Compatibility:** Hackintoshing is inherently hardware-dependent. While this tool aims for broad compatibility, success on every PC configuration cannot be guaranteed.
|
||||
* **Universal Compatibility:** While striving for broad compatibility, Hackintoshing is hardware-dependent. Success on every PC configuration cannot be guaranteed.
|
||||
* **Dependency on External Projects:** Relies on OpenCore and various community-sourced kexts and configurations. The `gibMacOS.py` script (or its underlying principles) is key for downloading assets.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Python:** Version 3.8 or newer.
|
||||
2. **Python Libraries:** `PyQt6`, `psutil`. Install via `pip install PyQt6 psutil`.
|
||||
3. **Core Utilities (all platforms, must be in PATH):**
|
||||
* `git` (used by `gibMacOS.py` and potentially for cloning other resources).
|
||||
* `7z` or `7za` (7-Zip command-line tool for archive extraction).
|
||||
4. **Platform-Specific CLI Tools for USB Writing:**
|
||||
3. **Core Utilities (All Platforms, in PATH):**
|
||||
* `git` (for `gibMacOS.py`).
|
||||
* `7z` or `7za` (7-Zip CLI for archive extraction).
|
||||
4. **`gibMacOS.py` Script:**
|
||||
* Clone `corpnewt/gibMacOS` (`git clone https://github.com/corpnewt/gibMacOS.git`) into a `scripts/gibMacOS` subdirectory within this project, or ensure `gibMacOS.py` is in the project root or system PATH and adjust `GIBMACOS_SCRIPT_PATH` in `main_app.py` if necessary.
|
||||
5. **Platform-Specific CLI Tools for USB Writing:**
|
||||
* **Linux (e.g., Debian 13 "Trixie"):**
|
||||
* `sgdisk`, `parted`, `partprobe` (from `gdisk`, `parted`, `util-linux`)
|
||||
* `mkfs.vfat` (from `dosfstools`)
|
||||
* `mkfs.hfsplus` (from `hfsprogs`)
|
||||
* `rsync`
|
||||
* `dd` (core utility)
|
||||
* `apfs-fuse`: Often requires manual compilation (e.g., from `sgan81/apfs-fuse` on GitHub). Typical build dependencies: `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev`. Ensure it's in your PATH.
|
||||
* `sgdisk` (from `gdisk`), `parted`, `partprobe` (from `util-linux`)
|
||||
* `mkfs.vfat` (from `dosfstools`), `mkfs.hfsplus` (from `hfsprogs`)
|
||||
* `rsync`, `dd`
|
||||
* `apfs-fuse`: Requires manual compilation (e.g., from `sgan81/apfs-fuse` on GitHub). Typical build dependencies: `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev`.
|
||||
* Install most via: `sudo apt update && sudo apt install gdisk parted dosfstools hfsprogs rsync util-linux p7zip-full` (or `p7zip`)
|
||||
* **macOS:**
|
||||
* `diskutil`, `hdiutil`, `rsync`, `cp`, `bless` (standard system tools).
|
||||
* `7z` (e.g., via Homebrew: `brew install p7zip`).
|
||||
* **Windows:**
|
||||
* `diskpart`, `robocopy` (standard system tools).
|
||||
* `7z.exe` (install and add to PATH).
|
||||
* A "dd for Windows" utility (user must install and ensure it's in PATH).
|
||||
* **macOS:** `diskutil`, `hdiutil`, `rsync`, `cp`, `dd`, `bless`. `7z` (e.g., `brew install p7zip`).
|
||||
* **Windows:** `diskpart`, `robocopy`. `7z.exe`. A "dd for Windows" utility.
|
||||
|
||||
## How to Run (Development Phase)
|
||||
|
||||
1. Ensure all prerequisites for your OS are met.
|
||||
2. Clone this repository.
|
||||
3. **Crucial:** Clone `corpnewt/gibMacOS` into a `./scripts/gibMacOS/` subdirectory within this project, or ensure `gibMacOS.py` is in the project root or your system PATH and update `GIBMACOS_SCRIPT_PATH` in `main_app.py` if necessary.
|
||||
4. Install Python libraries: `pip install PyQt6 psutil`.
|
||||
5. Execute `python main_app.py`.
|
||||
6. **For USB Writing Operations:**
|
||||
1. Meet all prerequisites for your OS, including `gibMacOS.py` setup.
|
||||
2. Clone this repository. Install Python libs: `pip install PyQt6 psutil`.
|
||||
3. Execute `python main_app.py`.
|
||||
4. **For USB Writing Operations:**
|
||||
* **Linux:** Run with `sudo python main_app.py`.
|
||||
* **macOS:** Run normally. You may be prompted for your password by system commands like `diskutil` or `sudo rsync`. Ensure the app has Full Disk Access if needed.
|
||||
* **macOS:** Run normally. May prompt for password for `sudo rsync` or `diskutil`. Ensure the app has Full Disk Access if needed.
|
||||
* **Windows:** Run as Administrator.
|
||||
|
||||
## Step-by-Step Usage Guide (New Workflow)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# usb_writer_linux.py (Refined asset copying)
|
||||
# usb_writer_linux.py (Finalizing installer asset copying - refined)
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
|
|
@ -6,66 +6,95 @@ import shutil
|
|||
import glob
|
||||
import re
|
||||
import plistlib
|
||||
import traceback
|
||||
|
||||
try:
|
||||
from plist_modifier import enhance_config_plist
|
||||
except ImportError:
|
||||
enhance_config_plist = None
|
||||
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled for USBWriterLinux.")
|
||||
# from constants import MACOS_VERSIONS # Imported in _get_gibmacos_product_folder
|
||||
|
||||
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
|
||||
|
||||
|
||||
class USBWriterLinux:
|
||||
def __init__(self, device: str, macos_download_path: str,
|
||||
progress_callback=None, enhance_plist_enabled: bool = False,
|
||||
target_macos_version: str = ""): # target_macos_version is display name e.g. "Sonoma"
|
||||
self.device = device
|
||||
self.macos_download_path = macos_download_path
|
||||
self.progress_callback = progress_callback
|
||||
self.enhance_plist_enabled = enhance_plist_enabled
|
||||
self.target_macos_version = target_macos_version
|
||||
|
||||
pid = os.getpid()
|
||||
target_macos_version: str = ""):
|
||||
self.device = device; self.macos_download_path = macos_download_path
|
||||
self.progress_callback = progress_callback; self.enhance_plist_enabled = enhance_plist_enabled
|
||||
self.target_macos_version = target_macos_version; pid = os.getpid()
|
||||
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
|
||||
self.temp_efi_build_dir = f"temp_efi_build_{pid}"
|
||||
self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For extracting HFS from DMG
|
||||
|
||||
self.mount_point_usb_esp = f"/mnt/usb_esp_temp_skyscope_{pid}"
|
||||
self.mount_point_usb_macos_target = f"/mnt/usb_macos_target_temp_skyscope_{pid}"
|
||||
self.temp_shared_support_mount = f"/mnt/shared_support_temp_{pid}"
|
||||
self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # Added for _extract_hfs_from_dmg_or_pkg
|
||||
|
||||
self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
|
||||
self.temp_dirs_to_clean = [
|
||||
self.temp_efi_build_dir, self.mount_point_usb_esp,
|
||||
self.mount_point_usb_macos_target, self.temp_dmg_extract_dir
|
||||
self.mount_point_usb_macos_target, self.temp_shared_support_mount,
|
||||
self.temp_dmg_extract_dir # Ensure this is cleaned
|
||||
]
|
||||
|
||||
def _report_progress(self, message: str):
|
||||
if self.progress_callback: self.progress_callback(message)
|
||||
else: print(message)
|
||||
def _report_progress(self, message: str, is_rsync_line: bool = False):
|
||||
if is_rsync_line:
|
||||
match = re.search(r"(\d+)%\s+", message)
|
||||
if match:
|
||||
try: percentage = int(match.group(1)); self.progress_callback(f"PROGRESS_VALUE:{percentage}")
|
||||
except ValueError: pass
|
||||
if self.progress_callback: self.progress_callback(message)
|
||||
else: print(message)
|
||||
else:
|
||||
if self.progress_callback: self.progress_callback(message)
|
||||
else: print(message)
|
||||
|
||||
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None):
|
||||
self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}")
|
||||
try:
|
||||
process = subprocess.run(
|
||||
command, check=check, capture_output=capture_output, text=True, timeout=timeout,
|
||||
shell=shell, cwd=working_dir,
|
||||
creationflags=0
|
||||
)
|
||||
if capture_output:
|
||||
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
|
||||
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
|
||||
return process
|
||||
except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
|
||||
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
|
||||
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise
|
||||
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None, stream_rsync_progress=False):
|
||||
cmd_list = command if isinstance(command, list) else command.split()
|
||||
is_rsync_progress_command = stream_rsync_progress and "rsync" in cmd_list[0 if cmd_list[0] != "sudo" else (1 if len(cmd_list) > 1 else 0)]
|
||||
|
||||
if is_rsync_progress_command:
|
||||
effective_cmd_list = list(cmd_list)
|
||||
rsync_idx = -1
|
||||
for i, arg in enumerate(effective_cmd_list):
|
||||
if "rsync" in arg: rsync_idx = i; break
|
||||
if rsync_idx != -1:
|
||||
conflicting_flags = ["-P", "--progress"]; effective_cmd_list = [arg for arg in effective_cmd_list if arg not in conflicting_flags]
|
||||
actual_rsync_cmd_index_in_list = -1
|
||||
for i, arg_part in enumerate(effective_cmd_list):
|
||||
if "rsync" in os.path.basename(arg_part): actual_rsync_cmd_index_in_list = i; break
|
||||
if actual_rsync_cmd_index_in_list != -1:
|
||||
if "--info=progress2" not in effective_cmd_list: effective_cmd_list.insert(actual_rsync_cmd_index_in_list + 1, "--info=progress2")
|
||||
if "--no-inc-recursive" not in effective_cmd_list : effective_cmd_list.insert(actual_rsync_cmd_index_in_list + 1, "--no-inc-recursive")
|
||||
else: self._report_progress("Warning: rsync command part not found for progress flag insertion.")
|
||||
self._report_progress(f"Executing (with progress streaming): {' '.join(effective_cmd_list)}")
|
||||
process = subprocess.Popen(effective_cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True, cwd=working_dir)
|
||||
stdout_lines, stderr_lines = [], []
|
||||
if process.stdout:
|
||||
for line in iter(process.stdout.readline, ''): line_strip = line.strip(); self._report_progress(line_strip, is_rsync_line=True); stdout_lines.append(line_strip)
|
||||
process.stdout.close()
|
||||
if process.stderr:
|
||||
for line in iter(process.stderr.readline, ''): line_strip = line.strip(); self._report_progress(f"STDERR: {line_strip}"); stderr_lines.append(line_strip)
|
||||
process.stderr.close()
|
||||
return_code = process.wait(timeout=timeout);
|
||||
if check and return_code != 0: raise subprocess.CalledProcessError(return_code, effective_cmd_list, output="\n".join(stdout_lines), stderr="\n".join(stderr_lines))
|
||||
return subprocess.CompletedProcess(args=effective_cmd_list, returncode=return_code, stdout="\n".join(stdout_lines), stderr="\n".join(stderr_lines))
|
||||
else:
|
||||
self._report_progress(f"Executing: {' '.join(cmd_list)}")
|
||||
try:
|
||||
process = subprocess.run(cmd_list, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=0)
|
||||
if capture_output:
|
||||
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
|
||||
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
|
||||
return process
|
||||
except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
|
||||
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
|
||||
except FileNotFoundError: self._report_progress(f"Error: Command '{cmd_list[0]}' not found."); raise
|
||||
|
||||
def _cleanup_temp_files_and_dirs(self):
|
||||
self._report_progress("Cleaning up temporary files and directories...")
|
||||
self._report_progress("Cleaning up...")
|
||||
for mp in self.temp_dirs_to_clean:
|
||||
if os.path.ismount(mp):
|
||||
self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15)
|
||||
|
||||
if os.path.ismount(mp): self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15)
|
||||
for f_path in self.temp_files_to_clean:
|
||||
if os.path.exists(f_path):
|
||||
try: self._run_command(["sudo", "rm", "-f", f_path], check=False)
|
||||
|
|
@ -75,278 +104,205 @@ class USBWriterLinux:
|
|||
try: self._run_command(["sudo", "rm", "-rf", d_path], check=False)
|
||||
except Exception as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
|
||||
|
||||
def check_dependencies(self):
|
||||
self._report_progress("Checking dependencies (sgdisk, mkfs.vfat, mkfs.hfsplus, 7z, rsync, dd)...")
|
||||
dependencies = ["sgdisk", "mkfs.vfat", "mkfs.hfsplus", "7z", "rsync", "dd"]
|
||||
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
|
||||
if missing_deps:
|
||||
msg = f"Missing dependencies: {', '.join(missing_deps)}. Please install them (e.g., hfsprogs, p7zip-full, gdisk)."
|
||||
self._report_progress(msg); raise RuntimeError(msg)
|
||||
self._report_progress("All critical dependencies for Linux USB installer creation found.")
|
||||
return True
|
||||
def check_dependencies(self): self._report_progress("Checking deps...");deps=["sgdisk","parted","mkfs.vfat","mkfs.hfsplus","7z","rsync","dd"];m=[d for d in deps if not shutil.which(d)]; assert not m, f"Missing: {', '.join(m)}. Install hfsprogs for mkfs.hfsplus, p7zip for 7z."; return True
|
||||
|
||||
def _get_gibmacos_product_folder(self) -> str:
|
||||
"""Heuristically finds the main product folder within gibMacOS downloads."""
|
||||
# gibMacOS often creates .../publicrelease/XXX - macOS [VersionName] [VersionNum]/
|
||||
# We need to find this folder.
|
||||
_report = self._report_progress
|
||||
_report(f"Searching for macOS product folder in {self.macos_download_path} for version {self.target_macos_version}")
|
||||
|
||||
version_parts = self.target_macos_version.split(" ") # e.g., "Sonoma" or "Mac OS X", "High Sierra"
|
||||
primary_name = version_parts[0] # "Sonoma", "Mac", "High"
|
||||
if primary_name == "Mac" and len(version_parts) > 2 and version_parts[1] == "OS": # "Mac OS X"
|
||||
primary_name = "OS X"
|
||||
if len(version_parts) > 2 and version_parts[2] == "X": primary_name = "OS X" # For "Mac OS X"
|
||||
|
||||
possible_folders = []
|
||||
for root, dirs, _ in os.walk(self.macos_download_path):
|
||||
for d_name in dirs:
|
||||
# Check if directory name contains "macOS" and a part of the target version name/number
|
||||
if "macOS" in d_name and (primary_name in d_name or self.target_macos_version in d_name):
|
||||
possible_folders.append(os.path.join(root, d_name))
|
||||
|
||||
if not possible_folders:
|
||||
_report(f"Could not automatically determine specific product folder. Using base download path: {self.macos_download_path}")
|
||||
return self.macos_download_path
|
||||
|
||||
# Prefer shorter paths or more specific matches if multiple found
|
||||
# This heuristic might need refinement. For now, take the first plausible one.
|
||||
_report(f"Found potential product folder(s): {possible_folders}. Using: {possible_folders[0]}")
|
||||
return possible_folders[0]
|
||||
|
||||
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder: str, description: str) -> str | None:
|
||||
"""Finds the first existing file matching a list of glob patterns within the product_folder."""
|
||||
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None:
|
||||
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
|
||||
self._report_progress(f"Searching for {description} using patterns {asset_patterns} in {product_folder}...")
|
||||
search_base = product_folder_path or self.macos_download_path
|
||||
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
|
||||
for pattern in asset_patterns:
|
||||
# Search both in root of product_folder and common subdirs like "SharedSupport" or "*.app/Contents/SharedSupport"
|
||||
search_glob_patterns = [
|
||||
os.path.join(product_folder, pattern),
|
||||
os.path.join(product_folder, "**", pattern), # Recursive search
|
||||
]
|
||||
for glob_pattern in search_glob_patterns:
|
||||
found_files = glob.glob(glob_pattern, recursive=True)
|
||||
common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"]
|
||||
for sub_dir_pattern in common_subdirs_for_pattern:
|
||||
current_search_base = os.path.join(search_base, sub_dir_pattern)
|
||||
# Escape special characters for glob, but allow wildcards in pattern itself
|
||||
# This simple escape might not be perfect for all glob patterns.
|
||||
glob_pattern = os.path.join(glob.escape(current_search_base), pattern)
|
||||
|
||||
found_files = glob.glob(glob_pattern, recursive=False)
|
||||
if found_files:
|
||||
# Sort to get a predictable one if multiple (e.g. if pattern is too generic)
|
||||
# Prefer files not too deep in structure if multiple found by simple pattern
|
||||
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
|
||||
self._report_progress(f"Found {description} at: {found_files[0]}")
|
||||
found_files.sort(key=os.path.getsize, reverse=True)
|
||||
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
|
||||
return found_files[0]
|
||||
self._report_progress(f"Warning: {description} not found with patterns: {asset_patterns} in {product_folder} or its subdirectories.")
|
||||
|
||||
if search_deep:
|
||||
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
|
||||
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
|
||||
if found_files_deep:
|
||||
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
|
||||
return found_files_deep[0]
|
||||
|
||||
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
|
||||
return None
|
||||
|
||||
def _extract_basesystem_hfs_from_source(self, source_dmg_path: str, output_hfs_path: str) -> bool:
|
||||
"""Extracts the primary HFS+ partition image (e.g., '4.hfs') from a source DMG (BaseSystem.dmg or InstallESD.dmg)."""
|
||||
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True)
|
||||
def _get_gibmacos_product_folder(self) -> str | None:
|
||||
from constants import MACOS_VERSIONS
|
||||
base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease")
|
||||
if not os.path.isdir(base_path): base_path = self.macos_download_path
|
||||
if os.path.isdir(base_path):
|
||||
for item in os.listdir(base_path):
|
||||
item_path = os.path.join(base_path, item)
|
||||
version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
|
||||
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag_from_constants in item.lower()):
|
||||
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path
|
||||
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path
|
||||
|
||||
def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
|
||||
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path
|
||||
try:
|
||||
self._report_progress(f"Extracting HFS+ partition image from {source_dmg_path} into {self.temp_dmg_extract_dir}...")
|
||||
# 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar for BaseSystem)
|
||||
# For InstallESD.dmg, it might be a different internal path or structure.
|
||||
# Assuming the target is a standard BaseSystem.dmg or a DMG containing such structure.
|
||||
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
|
||||
|
||||
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"))
|
||||
if not hfs_files:
|
||||
# Fallback: try extracting * (if only one file inside a simple DMG, like some custom BaseSystem.dmg)
|
||||
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*", f"-o{self.temp_dmg_extract_dir}"], check=True)
|
||||
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*")) # Check all files
|
||||
hfs_files = [f for f in hfs_files if not f.endswith((".xml", ".chunklist", ".plist")) and os.path.getsize(f) > 100*1024*1024] # Filter out small/meta files
|
||||
|
||||
if not hfs_files: raise RuntimeError(f"No suitable .hfs image found after extracting {source_dmg_path}")
|
||||
|
||||
final_hfs_file = max(hfs_files, key=os.path.getsize) # Assume largest is the one
|
||||
self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}")
|
||||
shutil.move(final_hfs_file, output_hfs_path) # Use shutil.move for local files
|
||||
return True
|
||||
except Exception as e:
|
||||
self._report_progress(f"Error during HFS extraction from DMG: {e}\n{traceback.format_exc()}")
|
||||
return False
|
||||
if dmg_or_pkg_path.endswith(".pkg"): self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True); dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert dmgs_in_pkg, "No DMG in PKG."; current_target = max(dmgs_in_pkg, key=os.path.getsize, default=dmgs_in_pkg[0]); assert current_target, "No primary DMG in PKG."; self._report_progress(f"Using DMG from PKG: {current_target}")
|
||||
assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}"
|
||||
basesystem_dmg_to_process = current_target
|
||||
if "basesystem.dmg" not in os.path.basename(current_target).lower(): self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True); found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True); assert found_bs_dmg, f"No BaseSystem.dmg from {current_target}"; basesystem_dmg_to_process = found_bs_dmg[0]
|
||||
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
|
||||
if not hfs_files: self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 2*1024*1024*1024] # Min 2GB HFS for BaseSystem
|
||||
assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}"
|
||||
final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True
|
||||
except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
|
||||
finally:
|
||||
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
|
||||
|
||||
def _create_minimal_efi_template(self, efi_dir_path):
|
||||
self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}"); oc_dir=os.path.join(efi_dir_path,"EFI","OC");os.makedirs(os.path.join(efi_dir_path,"EFI","BOOT"),exist_ok=True);os.makedirs(oc_dir,exist_ok=True);[os.makedirs(os.path.join(oc_dir,s),exist_ok=True) for s in ["Drivers","Kexts","ACPI","Tools","Resources"]];open(os.path.join(efi_dir_path,"EFI","BOOT","BOOTx64.efi"),"w").close();open(os.path.join(oc_dir,"OpenCore.efi"),"w").close();bc={"#Comment":"Basic config","Misc":{"Security":{"ScanPolicy":0,"SecureBootModel":"Disabled"}},"PlatformInfo":{"Generic":{"MLB":"CHANGE_ME_MLB","SystemSerialNumber":"CHANGE_ME_SERIAL","SystemUUID":"CHANGE_ME_UUID","ROM":b"\0"*6}}};plistlib.dump(bc,open(os.path.join(oc_dir,"config.plist"),'wb'),fmt=plistlib.PlistFormat.XML)
|
||||
|
||||
def format_and_write(self) -> bool:
|
||||
try:
|
||||
self.check_dependencies()
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
for mp in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]:
|
||||
self._run_command(["sudo", "mkdir", "-p", mp])
|
||||
|
||||
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
|
||||
self.check_dependencies(); self._cleanup_temp_files_and_dirs();
|
||||
for mp_dir in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]: self._run_command(["sudo", "mkdir", "-p", mp_dir])
|
||||
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!");
|
||||
for i in range(1, 10): self._run_command(["sudo", "umount", "-lf", f"{self.device}{i}"], check=False, timeout=5); self._run_command(["sudo", "umount", "-lf", f"{self.device}p{i}"], check=False, timeout=5)
|
||||
|
||||
self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...")
|
||||
self._run_command(["sudo", "sgdisk", "--zap-all", self.device])
|
||||
self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device])
|
||||
self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", f"2:Install macOS {self.target_macos_version}", self.device])
|
||||
self._run_command(["sudo", "sgdisk", "-n", "0:0:+551MiB", "-t", "0:ef00", "-c", "0:EFI", self.device])
|
||||
usb_vol_name = f"Install macOS {self.target_macos_version}"
|
||||
self._run_command(["sudo", "sgdisk", "-n", "0:0:0", "-t", "0:af00", "-c", f"0:{usb_vol_name[:11]}" , self.device])
|
||||
self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3)
|
||||
esp_dev=f"{self.device}1" if os.path.exists(f"{self.device}1") else f"{self.device}p1"; macos_part=f"{self.device}2" if os.path.exists(f"{self.device}2") else f"{self.device}p2"; assert os.path.exists(esp_dev) and os.path.exists(macos_part), "Partitions not found."
|
||||
self._report_progress(f"Formatting ESP {esp_dev}..."); self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_dev])
|
||||
self._report_progress(f"Formatting macOS partition {macos_part}..."); self._run_command(["sudo", "mkfs.hfsplus", "-v", usb_vol_name, macos_part])
|
||||
|
||||
esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None)
|
||||
macos_partition_dev = next((f"{self.device}{i}" for i in ["2", "p2"] if os.path.exists(f"{self.device}{i}")), None)
|
||||
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not reliably determine partition names for {self.device}.")
|
||||
product_folder_path = self._get_gibmacos_product_folder()
|
||||
basesystem_source_dmg_or_pkg = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)")
|
||||
if not basesystem_source_dmg_or_pkg: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.")
|
||||
if not self._extract_hfs_from_dmg_or_pkg(basesystem_source_dmg_or_pkg, self.temp_basesystem_hfs_path):
|
||||
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
|
||||
self._report_progress(f"Writing BaseSystem to {macos_part}..."); self._run_command(["sudo","dd",f"if={self.temp_basesystem_hfs_path}",f"of={macos_part}","bs=4M","status=progress","oflag=sync"])
|
||||
self._report_progress("Mounting macOS USB partition..."); self._run_command(["sudo","mount",macos_part,self.mount_point_usb_macos_target])
|
||||
|
||||
self._report_progress(f"Formatting ESP ({esp_partition_dev}) as FAT32...")
|
||||
self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_partition_dev])
|
||||
self._report_progress(f"Formatting macOS Install partition ({macos_partition_dev}) as HFS+...")
|
||||
self._run_command(["sudo", "mkfs.hfsplus", "-v", f"Install macOS {self.target_macos_version}", macos_partition_dev])
|
||||
# --- Finalizing macOS Installer Content on USB's HFS+ partition ---
|
||||
self._report_progress("Finalizing macOS installer content on USB...")
|
||||
usb_target_root = self.mount_point_usb_macos_target
|
||||
|
||||
# --- Prepare macOS Installer Content ---
|
||||
product_folder = self._get_gibmacos_product_folder()
|
||||
app_bundle_name = f"Install macOS {self.target_macos_version}.app"
|
||||
app_bundle_path_usb = os.path.join(usb_target_root, app_bundle_name)
|
||||
contents_path_usb = os.path.join(app_bundle_path_usb, "Contents")
|
||||
shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport")
|
||||
resources_path_usb_app = os.path.join(contents_path_usb, "Resources") # For createinstallmedia structure
|
||||
sys_install_pkgs_usb = os.path.join(usb_target_root, "System", "Installation", "Packages")
|
||||
coreservices_path_usb = os.path.join(usb_target_root, "System", "Library", "CoreServices")
|
||||
|
||||
# Find BaseSystem.dmg (or equivalent like InstallESD.dmg if BaseSystem.dmg is not directly available)
|
||||
# Some gibMacOS downloads might have InstallESD.dmg which contains BaseSystem.dmg.
|
||||
# Others might have BaseSystem.dmg directly.
|
||||
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
|
||||
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
|
||||
for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]:
|
||||
self._run_command(["sudo", "mkdir", "-p", p])
|
||||
|
||||
self._report_progress("Extracting bootable HFS+ image from source DMG...")
|
||||
if not self._extract_basesystem_hfs_from_source(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
|
||||
raise RuntimeError("Failed to extract HFS+ image from source DMG.")
|
||||
# Copy BaseSystem.dmg & BaseSystem.chunklist
|
||||
bs_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=True)
|
||||
bs_chunklist_src = self._find_gibmacos_asset("BaseSystem.chunklist", product_folder_path, search_deep=True)
|
||||
if bs_dmg_src:
|
||||
self._report_progress(f"Copying BaseSystem.dmg to USB CoreServices and App SharedSupport...")
|
||||
self._run_command(["sudo", "cp", bs_dmg_src, os.path.join(coreservices_path_usb, "BaseSystem.dmg")])
|
||||
self._run_command(["sudo", "cp", bs_dmg_src, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")])
|
||||
if bs_chunklist_src:
|
||||
self._report_progress(f"Copying BaseSystem.chunklist to USB CoreServices and App SharedSupport...")
|
||||
self._run_command(["sudo", "cp", bs_chunklist_src, os.path.join(coreservices_path_usb, "BaseSystem.chunklist")])
|
||||
self._run_command(["sudo", "cp", bs_chunklist_src, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")])
|
||||
if not bs_dmg_src or not bs_chunklist_src: self._report_progress("Warning: BaseSystem.dmg or .chunklist not found in product folder.")
|
||||
|
||||
self._report_progress(f"Writing BaseSystem HFS+ image to {macos_partition_dev} using dd...")
|
||||
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"])
|
||||
# Copy InstallInfo.plist
|
||||
installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True)
|
||||
if installinfo_src:
|
||||
self._report_progress(f"Copying InstallInfo.plist...")
|
||||
self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")])
|
||||
self._run_command(["sudo", "cp", installinfo_src, os.path.join(usb_target_root, "InstallInfo.plist")])
|
||||
else: self._report_progress("Warning: InstallInfo.plist (source) not found.")
|
||||
|
||||
self._report_progress("Mounting macOS Install partition on USB...")
|
||||
self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target])
|
||||
# Copy main installer package(s)
|
||||
main_pkg_src = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, search_deep=True) or self._find_gibmacos_asset("InstallESD.dmg", product_folder_path, search_deep=True)
|
||||
if main_pkg_src:
|
||||
pkg_basename = os.path.basename(main_pkg_src)
|
||||
self._report_progress(f"Copying main payload '{pkg_basename}' to App SharedSupport and System Packages...")
|
||||
self._run_command(["sudo", "cp", main_pkg_src, os.path.join(shared_support_path_usb_app, pkg_basename)])
|
||||
self._run_command(["sudo", "cp", main_pkg_src, os.path.join(sys_install_pkgs_usb, pkg_basename)])
|
||||
else: self._report_progress("Warning: Main installer package (InstallAssistant.pkg/InstallESD.dmg) not found.")
|
||||
|
||||
core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices")
|
||||
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
|
||||
diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True)
|
||||
if diag_src: self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")])
|
||||
|
||||
# Copy original BaseSystem.dmg and .chunklist from gibMacOS output
|
||||
original_bs_dmg = self._find_gibmacos_asset(["BaseSystem.dmg"], product_folder, "original BaseSystem.dmg")
|
||||
if original_bs_dmg:
|
||||
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg")
|
||||
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")])
|
||||
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist")
|
||||
if os.path.exists(original_bs_chunklist):
|
||||
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
|
||||
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
|
||||
else: self._report_progress("Warning: Original BaseSystem.dmg not found in product folder to copy to CoreServices.")
|
||||
template_boot_efi = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi")
|
||||
if os.path.exists(template_boot_efi) and os.path.getsize(template_boot_efi) > 0:
|
||||
self._run_command(["sudo", "cp", template_boot_efi, os.path.join(coreservices_path_usb, "boot.efi")])
|
||||
else: self._report_progress(f"Warning: Template BOOTx64.efi for installer's boot.efi not found or empty.")
|
||||
|
||||
install_info_src = self._find_gibmacos_asset(["InstallInfo.plist"], product_folder, "InstallInfo.plist")
|
||||
if install_info_src:
|
||||
self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist")
|
||||
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")])
|
||||
else: self._report_progress("Warning: InstallInfo.plist not found in product folder.")
|
||||
|
||||
# Copy Packages and other assets
|
||||
packages_target_path = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")
|
||||
self._run_command(["sudo", "mkdir", "-p", packages_target_path])
|
||||
|
||||
# Try to find and copy InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg contents for packages
|
||||
# This part is complex, as gibMacOS output varies.
|
||||
# If InstallAssistant.pkg is found, its contents (especially packages) are needed.
|
||||
# If SharedSupport.dmg is found, its contents are needed.
|
||||
install_assistant_pkg = self._find_gibmacos_asset(["InstallAssistant.pkg"], product_folder, "InstallAssistant.pkg")
|
||||
if install_assistant_pkg:
|
||||
self._report_progress(f"Copying contents of InstallAssistant.pkg (Packages) from {os.path.dirname(install_assistant_pkg)} to {packages_target_path} (simplified, may need selective copy)")
|
||||
# This is a placeholder. Real logic would extract from PKG or copy specific subfolders/files.
|
||||
# For now, just copy the PKG itself as an example.
|
||||
self._run_command(["sudo", "cp", install_assistant_pkg, packages_target_path])
|
||||
else:
|
||||
shared_support_dmg = self._find_gibmacos_asset(["SharedSupport.dmg"], product_folder, "SharedSupport.dmg for packages")
|
||||
if shared_support_dmg:
|
||||
self._report_progress(f"Copying contents of SharedSupport.dmg from {shared_support_dmg} to {packages_target_path} (simplified)")
|
||||
# Mount SharedSupport.dmg and rsync contents, or 7z extract and rsync
|
||||
# Placeholder: copy the DMG itself. Real solution needs extraction.
|
||||
self._run_command(["sudo", "cp", shared_support_dmg, packages_target_path])
|
||||
else:
|
||||
self._report_progress("Warning: Neither InstallAssistant.pkg nor SharedSupport.dmg found for main packages. Installer may be incomplete.")
|
||||
|
||||
# Create 'Install macOS [Version].app' structure (simplified)
|
||||
app_name = f"Install macOS {self.target_macos_version}.app"
|
||||
app_path_usb = os.path.join(self.mount_point_usb_macos_target, app_name)
|
||||
self._run_command(["sudo", "mkdir", "-p", os.path.join(app_path_usb, "Contents", "SharedSupport")])
|
||||
# Copying some key files into this structure might be needed too.
|
||||
|
||||
# --- OpenCore EFI Setup --- (same as before, but using self.temp_efi_build_dir)
|
||||
self._report_progress("Setting up OpenCore EFI on ESP...")
|
||||
if not os.path.isdir(OC_TEMPLATE_DIR): self._report_progress(f"FATAL: OpenCore template dir not found: {OC_TEMPLATE_DIR}"); return False
|
||||
|
||||
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
|
||||
self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
|
||||
# Create .IAProductInfo (Simplified XML string to avoid f-string issues in tool call)
|
||||
ia_product_info_path = os.path.join(usb_target_root, ".IAProductInfo")
|
||||
ia_content_xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plist version=\"1.0\"><dict><key>Product ID</key><string>com.apple.pkg.InstallAssistant</string><key>Product Path</key><string>" + app_bundle_name + "/Contents/SharedSupport/InstallAssistant.pkg</string></dict></plist>"
|
||||
temp_ia_path = f"temp_iaproductinfo_{pid}.plist"
|
||||
with open(temp_ia_path, "w") as f: f.write(ia_content_xml)
|
||||
self._run_command(["sudo", "cp", temp_ia_path, ia_product_info_path])
|
||||
if os.path.exists(temp_ia_path): os.remove(temp_ia_path)
|
||||
self._report_progress("Created .IAProductInfo.")
|
||||
self._report_progress("macOS installer assets fully copied to USB.")
|
||||
|
||||
# --- OpenCore EFI Setup ---
|
||||
self._report_progress("Setting up OpenCore EFI on ESP..."); self._run_command(["sudo", "mount", esp_dev, self.mount_point_usb_esp])
|
||||
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir)
|
||||
else: self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
|
||||
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
|
||||
# If template is config-template.plist, rename it for enhancement
|
||||
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
|
||||
self._run_command(["sudo", "mv", os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path])
|
||||
|
||||
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
|
||||
if not os.path.exists(temp_config_plist_path):
|
||||
template_plist = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
|
||||
if os.path.exists(template_plist): shutil.copy2(template_plist, temp_config_plist_path)
|
||||
else: plistlib.dump({"#Comment": "Basic config by Skyscope"}, open(temp_config_plist_path, 'wb'), fmt=plistlib.PlistFormat.XML)
|
||||
if self.enhance_plist_enabled and enhance_config_plist:
|
||||
self._report_progress("Attempting to enhance config.plist...")
|
||||
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement successful.")
|
||||
else: self._report_progress("config.plist enhancement failed or had issues.")
|
||||
|
||||
self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp])
|
||||
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
|
||||
else: self._report_progress("config.plist enhancement call failed or had issues.")
|
||||
self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...")
|
||||
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"])
|
||||
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"], stream_rsync_progress=True)
|
||||
|
||||
self._report_progress("USB Installer creation process completed successfully.")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._report_progress(f"An error occurred during USB writing: {e}\n{traceback.format_exc()}")
|
||||
self._report_progress(f"An error occurred during USB writing: {e}"); self._report_progress(traceback.format_exc())
|
||||
return False
|
||||
finally:
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import traceback; from constants import MACOS_VERSIONS
|
||||
if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1)
|
||||
print("USB Writer Linux Standalone Test - Installer Method (Refined)")
|
||||
|
||||
mock_download_dir = f"temp_macos_download_test_{os.getpid()}"
|
||||
os.makedirs(mock_download_dir, exist_ok=True)
|
||||
|
||||
# Create a more structured mock download similar to gibMacOS output
|
||||
product_name_slug = f"000-00000 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.0" # Example
|
||||
specific_product_folder = os.path.join(mock_download_dir, "publicrelease", product_name_slug)
|
||||
os.makedirs(specific_product_folder, exist_ok=True)
|
||||
|
||||
# Mock BaseSystem.dmg (tiny, not functional, for path testing)
|
||||
dummy_bs_dmg_path = os.path.join(specific_product_folder, "BaseSystem.dmg")
|
||||
if not os.path.exists(dummy_bs_dmg_path):
|
||||
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy
|
||||
|
||||
# Mock BaseSystem.chunklist
|
||||
dummy_bs_chunklist_path = os.path.join(specific_product_folder, "BaseSystem.chunklist")
|
||||
if not os.path.exists(dummy_bs_chunklist_path):
|
||||
with open(dummy_bs_chunklist_path, "w") as f: f.write("dummy chunklist")
|
||||
|
||||
# Mock InstallInfo.plist
|
||||
dummy_installinfo_path = os.path.join(specific_product_folder, "InstallInfo.plist")
|
||||
if not os.path.exists(dummy_installinfo_path):
|
||||
with open(dummy_installinfo_path, "w") as f: plistlib.dump({"DummyInstallInfo": True}, f)
|
||||
|
||||
# Mock InstallAssistant.pkg (empty for now, just to test its presence)
|
||||
dummy_pkg_path = os.path.join(specific_product_folder, "InstallAssistant.pkg")
|
||||
if not os.path.exists(dummy_pkg_path):
|
||||
with open(dummy_pkg_path, "wb") as f: f.write(os.urandom(1024))
|
||||
|
||||
|
||||
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
|
||||
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
|
||||
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
|
||||
if not os.path.exists(dummy_config_template_path):
|
||||
with open(dummy_config_template_path, "w") as f: f.write("<plist><dict><key>TestTemplate</key><true/></dict></plist>")
|
||||
|
||||
print("\nAvailable block devices (be careful!):")
|
||||
subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True)
|
||||
print("USB Writer Linux Standalone Test - Installer Method (Fuller Asset Copying Logic)")
|
||||
mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
|
||||
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
|
||||
mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
|
||||
mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
|
||||
specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
|
||||
os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True); os.makedirs(specific_product_folder, exist_ok=True)
|
||||
with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
|
||||
with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist")
|
||||
with open(os.path.join(specific_product_folder, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f)
|
||||
with open(os.path.join(specific_product_folder, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024))
|
||||
with open(os.path.join(specific_product_folder, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024))
|
||||
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
|
||||
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True)
|
||||
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True)
|
||||
with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML)
|
||||
with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("dummy bootx64.efi")
|
||||
print("\nAvailable block devices (be careful!):"); subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True)
|
||||
test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ")
|
||||
|
||||
if not test_device or not test_device.startswith("/dev/"):
|
||||
print("Invalid device. Exiting.")
|
||||
else:
|
||||
confirm = input(f"Are you absolutely sure you want to wipe {test_device} and create installer? (yes/NO): ")
|
||||
success = False
|
||||
if confirm.lower() == 'yes':
|
||||
writer = USBWriterLinux(
|
||||
device=test_device,
|
||||
macos_download_path=mock_download_dir, # Pass base download dir
|
||||
progress_callback=print,
|
||||
enhance_plist_enabled=True,
|
||||
target_macos_version=sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
|
||||
)
|
||||
success = writer.format_and_write()
|
||||
else: print("Test cancelled by user.")
|
||||
print(f"Test finished. Success: {success}")
|
||||
|
||||
if os.path.exists(mock_download_dir): shutil.rmtree(mock_download_dir, ignore_errors=True)
|
||||
if not test_device or not test_device.startswith("/dev/"): print("Invalid device."); shutil.rmtree(mock_download_dir); shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True); exit(1)
|
||||
if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes':
|
||||
writer = USBWriterLinux(test_device, mock_download_dir, print, True, target_version_cli)
|
||||
writer.format_and_write()
|
||||
else: print("Test cancelled.")
|
||||
shutil.rmtree(mock_download_dir, ignore_errors=True);
|
||||
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template dir for other tests
|
||||
print("Mock download dir cleaned up.")
|
||||
|
|
|
|||
|
|
@ -15,38 +15,46 @@ except ImportError:
|
|||
|
||||
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
|
||||
|
||||
try:
|
||||
from constants import MACOS_VERSIONS
|
||||
except ImportError:
|
||||
MACOS_VERSIONS = {"Sonoma": "14", "Ventura": "13", "Monterey": "12"}
|
||||
print("Warning: constants.py not found, using fallback MACOS_VERSIONS for _get_gibmacos_product_folder.")
|
||||
|
||||
|
||||
class USBWriterMacOS:
|
||||
def __init__(self, device: str, macos_download_path: str,
|
||||
progress_callback=None, enhance_plist_enabled: bool = False,
|
||||
target_macos_version: str = ""):
|
||||
self.device = device # e.g., /dev/diskX
|
||||
self.device = device
|
||||
self.macos_download_path = macos_download_path
|
||||
self.progress_callback = progress_callback
|
||||
self.enhance_plist_enabled = enhance_plist_enabled
|
||||
self.target_macos_version = target_macos_version
|
||||
|
||||
pid = os.getpid()
|
||||
self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs" # Use /tmp for macOS
|
||||
self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs"
|
||||
self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}"
|
||||
self.temp_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{pid}" # For source BaseSystem.dmg's EFI (if needed)
|
||||
self.temp_usb_esp_mount = f"/tmp/usb_esp_temp_skyscope_{pid}"
|
||||
self.temp_macos_source_mount = f"/tmp/macos_source_temp_skyscope_{pid}" # Not used in this flow
|
||||
self.temp_usb_macos_target_mount = f"/tmp/usb_macos_target_temp_skyscope_{pid}"
|
||||
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" # For 7z extractions
|
||||
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}"
|
||||
|
||||
self.mounted_usb_esp_path = None # Will be like /Volumes/EFI
|
||||
self.mounted_usb_macos_path = None # Will be like /Volumes/Install macOS ...
|
||||
self.mounted_source_basesystem_path = f"/tmp/source_basesystem_mount_{pid}"
|
||||
|
||||
self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
|
||||
self.temp_dirs_to_clean = [
|
||||
self.temp_efi_build_dir, self.temp_opencore_mount,
|
||||
self.temp_usb_esp_mount, self.temp_macos_source_mount,
|
||||
self.temp_usb_macos_target_mount, self.temp_dmg_extract_dir
|
||||
self.temp_efi_build_dir, self.temp_dmg_extract_dir,
|
||||
self.mounted_source_basesystem_path
|
||||
# Actual USB mount points (/Volumes/EFI, /Volumes/Install macOS...) are unmounted, not rmdir'd from here
|
||||
]
|
||||
self.attached_dmg_devices = [] # Store devices from hdiutil attach
|
||||
self.attached_dmg_devices = [] # Store device paths from hdiutil attach
|
||||
|
||||
def _report_progress(self, message: str): # ... (same)
|
||||
def _report_progress(self, message: str, is_rsync_line: bool = False):
|
||||
# Simplified progress for macOS writer for now, can add rsync parsing later if needed
|
||||
if self.progress_callback: self.progress_callback(message)
|
||||
else: print(message)
|
||||
|
||||
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False): # ... (same)
|
||||
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False):
|
||||
self._report_progress(f"Executing: {' '.join(command)}")
|
||||
try:
|
||||
process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell)
|
||||
|
|
@ -58,49 +66,84 @@ class USBWriterMacOS:
|
|||
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
|
||||
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0]}' not found."); raise
|
||||
|
||||
def _cleanup_temp_files_and_dirs(self): # Updated for macOS
|
||||
self._report_progress("Cleaning up temporary files and directories...")
|
||||
for f_path in self.temp_files_to_clean:
|
||||
if os.path.exists(f_path):
|
||||
try: os.remove(f_path) # No sudo needed for /tmp files usually
|
||||
except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
|
||||
def _cleanup_temp_files_and_dirs(self):
|
||||
self._report_progress("Cleaning up temporary files, directories, and mounts on macOS...")
|
||||
|
||||
# Detach DMGs first
|
||||
for dev_path in list(self.attached_dmg_devices): # Iterate copy
|
||||
# Unmount our specific /tmp mount points first
|
||||
if self.mounted_source_basesystem_path and os.path.ismount(self.mounted_source_basesystem_path):
|
||||
self._unmount_path(self.mounted_source_basesystem_path, force=True)
|
||||
# System mount points like /Volumes/EFI or /Volumes/Install macOS... are unmounted by diskutil unmountDisk or unmount
|
||||
# We also add them to temp_dirs_to_clean if we used their dynamic path for rmdir later (but only if they were /tmp based)
|
||||
|
||||
for dev_path in list(self.attached_dmg_devices):
|
||||
self._detach_dmg(dev_path)
|
||||
self.attached_dmg_devices = []
|
||||
|
||||
for f_path in self.temp_files_to_clean:
|
||||
if os.path.exists(f_path):
|
||||
try: os.remove(f_path)
|
||||
except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
|
||||
|
||||
for d_path in self.temp_dirs_to_clean:
|
||||
if os.path.ismount(d_path):
|
||||
try: self._run_command(["diskutil", "unmount", "force", d_path], check=False, timeout=30)
|
||||
except Exception: pass # Ignore if already unmounted or error
|
||||
if os.path.exists(d_path):
|
||||
if os.path.exists(d_path) and d_path.startswith("/tmp/"): # Only remove /tmp dirs we created
|
||||
try: shutil.rmtree(d_path, ignore_errors=True)
|
||||
except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
|
||||
|
||||
def _detach_dmg(self, device_path_or_mount_point):
|
||||
if not device_path_or_mount_point: return
|
||||
self._report_progress(f"Attempting to detach DMG associated with {device_path_or_mount_point}...")
|
||||
try:
|
||||
# hdiutil detach can take a device path or sometimes a mount path if it's unique enough
|
||||
# Using -force to ensure it detaches even if volumes are "busy" (after unmount attempts)
|
||||
self._run_command(["hdiutil", "detach", device_path_or_mount_point, "-force"], check=False, timeout=30)
|
||||
if device_path_or_mount_point in self.attached_dmg_devices: # Check if it was in our list
|
||||
self.attached_dmg_devices.remove(device_path_or_mount_point)
|
||||
# Also try to remove if it's a /dev/diskX path that got added
|
||||
if device_path_or_mount_point.startswith("/dev/") and device_path_or_mount_point in self.attached_dmg_devices:
|
||||
self.attached_dmg_devices.remove(device_path_or_mount_point)
|
||||
def _unmount_path(self, mount_path_or_device, is_device=False, force=False):
|
||||
target = mount_path_or_device
|
||||
cmd_base = ["diskutil"]
|
||||
action = "unmountDisk" if is_device else "unmount"
|
||||
cmd = cmd_base + ([action, "force", target] if force else [action, target])
|
||||
|
||||
# Check if it's a valid target for unmount/unmountDisk
|
||||
# For mount paths, check os.path.ismount. For devices, check if base device exists.
|
||||
can_unmount = False
|
||||
if is_device:
|
||||
# Extract base disk identifier like /dev/diskX from /dev/diskXsY
|
||||
base_device = re.match(r"(/dev/disk\d+)", target)
|
||||
if base_device and os.path.exists(base_device.group(1)):
|
||||
can_unmount = True
|
||||
elif os.path.ismount(target):
|
||||
can_unmount = True
|
||||
|
||||
if can_unmount:
|
||||
self._report_progress(f"Attempting to {action} {'forcefully ' if force else ''}{target}...")
|
||||
self._run_command(cmd, check=False, timeout=60) # Increased timeout for diskutil
|
||||
else:
|
||||
self._report_progress(f"Skipping unmount for {target}, not a valid mount point or device for this action.")
|
||||
|
||||
|
||||
def _detach_dmg(self, device_path):
|
||||
if not device_path or not device_path.startswith("/dev/disk"): return
|
||||
self._report_progress(f"Attempting to detach DMG device {device_path}...")
|
||||
try:
|
||||
# Ensure it's actually a virtual disk from hdiutil
|
||||
is_virtual_disk = False
|
||||
try:
|
||||
info_result = self._run_command(["diskutil", "info", "-plist", device_path], capture_output=True)
|
||||
if info_result.returncode == 0 and info_result.stdout:
|
||||
disk_info = plistlib.loads(info_result.stdout.encode('utf-8'))
|
||||
if disk_info.get("VirtualOrPhysical") == "Virtual":
|
||||
is_virtual_disk = True
|
||||
except Exception: pass # Ignore parsing errors, proceed to detach attempt
|
||||
|
||||
if is_virtual_disk:
|
||||
self._run_command(["hdiutil", "detach", device_path, "-force"], check=False, timeout=30)
|
||||
else:
|
||||
self._report_progress(f"{device_path} is not a virtual disk, or info check failed. Skipping direct hdiutil detach.")
|
||||
|
||||
if device_path in self.attached_dmg_devices:
|
||||
self.attached_dmg_devices.remove(device_path)
|
||||
except Exception as e:
|
||||
self._report_progress(f"Could not detach {device_path_or_mount_point}: {e}")
|
||||
self._report_progress(f"Could not detach {device_path}: {e}")
|
||||
|
||||
|
||||
def check_dependencies(self):
|
||||
self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd)...")
|
||||
dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"]
|
||||
self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd, bless)...")
|
||||
dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd", "bless"]
|
||||
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
|
||||
if missing_deps:
|
||||
msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`)."
|
||||
msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`). Others are standard."
|
||||
self._report_progress(msg); raise RuntimeError(msg)
|
||||
self._report_progress("All critical dependencies for macOS USB installer creation found.")
|
||||
return True
|
||||
|
|
@ -111,149 +154,149 @@ class USBWriterMacOS:
|
|||
if os.path.isdir(base_path):
|
||||
for item in os.listdir(base_path):
|
||||
item_path = os.path.join(base_path, item)
|
||||
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or MACOS_VERSIONS.get(self.target_macos_version, "").lower() in item.lower()): # MACOS_VERSIONS needs to be accessible or passed if not global
|
||||
version_tag = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower()
|
||||
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag in item.lower()):
|
||||
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path
|
||||
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using base download path."); return self.macos_download_path
|
||||
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path
|
||||
|
||||
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None) -> str | None:
|
||||
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None:
|
||||
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
|
||||
search_base = product_folder_path or self.macos_download_path
|
||||
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
|
||||
for pattern in asset_patterns:
|
||||
# Using iglob for efficiency if many files, but glob is fine for fewer expected matches
|
||||
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True)
|
||||
if found_files:
|
||||
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
|
||||
self._report_progress(f"Found {pattern}: {found_files[0]}")
|
||||
return found_files[0]
|
||||
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.")
|
||||
common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"]
|
||||
for sub_dir_pattern in common_subdirs_for_pattern:
|
||||
current_search_base = os.path.join(search_base, sub_dir_pattern)
|
||||
glob_pattern = os.path.join(glob.escape(current_search_base), pattern)
|
||||
found_files = glob.glob(glob_pattern, recursive=False)
|
||||
if found_files:
|
||||
found_files.sort(key=os.path.getsize, reverse=True)
|
||||
self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})")
|
||||
return found_files[0]
|
||||
if search_deep:
|
||||
deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern)
|
||||
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len)
|
||||
if found_files_deep:
|
||||
self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}")
|
||||
return found_files_deep[0]
|
||||
self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.")
|
||||
return None
|
||||
|
||||
def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
|
||||
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path
|
||||
try:
|
||||
if dmg_or_pkg_path.endswith(".pkg"):
|
||||
self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True)
|
||||
dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg"));
|
||||
if not dmgs_in_pkg: raise RuntimeError("No DMG found in PKG.")
|
||||
current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None) or dmgs_in_pkg[0]
|
||||
if not current_target: raise RuntimeError("Could not determine primary DMG in PKG.")
|
||||
self._report_progress(f"Using DMG from PKG: {current_target}")
|
||||
if not current_target or not current_target.endswith(".dmg"): raise RuntimeError(f"Not a valid DMG: {current_target}")
|
||||
|
||||
self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True); dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert dmgs_in_pkg, "No DMG in PKG."; current_target = max(dmgs_in_pkg, key=os.path.getsize, default=dmgs_in_pkg[0]); assert current_target, "No primary DMG in PKG."; self._report_progress(f"Using DMG from PKG: {current_target}")
|
||||
assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}"
|
||||
basesystem_dmg_to_process = current_target
|
||||
# If current_target is InstallESD.dmg or SharedSupport.dmg, it contains BaseSystem.dmg
|
||||
if "basesystem.dmg" not in os.path.basename(current_target).lower():
|
||||
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}...")
|
||||
self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True)
|
||||
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=True)
|
||||
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
|
||||
basesystem_dmg_to_process = found_bs_dmg[0]
|
||||
|
||||
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}...")
|
||||
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
|
||||
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
|
||||
if not hfs_files: raise RuntimeError(f"No .hfs file found from {basesystem_dmg_to_process}")
|
||||
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True); found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True); assert found_bs_dmg, f"No BaseSystem.dmg from {current_target}"; basesystem_dmg_to_process = found_bs_dmg[0]
|
||||
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
|
||||
if not hfs_files: self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 2*1024*1024*1024]
|
||||
assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}"
|
||||
final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True
|
||||
except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
|
||||
finally:
|
||||
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def _create_minimal_efi_template(self, efi_dir_path): # Same as linux version
|
||||
self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}")
|
||||
oc_dir = os.path.join(efi_dir_path, "EFI", "OC"); os.makedirs(os.path.join(efi_dir_path, "EFI", "BOOT"), exist_ok=True); os.makedirs(oc_dir, exist_ok=True)
|
||||
for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True)
|
||||
with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("")
|
||||
with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("")
|
||||
basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}}
|
||||
try:
|
||||
with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML)
|
||||
self._report_progress("Created basic placeholder config.plist.")
|
||||
except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}")
|
||||
|
||||
def _create_minimal_efi_template(self, efi_dir_path):
|
||||
self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}"); oc_dir=os.path.join(efi_dir_path,"EFI","OC");os.makedirs(os.path.join(efi_dir_path,"EFI","BOOT"),exist_ok=True);os.makedirs(oc_dir,exist_ok=True);[os.makedirs(os.path.join(oc_dir,s),exist_ok=True) for s in ["Drivers","Kexts","ACPI","Tools","Resources"]];open(os.path.join(efi_dir_path,"EFI","BOOT","BOOTx64.efi"),"w").close();open(os.path.join(oc_dir,"OpenCore.efi"),"w").close();bc={"#Comment":"Basic config","Misc":{"Security":{"ScanPolicy":0,"SecureBootModel":"Disabled"}},"PlatformInfo":{"Generic":{"MLB":"CHANGE_ME_MLB","SystemSerialNumber":"CHANGE_ME_SERIAL","SystemUUID":"CHANGE_ME_UUID","ROM":b"\0"*6}}};plistlib.dump(bc,open(os.path.join(oc_dir,"config.plist"),'wb'),fmt=plistlib.PlistFormat.XML)
|
||||
|
||||
def format_and_write(self) -> bool:
|
||||
try:
|
||||
self.check_dependencies()
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
for mp_dir in self.temp_dirs_to_clean: # Use full list from constructor
|
||||
os.makedirs(mp_dir, exist_ok=True)
|
||||
for mp_dir in self.temp_dirs_to_clean:
|
||||
if not os.path.exists(mp_dir): os.makedirs(mp_dir, exist_ok=True)
|
||||
|
||||
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
|
||||
self._run_command(["diskutil", "unmountDisk", "force", self.device], check=False, timeout=60); time.sleep(2)
|
||||
|
||||
installer_vol_name = f"Install macOS {self.target_macos_version}"
|
||||
self._report_progress(f"Partitioning {self.device} as GPT: EFI (FAT32, 551MB), '{installer_vol_name}' (HFS+)...")
|
||||
self._report_progress(f"Partitioning {self.device} for '{installer_vol_name}'...")
|
||||
self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3)
|
||||
|
||||
# Get actual partition identifiers
|
||||
disk_info_plist = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
|
||||
if not disk_info_plist: raise RuntimeError("Failed to get disk info after partitioning.")
|
||||
disk_info = plistlib.loads(disk_info_plist.encode('utf-8'))
|
||||
disk_info_plist_str = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
|
||||
if not disk_info_plist_str: raise RuntimeError("Failed to get disk info after partitioning.")
|
||||
disk_info = plistlib.loads(disk_info_plist_str.encode('utf-8'))
|
||||
|
||||
esp_partition_dev = None; macos_partition_dev = None
|
||||
for disk_entry in disk_info.get("AllDisksAndPartitions", []):
|
||||
if disk_entry.get("DeviceIdentifier") == self.device.replace("/dev/", ""):
|
||||
for part in disk_entry.get("Partitions", []):
|
||||
if part.get("VolumeName") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
|
||||
elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
|
||||
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}).")
|
||||
main_disk_entry = next((d for d in disk_info.get("AllDisksAndPartitions", []) if d.get("DeviceIdentifier") == self.device.replace("/dev/", "")), None)
|
||||
if main_disk_entry:
|
||||
for part in main_disk_entry.get("Partitions", []):
|
||||
if part.get("Content") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
|
||||
elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}"
|
||||
|
||||
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}). Check diskutil list output.")
|
||||
self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}")
|
||||
|
||||
# --- Prepare macOS Installer Content ---
|
||||
product_folder = self._get_gibmacos_product_folder()
|
||||
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
|
||||
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
|
||||
product_folder_path = self._get_gibmacos_product_folder()
|
||||
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)")
|
||||
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.")
|
||||
|
||||
if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
|
||||
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
|
||||
|
||||
raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk") # Use raw device for dd
|
||||
raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk")
|
||||
self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...")
|
||||
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800)
|
||||
|
||||
self._report_progress(f"Mounting macOS Install partition ({macos_partition_dev}) on USB...")
|
||||
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev])
|
||||
self.mounted_usb_macos_path = f"/Volumes/{installer_vol_name}"
|
||||
if not os.path.ismount(self.mounted_usb_macos_path):
|
||||
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev])
|
||||
self.mounted_usb_macos_path = self.temp_usb_macos_target_mount
|
||||
|
||||
core_services_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices")
|
||||
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
|
||||
self._report_progress(f"macOS partition mounted at {self.mounted_usb_macos_path}")
|
||||
|
||||
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder)
|
||||
if original_bs_dmg:
|
||||
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg")
|
||||
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")])
|
||||
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist")
|
||||
if os.path.exists(original_bs_chunklist):
|
||||
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
|
||||
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
|
||||
usb_target_root = self.mounted_usb_macos_path
|
||||
app_bundle_name = f"Install macOS {self.target_macos_version}.app"
|
||||
app_bundle_path_usb = os.path.join(usb_target_root, app_bundle_name)
|
||||
contents_path_usb = os.path.join(app_bundle_path_usb, "Contents")
|
||||
shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport")
|
||||
resources_path_usb_app = os.path.join(contents_path_usb, "Resources")
|
||||
sys_install_pkgs_usb = os.path.join(usb_target_root, "System", "Installation", "Packages")
|
||||
coreservices_path_usb = os.path.join(usb_target_root, "System", "Library", "CoreServices")
|
||||
|
||||
install_info_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder)
|
||||
if install_info_src:
|
||||
self._report_progress(f"Copying InstallInfo.plist to {self.temp_usb_macos_target_mount}/InstallInfo.plist")
|
||||
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.temp_usb_macos_target_mount, "InstallInfo.plist")])
|
||||
for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]:
|
||||
self._run_command(["sudo", "mkdir", "-p", p])
|
||||
|
||||
packages_dir_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages")
|
||||
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb])
|
||||
for f_name in ["BaseSystem.dmg", "BaseSystem.chunklist"]:
|
||||
src_file = self._find_gibmacos_asset(f_name, product_folder_path, search_deep=True)
|
||||
if src_file: self._run_command(["sudo", "cp", src_file, os.path.join(shared_support_path_usb_app, os.path.basename(src_file))]); self._run_command(["sudo", "cp", src_file, os.path.join(coreservices_path_usb, os.path.basename(src_file))])
|
||||
else: self._report_progress(f"Warning: {f_name} not found.")
|
||||
|
||||
# Copy main installer package(s) or app contents. This is simplified.
|
||||
# A real createinstallmedia copies the .app then uses it. We are building manually.
|
||||
# We need to find the main payload: InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg content.
|
||||
main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "Main Installer Payload (PKG/DMG)")
|
||||
if main_payload_src:
|
||||
self._report_progress(f"Copying main payload {os.path.basename(main_payload_src)} to {packages_dir_usb}/")
|
||||
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb, os.path.basename(main_payload_src))])
|
||||
# If it's SharedSupport.dmg, its contents might be what's needed in Packages or elsewhere.
|
||||
# If InstallAssistant.pkg, it might need to be placed at root or specific app structure.
|
||||
else: self._report_progress("Warning: Main installer payload not found. Installer may be incomplete.")
|
||||
installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True)
|
||||
if installinfo_src: self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")]); self._run_command(["sudo", "cp", installinfo_src, os.path.join(usb_target_root, "InstallInfo.plist")])
|
||||
else: self._report_progress("Warning: InstallInfo.plist not found.")
|
||||
|
||||
main_pkg_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True)
|
||||
if main_pkg_src: pkg_basename = os.path.basename(main_pkg_src); self._run_command(["sudo", "cp", main_pkg_src, os.path.join(shared_support_path_usb_app, pkg_basename)]); self._run_command(["sudo", "cp", main_pkg_src, os.path.join(sys_install_pkgs_usb, pkg_basename)])
|
||||
else: self._report_progress("Warning: Main installer PKG/DMG not found.")
|
||||
|
||||
diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True)
|
||||
if diag_src: self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")])
|
||||
|
||||
template_boot_efi = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi")
|
||||
if os.path.exists(template_boot_efi) and os.path.getsize(template_boot_efi) > 0: self._run_command(["sudo", "cp", template_boot_efi, os.path.join(coreservices_path_usb, "boot.efi")])
|
||||
else: self._report_progress(f"Warning: Template BOOTx64.efi for installer's boot.efi not found or empty.")
|
||||
|
||||
ia_product_info_path = os.path.join(usb_target_root, ".IAProductInfo")
|
||||
ia_content_xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plist version=\"1.0\"><dict><key>Product ID</key><string>com.apple.pkg.InstallAssistant</string><key>Product Path</key><string>" + app_bundle_name + "/Contents/SharedSupport/InstallAssistant.pkg</string></dict></plist>"
|
||||
temp_ia_path = f"/tmp/temp_iaproductinfo_{pid}.plist"
|
||||
with open(temp_ia_path, "w") as f: f.write(ia_content_xml)
|
||||
self._run_command(["sudo", "cp", temp_ia_path, ia_product_info_path])
|
||||
if os.path.exists(temp_ia_path): os.remove(temp_ia_path)
|
||||
|
||||
self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")])
|
||||
self._report_progress("macOS installer assets copied.")
|
||||
|
||||
# --- OpenCore EFI Setup ---
|
||||
self._report_progress("Setting up OpenCore EFI on ESP...")
|
||||
self.mounted_usb_esp_path = f"/Volumes/EFI" # Default mount path for ESP
|
||||
if not os.path.ismount(self.mounted_usb_esp_path):
|
||||
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
|
||||
self.mounted_usb_esp_path = self.temp_usb_esp_mount
|
||||
|
||||
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir)
|
||||
else: self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}"); self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
|
||||
else: self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
|
||||
|
||||
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
|
||||
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
|
||||
|
|
@ -261,52 +304,59 @@ class USBWriterMacOS:
|
|||
|
||||
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
|
||||
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only)...")
|
||||
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
|
||||
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement complete.")
|
||||
else: self._report_progress("config.plist enhancement call failed or had issues.")
|
||||
|
||||
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
|
||||
self._report_progress(f"Copying final EFI folder to USB ESP ({self.temp_usb_esp_mount})...")
|
||||
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"])
|
||||
self._report_progress(f"Copying final EFI folder to USB ESP ({self.mounted_usb_esp_path})...")
|
||||
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mounted_usb_esp_path}/EFI/"])
|
||||
|
||||
self._report_progress(f"Blessing the installer volume: {self.mounted_usb_macos_path}")
|
||||
bless_target_folder = os.path.join(self.mounted_usb_macos_path, "System", "Library", "CoreServices")
|
||||
self._run_command(["sudo", "bless", "--folder", bless_target_folder, "--label", installer_vol_name, "--setBoot"], check=False)
|
||||
|
||||
self._report_progress("USB Installer creation process completed successfully.")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._report_progress(f"An error occurred during USB writing on macOS: {e}\n{traceback.format_exc()}")
|
||||
self._report_progress(f"An error occurred during USB writing on macOS: {e}"); self._report_progress(traceback.format_exc())
|
||||
return False
|
||||
finally:
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import traceback
|
||||
from constants import MACOS_VERSIONS
|
||||
if platform.system() != "Darwin": print("This script is intended for macOS for standalone testing."); exit(1)
|
||||
print("USB Writer macOS Standalone Test - Installer Method")
|
||||
mock_download_dir = f"/tmp/temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
|
||||
# Simulate a more realistic gibMacOS product folder structure for testing _get_gibmacos_product_folder
|
||||
mock_product_name = f"012-34567 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.1.2"
|
||||
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
|
||||
mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower()
|
||||
mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x"
|
||||
mock_product_folder_path = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
|
||||
os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True) # Create SharedSupport directory
|
||||
os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True)
|
||||
with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024))
|
||||
with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist")
|
||||
with open(os.path.join(mock_product_folder_path, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f)
|
||||
with open(os.path.join(mock_product_folder_path, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024))
|
||||
with open(os.path.join(mock_product_folder_path, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024))
|
||||
|
||||
# Create dummy BaseSystem.dmg inside the product folder's SharedSupport
|
||||
dummy_bs_dmg_path = os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg")
|
||||
if not os.path.exists(dummy_bs_dmg_path):
|
||||
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy DMG
|
||||
|
||||
dummy_installinfo_path = os.path.join(mock_product_folder_path, "InstallInfo.plist")
|
||||
if not os.path.exists(dummy_installinfo_path):
|
||||
with open(dummy_installinfo_path, "wb") as f: plistlib.dump({"DisplayName":f"macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'}"},f)
|
||||
|
||||
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
|
||||
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
|
||||
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True)
|
||||
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True)
|
||||
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True)
|
||||
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
|
||||
if not os.path.exists(dummy_config_template_path):
|
||||
with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f)
|
||||
with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f, fmt=plistlib.PlistFormat.XML)
|
||||
dummy_bootx64_efi_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi")
|
||||
if not os.path.exists(dummy_bootx64_efi_path):
|
||||
with open(dummy_bootx64_efi_path, "w") as f: f.write("dummy bootx64.efi content")
|
||||
|
||||
|
||||
print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False)
|
||||
test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ")
|
||||
if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) # No need to clean OC_TEMPLATE_DIR here
|
||||
if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1)
|
||||
if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes':
|
||||
writer = USBWriterMacOS(test_device, mock_download_dir, print, True, sys.argv[1] if len(sys.argv) > 1 else "Sonoma")
|
||||
writer = USBWriterMacOS(test_device, mock_download_dir, print, True, target_version_cli)
|
||||
writer.format_and_write()
|
||||
else: print("Test cancelled.")
|
||||
shutil.rmtree(mock_download_dir, ignore_errors=True)
|
||||
# Deliberately not cleaning OC_TEMPLATE_DIR in test, as it might be shared or pre-existing.
|
||||
print("Mock download dir cleaned up.")
|
||||
|
|
|
|||
|
|
@ -1,71 +1,92 @@
|
|||
# usb_writer_windows.py (Refactoring for Installer Workflow)
|
||||
# usb_writer_windows.py (Refining EFI setup and manual step guidance)
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import re
|
||||
import glob # For _find_gibmacos_asset
|
||||
import glob
|
||||
import plistlib
|
||||
import traceback
|
||||
import sys # For checking psutil import
|
||||
import sys # Added for psutil check
|
||||
|
||||
# Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test
|
||||
try:
|
||||
from PyQt6.QtWidgets import QMessageBox # For user guidance
|
||||
from PyQt6.QtWidgets import QMessageBox
|
||||
except ImportError:
|
||||
class QMessageBox: # Mock for standalone testing
|
||||
# Mock QMessageBox for standalone testing or if PyQt6 is not available
|
||||
class QMessageBox:
|
||||
Information = 1 # Dummy enum value
|
||||
Warning = 2 # Dummy enum value
|
||||
Question = 3 # Dummy enum value
|
||||
YesRole = 0 # Dummy role
|
||||
NoRole = 1 # Dummy role
|
||||
|
||||
@staticmethod
|
||||
def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'")
|
||||
def information(parent, title, message, buttons=None, defaultButton=None):
|
||||
print(f"INFO (QMessageBox mock): Title='{title}', Message='{message}'")
|
||||
return QMessageBox.Yes # Simulate a positive action if needed
|
||||
@staticmethod
|
||||
def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox
|
||||
Yes = 1 # Mock value
|
||||
No = 0 # Mock value
|
||||
Cancel = 0 # Mock value
|
||||
def warning(parent, title, message, buttons=None, defaultButton=None):
|
||||
print(f"WARNING (QMessageBox mock): Title='{title}', Message='{message}'")
|
||||
return QMessageBox.Yes # Simulate a positive action
|
||||
@staticmethod
|
||||
def critical(parent, title, message, buttons=None, defaultButton=None):
|
||||
print(f"CRITICAL (QMessageBox mock): Title='{title}', Message='{message}'")
|
||||
return QMessageBox.Yes # Simulate a positive action
|
||||
# Add other static methods if your code uses them, e.g. question
|
||||
@staticmethod
|
||||
def question(parent, title, message, buttons=None, defaultButton=None):
|
||||
print(f"QUESTION (QMessageBox mock): Title='{title}', Message='{message}'")
|
||||
return QMessageBox.Yes # Simulate 'Yes' for testing
|
||||
|
||||
# Dummy button values if your code checks for specific button results
|
||||
Yes = 0x00004000
|
||||
No = 0x00010000
|
||||
Cancel = 0x00400000
|
||||
|
||||
|
||||
try:
|
||||
from plist_modifier import enhance_config_plist
|
||||
except ImportError:
|
||||
enhance_config_plist = None
|
||||
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled.")
|
||||
print("Warning: plist_modifier not found. Enhancement will be skipped.")
|
||||
def enhance_config_plist(plist_path, macos_version, progress_callback):
|
||||
if progress_callback:
|
||||
progress_callback("Skipping plist enhancement: plist_modifier not available.")
|
||||
return False # Indicate failure or no action
|
||||
|
||||
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
|
||||
# This path needs to be correct relative to where usb_writer_windows.py is, or use an absolute path strategy
|
||||
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "EFI_template_installer")
|
||||
|
||||
class USBWriterWindows:
|
||||
def __init__(self, device_id_str: str, macos_download_path: str,
|
||||
progress_callback=None, enhance_plist_enabled: bool = False,
|
||||
target_macos_version: str = ""):
|
||||
# device_id_str is expected to be the disk number string from user, e.g., "1", "2"
|
||||
self.device_id_str = device_id_str
|
||||
self.disk_number = "".join(filter(str.isdigit, device_id_str))
|
||||
if not self.disk_number:
|
||||
raise ValueError(f"Invalid device_id format: '{device_id_str}'. Must contain a disk number.")
|
||||
|
||||
self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}"
|
||||
|
||||
self.macos_download_path = macos_download_path
|
||||
self.progress_callback = progress_callback
|
||||
self.enhance_plist_enabled = enhance_plist_enabled
|
||||
self.target_macos_version = target_macos_version
|
||||
|
||||
pid = os.getpid()
|
||||
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
|
||||
self.temp_efi_build_dir = f"temp_efi_build_{pid}"
|
||||
self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For 7z extractions
|
||||
# Use system temp for Windows more reliably
|
||||
self.temp_dir_base = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_usb_temp_{pid}")
|
||||
self.temp_basesystem_hfs_path = os.path.join(self.temp_dir_base, f"temp_basesystem_{pid}.hfs")
|
||||
self.temp_efi_build_dir = os.path.join(self.temp_dir_base, f"temp_efi_build_{pid}")
|
||||
self.temp_dmg_extract_dir = os.path.join(self.temp_dir_base, f"temp_dmg_extract_{pid}")
|
||||
|
||||
|
||||
self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
|
||||
self.temp_dirs_to_clean = [self.temp_efi_build_dir, self.temp_dmg_extract_dir]
|
||||
self.temp_files_to_clean = [self.temp_basesystem_hfs_path] # Specific files outside temp_dir_base (if any)
|
||||
self.temp_dirs_to_clean = [self.temp_dir_base] # Base temp dir for this instance
|
||||
self.assigned_efi_letter = None
|
||||
|
||||
def _report_progress(self, message: str):
|
||||
if self.progress_callback: self.progress_callback(message)
|
||||
else: print(message)
|
||||
|
||||
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None):
|
||||
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None, creationflags=0):
|
||||
self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}")
|
||||
try:
|
||||
process = subprocess.run(
|
||||
command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=creationflags)
|
||||
if capture_output:
|
||||
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
|
||||
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
|
||||
|
|
@ -74,262 +95,529 @@ class USBWriterWindows:
|
|||
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
|
||||
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise
|
||||
|
||||
|
||||
def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None:
|
||||
script_file_path = f"diskpart_script_{os.getpid()}.txt"; output_text = ""
|
||||
with open(script_file_path, "w") as f: f.write(script_content)
|
||||
script_file_path = os.path.join(self.temp_dir_base, f"diskpart_script_{os.getpid()}.txt")
|
||||
os.makedirs(self.temp_dir_base, exist_ok=True)
|
||||
output_text = None
|
||||
try:
|
||||
self._report_progress(f"Running diskpart script:\n{script_content}")
|
||||
process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False)
|
||||
with open(script_file_path, "w") as f: f.write(script_content)
|
||||
# Use CREATE_NO_WINDOW for subprocess.run with diskpart
|
||||
process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
output_text = (process.stdout or "") + "\n" + (process.stderr or "")
|
||||
|
||||
success_indicators = ["DiskPart successfully", "successfully completed", "succeeded in creating", "successfully formatted", "successfully assigned"]
|
||||
has_success_indicator = any(indicator in output_text for indicator in success_indicators)
|
||||
has_error_indicator = "Virtual Disk Service error" in output_text or "DiskPart has encountered an error" in output_text
|
||||
|
||||
if has_error_indicator:
|
||||
self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}")
|
||||
elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text :
|
||||
self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}")
|
||||
|
||||
if capture_output_for_parse: return output_text
|
||||
finally:
|
||||
if os.path.exists(script_file_path): os.remove(script_file_path)
|
||||
return output_text if capture_output_for_parse else None
|
||||
|
||||
if os.path.exists(script_file_path):
|
||||
try: os.remove(script_file_path)
|
||||
except OSError as e: self._report_progress(f"Warning: Could not remove temp diskpart script {script_file_path}: {e}")
|
||||
return None # Explicitly return None if not capturing for parse or if it fails before return
|
||||
|
||||
def _cleanup_temp_files_and_dirs(self):
|
||||
self._report_progress("Cleaning up...")
|
||||
self._report_progress("Cleaning up temporary files and directories on Windows...")
|
||||
for f_path in self.temp_files_to_clean:
|
||||
if os.path.exists(f_path):
|
||||
try: os.remove(f_path)
|
||||
except Exception as e: self._report_progress(f"Could not remove temp file {f_path}: {e}")
|
||||
for d_path in self.temp_dirs_to_clean:
|
||||
except OSError as e: self._report_progress(f"Error removing file {f_path}: {e}")
|
||||
|
||||
for d_path in self.temp_dirs_to_clean: # self.temp_dir_base is the main one
|
||||
if os.path.exists(d_path):
|
||||
try: shutil.rmtree(d_path, ignore_errors=True)
|
||||
except Exception as e: self._report_progress(f"Could not remove temp dir {d_path}: {e}")
|
||||
try: shutil.rmtree(d_path, ignore_errors=False) # Try with ignore_errors=False first
|
||||
except OSError as e:
|
||||
self._report_progress(f"Error removing dir {d_path}: {e}. Attempting force remove.")
|
||||
try: shutil.rmtree(d_path, ignore_errors=True) # Fallback to ignore_errors=True
|
||||
except OSError as e_force: self._report_progress(f"Force remove for dir {d_path} also failed: {e_force}")
|
||||
|
||||
|
||||
def _find_available_drive_letter(self) -> str | None:
|
||||
import string; used_letters = set()
|
||||
import string
|
||||
used_letters = set()
|
||||
try:
|
||||
if 'psutil' in sys.modules: # Check if psutil was imported by main app
|
||||
partitions = sys.modules['psutil'].disk_partitions(all=True)
|
||||
# Try to use psutil if available (e.g., when run from main_app.py)
|
||||
if 'psutil' in sys.modules:
|
||||
import psutil # Ensure it's imported here if check passes
|
||||
partitions = psutil.disk_partitions(all=True)
|
||||
for p in partitions:
|
||||
if p.mountpoint and len(p.mountpoint) >= 2 and p.mountpoint[1] == ':': # Check for "X:"
|
||||
if p.mountpoint and len(p.mountpoint) == 2 and p.mountpoint[1] == ':':
|
||||
used_letters.add(p.mountpoint[0].upper())
|
||||
except Exception as e:
|
||||
self._report_progress(f"Could not list used drive letters with psutil: {e}. Will try common letters.")
|
||||
else: # Fallback if psutil is not available (e.g. pure standalone script)
|
||||
self._report_progress("psutil not available, using limited drive letter detection.")
|
||||
# Basic check, might not be exhaustive
|
||||
for letter in string.ascii_uppercase[3:]: # D onwards
|
||||
if os.path.exists(f"{letter}:\\"):
|
||||
used_letters.add(letter)
|
||||
|
||||
except Exception as e:
|
||||
self._report_progress(f"Error detecting used drive letters: {e}. Proceeding with caution.")
|
||||
|
||||
# Prefer letters from S onwards, less likely to conflict with user drives
|
||||
for letter in "STUVWXYZGHIJKLMNOPQR":
|
||||
if letter not in used_letters and letter > 'D': # Avoid A, B, C, D
|
||||
if letter not in used_letters and letter > 'C': # Ensure it's not A, B, C
|
||||
return letter
|
||||
return None
|
||||
|
||||
def check_dependencies(self):
|
||||
self._report_progress("Checking dependencies (diskpart, robocopy, 7z, dd for Windows [manual check])...")
|
||||
dependencies = ["diskpart", "robocopy", "7z"]
|
||||
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
|
||||
if missing_deps:
|
||||
msg = f"Missing dependencies: {', '.join(missing_deps)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip CLI) needs to be installed and in PATH."
|
||||
self._report_progress(msg); raise RuntimeError(msg)
|
||||
self._report_progress("Base dependencies found. Ensure a 'dd for Windows' utility is installed and in your PATH for writing the main macOS BaseSystem image.")
|
||||
missing = [dep for dep in dependencies if not shutil.which(dep)]
|
||||
if missing:
|
||||
msg = f"Missing dependencies: {', '.join(missing)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip) needs to be installed and its directory added to the system PATH."
|
||||
self._report_progress(msg)
|
||||
raise RuntimeError(msg)
|
||||
self._report_progress("Please ensure a 'dd for Windows' utility (e.g., from SUSE, Cygwin, or http://www.chrysocome.net/dd) is installed and accessible from your PATH for writing the main macOS BaseSystem image.")
|
||||
return True
|
||||
|
||||
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None) -> str | None:
|
||||
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
|
||||
search_base = product_folder_path or self.macos_download_path
|
||||
self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...")
|
||||
for pattern in asset_patterns:
|
||||
found_files = glob.glob(os.path.join(search_base, "**", pattern), recursive=True)
|
||||
def _find_gibmacos_asset(self, asset_name: str, product_folder_path: str | None = None, search_deep=True) -> str | None:
|
||||
search_locations = []
|
||||
if product_folder_path and os.path.isdir(product_folder_path):
|
||||
search_locations.extend([product_folder_path, os.path.join(product_folder_path, "SharedSupport")])
|
||||
|
||||
# Also search directly in macos_download_path and a potential "macOS Install Data" subdirectory
|
||||
search_locations.extend([self.macos_download_path, os.path.join(self.macos_download_path, "macOS Install Data")])
|
||||
|
||||
# If a version-specific folder exists at the root of macos_download_path (less common for gibMacOS structure)
|
||||
if os.path.isdir(self.macos_download_path):
|
||||
for item in os.listdir(self.macos_download_path):
|
||||
item_path = os.path.join(self.macos_download_path, item)
|
||||
if os.path.isdir(item_path) and self.target_macos_version.lower() in item.lower():
|
||||
search_locations.append(item_path)
|
||||
search_locations.append(os.path.join(item_path, "SharedSupport"))
|
||||
# Assuming first match is good enough for this heuristic
|
||||
break
|
||||
|
||||
# Deduplicate search locations while preserving order (Python 3.7+)
|
||||
search_locations = list(dict.fromkeys(search_locations))
|
||||
|
||||
for loc in search_locations:
|
||||
if not os.path.isdir(loc): continue
|
||||
|
||||
path = os.path.join(loc, asset_name)
|
||||
if os.path.exists(path):
|
||||
self._report_progress(f"Found '{asset_name}' at: {path}")
|
||||
return path
|
||||
|
||||
# Case-insensitive glob as fallback for direct name match
|
||||
# Create a pattern like "[bB][aA][sS][eE][sS][yY][sS][tT][eE][mM].[dD][mM][gG]"
|
||||
pattern_parts = [f"[{c.lower()}{c.upper()}]" if c.isalpha() else re.escape(c) for c in asset_name]
|
||||
insensitive_glob_pattern = "".join(pattern_parts)
|
||||
|
||||
found_files = glob.glob(os.path.join(loc, insensitive_glob_pattern), recursive=False)
|
||||
if found_files:
|
||||
found_files.sort(key=lambda x: (x.count(os.sep), len(x)))
|
||||
self._report_progress(f"Found {pattern}: {found_files[0]}")
|
||||
self._report_progress(f"Found '{asset_name}' via case-insensitive glob at: {found_files[0]}")
|
||||
return found_files[0]
|
||||
self._report_progress(f"Warning: Asset pattern(s) {asset_patterns} not found in {search_base}.")
|
||||
|
||||
if search_deep:
|
||||
self._report_progress(f"Asset '{asset_name}' not found in primary locations, starting deep search in {self.macos_download_path}...")
|
||||
deep_search_pattern = os.path.join(self.macos_download_path, "**", asset_name)
|
||||
# Sort by length to prefer shallower paths, then alphabetically
|
||||
found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=lambda p: (len(os.path.dirname(p)), p))
|
||||
if found_files_deep:
|
||||
self._report_progress(f"Found '{asset_name}' via deep search at: {found_files_deep[0]}")
|
||||
return found_files_deep[0]
|
||||
|
||||
self._report_progress(f"Warning: Asset '{asset_name}' not found.")
|
||||
return None
|
||||
|
||||
def _get_gibmacos_product_folder(self) -> str | None:
|
||||
from constants import MACOS_VERSIONS # Import for this method
|
||||
# constants.py should be in the same directory or Python path
|
||||
try: from constants import MACOS_VERSIONS
|
||||
except ImportError: MACOS_VERSIONS = {} ; self._report_progress("Warning: MACOS_VERSIONS from constants.py not loaded.")
|
||||
|
||||
# Standard gibMacOS download structure: macOS Downloads/publicrelease/012-34567 - macOS Sonoma 14.0
|
||||
base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease")
|
||||
if not os.path.isdir(base_path): base_path = self.macos_download_path
|
||||
if not os.path.isdir(base_path):
|
||||
# Fallback if "macOS Downloads/publicrelease" is not present, use macos_download_path directly
|
||||
base_path = self.macos_download_path
|
||||
|
||||
if os.path.isdir(base_path):
|
||||
potential_folders = []
|
||||
for item in os.listdir(base_path):
|
||||
item_path = os.path.join(base_path, item)
|
||||
if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or MACOS_VERSIONS.get(self.target_macos_version, "").lower() in item.lower()):
|
||||
self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path
|
||||
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using base download path: {self.macos_download_path}"); return self.macos_download_path
|
||||
# Check if it's a directory and matches target_macos_version (name or tag)
|
||||
version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version.lower().replace(" ", ""))
|
||||
if os.path.isdir(item_path) and \
|
||||
(self.target_macos_version.lower() in item.lower() or \
|
||||
version_tag_from_constants.lower() in item.lower().replace(" ", "")):
|
||||
potential_folders.append(item_path)
|
||||
|
||||
if potential_folders:
|
||||
# Sort by length (prefer shorter, more direct matches) or other heuristics if needed
|
||||
best_match = min(potential_folders, key=len)
|
||||
self._report_progress(f"Identified gibMacOS product folder: {best_match}")
|
||||
return best_match
|
||||
|
||||
self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}")
|
||||
return self.macos_download_path # Fallback to the root download path
|
||||
|
||||
def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool:
|
||||
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path
|
||||
temp_extract_dir = self.temp_dmg_extract_dir
|
||||
os.makedirs(temp_extract_dir, exist_ok=True)
|
||||
current_target = dmg_or_pkg_path
|
||||
try:
|
||||
if dmg_or_pkg_path.endswith(".pkg"):
|
||||
self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True)
|
||||
dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg"));
|
||||
if not dmgs_in_pkg: raise RuntimeError("No DMG found in PKG.")
|
||||
current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None) or dmgs_in_pkg[0]
|
||||
if not current_target: raise RuntimeError("Could not determine primary DMG in PKG.")
|
||||
if not os.path.exists(current_target):
|
||||
self._report_progress(f"Error: Input file for HFS extraction does not exist: {current_target}"); return False
|
||||
|
||||
# Step 1: If it's a PKG, extract DMGs from it.
|
||||
if dmg_or_pkg_path.lower().endswith(".pkg"):
|
||||
self._report_progress(f"Extracting DMG(s) from PKG: {current_target} using 7z...")
|
||||
# Using 'e' to extract flat, '-txar' for PKG/XAR format.
|
||||
self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{temp_extract_dir}", "-y"], check=True)
|
||||
dmgs_in_pkg = glob.glob(os.path.join(temp_extract_dir, "*.dmg"))
|
||||
if not dmgs_in_pkg: self._report_progress(f"No DMG files found after extracting PKG: {current_target}"); return False
|
||||
# Select the largest DMG, assuming it's the main one.
|
||||
current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None)
|
||||
if not current_target: self._report_progress("Failed to select a DMG from PKG contents."); return False
|
||||
self._report_progress(f"Using DMG from PKG: {current_target}")
|
||||
if not current_target or not current_target.endswith(".dmg"): raise RuntimeError(f"Not a valid DMG: {current_target}")
|
||||
|
||||
# Step 2: Ensure we have a DMG file.
|
||||
if not current_target or not current_target.lower().endswith(".dmg"):
|
||||
self._report_progress(f"Not a valid DMG file for HFS extraction: {current_target}"); return False
|
||||
|
||||
basesystem_dmg_to_process = current_target
|
||||
# Step 3: If the DMG is not BaseSystem.dmg, try to extract BaseSystem.dmg from it.
|
||||
# This handles cases like SharedSupport.dmg containing BaseSystem.dmg.
|
||||
if "basesystem.dmg" not in os.path.basename(current_target).lower():
|
||||
self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True)
|
||||
found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*BaseSystem.dmg"), recursive=True)
|
||||
if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}")
|
||||
basesystem_dmg_to_process = found_bs_dmg[0]
|
||||
self._report_progress(f"Extracting BaseSystem.dmg from container DMG: {current_target} using 7z...")
|
||||
# Extract recursively, looking for any path that includes BaseSystem.dmg
|
||||
self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{temp_extract_dir}", "-y"], check=True)
|
||||
found_bs_dmg_list = glob.glob(os.path.join(temp_extract_dir, "**", "*BaseSystem.dmg"), recursive=True)
|
||||
if not found_bs_dmg_list: self._report_progress(f"No BaseSystem.dmg found within {current_target}"); return False
|
||||
basesystem_dmg_to_process = max(found_bs_dmg_list, key=os.path.getsize, default=None) # Largest if multiple
|
||||
if not basesystem_dmg_to_process: self._report_progress("Failed to select BaseSystem.dmg from container."); return False
|
||||
self._report_progress(f"Processing extracted BaseSystem.dmg: {basesystem_dmg_to_process}")
|
||||
|
||||
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
|
||||
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
|
||||
if not hfs_files:
|
||||
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True) # Try extracting all files
|
||||
hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 100*1024*1024]
|
||||
# Step 4: Extract HFS partition image from BaseSystem.dmg.
|
||||
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process} using 7z...")
|
||||
# Using 'e' to extract flat, '-tdmg' for DMG format. Looking for '*.hfs' or specific partition files.
|
||||
# Common HFS file names inside BaseSystem.dmg are like '2.hfs' or similar.
|
||||
# Sometimes they don't have .hfs extension, 7z might list them by index.
|
||||
# We will try to extract any .hfs file.
|
||||
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{temp_extract_dir}", "-y"], check=True)
|
||||
hfs_files = glob.glob(os.path.join(temp_extract_dir, "*.hfs"))
|
||||
|
||||
if not hfs_files: raise RuntimeError(f"No suitable .hfs image found after extracting {basesystem_dmg_to_process}")
|
||||
final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True
|
||||
except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
|
||||
finally:
|
||||
if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
|
||||
if not hfs_files: # If no .hfs, try extracting by common partition indices if 7z supports listing them for DMG
|
||||
self._report_progress("No direct '*.hfs' found. Attempting extraction of common HFS partition by index (e.g., '2', '3')...")
|
||||
# This is more complex as 7z CLI might not easily allow extracting by index directly without listing first.
|
||||
# For now, we rely on .hfs existing. If this fails, user might need to extract manually with 7z GUI.
|
||||
# A more robust solution would involve listing contents and then extracting the correct file.
|
||||
self._report_progress("Extraction by index is not implemented. Please ensure BaseSystem.dmg contains a directly extractable .hfs file.")
|
||||
return False
|
||||
|
||||
def _create_minimal_efi_template(self, efi_dir_path):
|
||||
self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}")
|
||||
oc_dir = os.path.join(efi_dir_path, "EFI", "OC"); os.makedirs(os.path.join(efi_dir_path, "EFI", "BOOT"), exist_ok=True); os.makedirs(oc_dir, exist_ok=True)
|
||||
for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True)
|
||||
with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("")
|
||||
with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("")
|
||||
basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}}
|
||||
if not hfs_files: self._report_progress(f"No HFS files found after extracting DMG: {basesystem_dmg_to_process}"); return False
|
||||
|
||||
final_hfs_file = max(hfs_files, key=os.path.getsize, default=None) # Largest HFS file
|
||||
if not final_hfs_file: self._report_progress("Failed to select HFS file."); return False
|
||||
|
||||
self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}")
|
||||
shutil.move(final_hfs_file, output_hfs_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False
|
||||
|
||||
def _create_minimal_efi_template_content(self, efi_dir_path_root):
|
||||
self._report_progress(f"Minimal EFI template directory '{OC_TEMPLATE_DIR}' not found or is empty. Creating basic structure at {efi_dir_path_root}")
|
||||
efi_path = os.path.join(efi_dir_path_root, "EFI")
|
||||
oc_dir = os.path.join(efi_path, "OC")
|
||||
os.makedirs(os.path.join(efi_path, "BOOT"), exist_ok=True)
|
||||
os.makedirs(oc_dir, exist_ok=True)
|
||||
for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]:
|
||||
os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True)
|
||||
|
||||
# Create dummy BOOTx64.efi and OpenCore.efi
|
||||
with open(os.path.join(efi_path, "BOOT", "BOOTx64.efi"), "w") as f: f.write("Minimal Boot")
|
||||
with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("Minimal OC")
|
||||
|
||||
# Create a very basic config.plist
|
||||
basic_config = {
|
||||
"#WARNING": "This is a minimal config.plist. Replace with a full one for booting macOS!",
|
||||
"Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}},
|
||||
"PlatformInfo": {"Generic": {"MLB": "CHANGE_ME_MLB", "SystemSerialNumber": "CHANGE_ME_SERIAL", "SystemUUID": "CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}},
|
||||
"NVRAM": {"Add": {"4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14": {"DefaultBackgroundColor": "00000000", "UIScale": "01"}}}, # Basic NVRAM
|
||||
"UEFI": {"Drivers": ["OpenRuntime.efi"], "Input": {"KeySupport": True}} # Example
|
||||
}
|
||||
config_plist_path = os.path.join(oc_dir, "config.plist")
|
||||
try:
|
||||
with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML)
|
||||
self._report_progress("Created basic placeholder config.plist.")
|
||||
except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}")
|
||||
with open(config_plist_path, 'wb') as fp:
|
||||
plistlib.dump(basic_config, fp, fmt=plistlib.PlistFormat.XML)
|
||||
self._report_progress(f"Created minimal config.plist at {config_plist_path}")
|
||||
except Exception as e:
|
||||
self._report_progress(f"Error creating minimal config.plist: {e}")
|
||||
|
||||
|
||||
def format_and_write(self) -> bool:
|
||||
try:
|
||||
self.check_dependencies()
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
os.makedirs(self.temp_efi_build_dir, exist_ok=True)
|
||||
if os.path.exists(self.temp_dir_base):
|
||||
self._report_progress(f"Cleaning up existing temp base directory: {self.temp_dir_base}")
|
||||
shutil.rmtree(self.temp_dir_base, ignore_errors=True)
|
||||
os.makedirs(self.temp_dir_base, exist_ok=True)
|
||||
os.makedirs(self.temp_efi_build_dir, exist_ok=True) # For building EFI contents before copy
|
||||
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True) # For 7z extractions
|
||||
|
||||
self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!")
|
||||
# Optional: Add a QMessageBox.question here for final confirmation in GUI mode
|
||||
|
||||
self.assigned_efi_letter = self._find_available_drive_letter()
|
||||
if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.")
|
||||
self._report_progress(f"Will assign letter {self.assigned_efi_letter}: to EFI partition.")
|
||||
self._report_progress(f"Will attempt to assign letter {self.assigned_efi_letter}: to EFI partition.")
|
||||
|
||||
installer_vol_label = f"Install macOS {self.target_macos_version}"
|
||||
# Ensure label for diskpart is max 32 chars for FAT32. "Install macOS Monterey" is 23 chars.
|
||||
diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n"
|
||||
diskpart_script_part1 += f"create partition efi size=550 label=\"EFI\"\nformat fs=fat32 quick\nassign letter={self.assigned_efi_letter}\n" # Assign after format
|
||||
diskpart_script_part1 += f"create partition primary label=\"Install macOS {self.target_macos_version}\" id=AF00\nexit\n" # Set HFS+ type ID
|
||||
# Create EFI (ESP) partition, 550MB is generous and common
|
||||
diskpart_script_part1 += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n"
|
||||
# Create main macOS partition (HFS+). Let diskpart use remaining space.
|
||||
# AF00 is Apple HFS+ type GUID. For APFS, it's 7C3457EF-0000-11AA-AA11-00306543ECAC
|
||||
# We create as HFS+ because BaseSystem is HFS+. Installer will convert if needed.
|
||||
diskpart_script_part1 += f"create partition primary label=\"{installer_vol_label[:31]}\" id=AF00\nexit\n"
|
||||
|
||||
self._run_diskpart_script(diskpart_script_part1)
|
||||
time.sleep(5)
|
||||
self._report_progress("Disk partitioning complete. Waiting for volumes to settle...")
|
||||
time.sleep(5) # Give Windows time to recognize new partitions
|
||||
|
||||
macos_partition_offset_str = "Offset not determined by diskpart"
|
||||
macos_partition_number_str = "2 (assumed)"
|
||||
macos_partition_number_str = "2 (typically)"; macos_partition_offset_str = "Offset not automatically determined for Windows dd"
|
||||
try:
|
||||
# Attempt to get partition details. This is informational.
|
||||
diskpart_script_detail = f"select disk {self.disk_number}\nlist partition\nexit\n"
|
||||
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
|
||||
if detail_output:
|
||||
# Try to find Partition 2, assuming it's our target HFS+ partition
|
||||
part_match = re.search(r"Partition 2\s+Primary\s+\d+\s+[GMK]B\s+(\d+)\s+[GMK]B", detail_output, re.IGNORECASE)
|
||||
if part_match:
|
||||
macos_partition_offset_str = f"{part_match.group(1)} MB (approx. from start of disk for Partition 2)"
|
||||
else: # Fallback if specific regex fails
|
||||
self._report_progress("Could not parse partition 2 offset, using generic message.")
|
||||
except Exception as e:
|
||||
self._report_progress(f"Could not get detailed partition info from diskpart: {e}")
|
||||
|
||||
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n"
|
||||
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
|
||||
if detail_output:
|
||||
self._report_progress(f"Detail Partition Output:\n{detail_output}")
|
||||
offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE)
|
||||
if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
|
||||
|
||||
part_num_match = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE) # Match "Partition X" then "Type" on next line
|
||||
if part_num_match:
|
||||
macos_partition_number_str = part_num_match.group(1)
|
||||
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
|
||||
|
||||
# --- OpenCore EFI Setup ---
|
||||
self._report_progress("Setting up OpenCore EFI on ESP...")
|
||||
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir)
|
||||
if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR):
|
||||
self._report_progress(f"EFI_template_installer at '{OC_TEMPLATE_DIR}' is missing or empty.")
|
||||
self._create_minimal_efi_template_content(self.temp_efi_build_dir) # Create in temp_efi_build_dir
|
||||
else:
|
||||
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
|
||||
if os.path.exists(self.temp_efi_build_dir): shutil.rmtree(self.temp_efi_build_dir)
|
||||
self._report_progress(f"Copying EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
|
||||
shutil.copytree(OC_TEMPLATE_DIR, self.temp_efi_build_dir, dirs_exist_ok=True)
|
||||
|
||||
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
|
||||
if not os.path.exists(temp_config_plist_path):
|
||||
template_plist_src = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") # Name used in prior step
|
||||
if os.path.exists(template_plist_src): shutil.copy2(template_plist_src, temp_config_plist_path)
|
||||
else: self._create_minimal_efi_template(self.temp_efi_build_dir) # Fallback to create basic if template also missing
|
||||
template_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")
|
||||
if os.path.exists(template_plist_path):
|
||||
self._report_progress(f"Using template config: {template_plist_path}")
|
||||
shutil.copy2(template_plist_path, temp_config_plist_path)
|
||||
else:
|
||||
self._report_progress("No config.plist or config-template.plist found in EFI template. Creating a minimal one.")
|
||||
plistlib.dump({"#Comment": "Minimal config by Skyscope - REPLACE ME", "PlatformInfo": {"Generic": {"MLB": "CHANGE_ME"}}},
|
||||
open(temp_config_plist_path, 'wb'), fmt=plistlib.PlistFormat.XML)
|
||||
|
||||
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
|
||||
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only for this feature)...")
|
||||
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
|
||||
else: self._report_progress("config.plist enhancement call failed or had issues.")
|
||||
if self.enhance_plist_enabled and enhance_config_plist: # Check if function exists
|
||||
self._report_progress("Attempting to enhance config.plist (note: hardware detection for enhancement is primarily Linux-based)...")
|
||||
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress):
|
||||
self._report_progress("config.plist enhancement process complete.")
|
||||
else:
|
||||
self._report_progress("config.plist enhancement process failed or had issues (this is expected on Windows for hardware-specifics).")
|
||||
|
||||
target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\"
|
||||
if not os.path.exists(target_efi_on_usb_root): # Wait and check again
|
||||
time.sleep(3)
|
||||
# Ensure the assigned drive letter is actually available before robocopy
|
||||
if not os.path.exists(target_efi_on_usb_root):
|
||||
time.sleep(3) # Extra wait
|
||||
if not os.path.exists(target_efi_on_usb_root):
|
||||
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign.")
|
||||
raise RuntimeError(f"EFI partition {target_efi_on_usb_root} not accessible after formatting and assignment.")
|
||||
|
||||
self._report_progress(f"Copying final EFI folder to USB ESP ({target_efi_on_usb_root})...")
|
||||
self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), target_efi_on_usb_root + "EFI", "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True)
|
||||
self._report_progress(f"Copying final EFI folder from {os.path.join(self.temp_efi_build_dir, 'EFI')} to USB ESP ({target_efi_on_usb_root}EFI)...")
|
||||
# Using robocopy: /E for subdirs (incl. empty), /S for non-empty, /NFL no file list, /NDL no dir list, /NJH no job header, /NJS no job summary, /NC no class, /NS no size, /NP no progress
|
||||
# /MT:8 for multithreading (default is 8, can be 1-128)
|
||||
self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), os.path.join(target_efi_on_usb_root, "EFI"), "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/MT:8", "/R:3", "/W:5"], check=True)
|
||||
self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}")
|
||||
|
||||
# --- Prepare BaseSystem ---
|
||||
self._report_progress("Locating BaseSystem image from downloaded assets...")
|
||||
# --- Prepare BaseSystem HFS Image ---
|
||||
self._report_progress("Locating BaseSystem image (DMG or PKG containing it) from downloaded assets...")
|
||||
product_folder_path = self._get_gibmacos_product_folder()
|
||||
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
|
||||
if not source_for_hfs_extraction: source_for_hfs_extraction = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, "InstallAssistant.pkg as BaseSystem source")
|
||||
if not source_for_hfs_extraction: raise RuntimeError("Could not find BaseSystem.dmg, InstallESD.dmg, SharedSupport.dmg or InstallAssistant.pkg.")
|
||||
|
||||
if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
|
||||
raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
|
||||
|
||||
abs_hfs_path = os.path.abspath(self.temp_basesystem_hfs_path)
|
||||
guidance_message = (
|
||||
f"EFI setup complete on drive {self.assigned_efi_letter}:.\n"
|
||||
f"BaseSystem HFS image extracted to: '{abs_hfs_path}'.\n\n"
|
||||
f"MANUAL STEP REQUIRED FOR MAIN macOS PARTITION:\n"
|
||||
f"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
|
||||
f"2. Use a 'dd for Windows' utility to write the extracted HFS image.\n"
|
||||
f" Target: Disk {self.disk_number} (Path: {self.physical_drive_path}), Partition {macos_partition_number_str} (Offset: {macos_partition_offset_str}).\n"
|
||||
f" Example command (VERIFY SYNTAX FOR YOUR DD TOOL!):\n"
|
||||
f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} --target-partition {macos_partition_number_str} bs=4M --progress` (Conceptual, if dd supports partition targeting by number)\n"
|
||||
f" OR, if writing to the whole disk by offset (VERY ADVANCED & RISKY if offset is wrong):\n"
|
||||
f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} seek=<OFFSET_IN_BLOCKS_OR_BYTES> bs=<YOUR_BLOCK_SIZE> ...` (Offset from diskpart is in bytes)\n\n"
|
||||
"3. After writing BaseSystem, manually copy other installer files (like InstallAssistant.pkg or contents of SharedSupport.dmg) from "
|
||||
f"'{self.macos_download_path}' to the 'Install macOS {self.target_macos_version}' partition on the USB. This requires a tool that can write to HFS+ partitions from Windows (e.g., TransMac, HFSExplorer, or do this from a Mac/Linux environment).\n\n"
|
||||
"This tool CANNOT fully automate HFS+ partition writing or HFS+ file copying on Windows."
|
||||
basesystem_source_dmg_or_pkg = (
|
||||
self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path) or
|
||||
self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path) or # Common for newer macOS
|
||||
self._find_gibmacos_asset("SharedSupport.dmg", product_folder_path) # Older fallback
|
||||
)
|
||||
self._report_progress(f"GUIDANCE:\n{guidance_message}")
|
||||
QMessageBox.information(None, "Manual Steps Required for Windows USB", guidance_message) # Ensure QMessageBox is available or mocked
|
||||
if not basesystem_source_dmg_or_pkg:
|
||||
# Last resort: search for any large PKG file as it might be the installer
|
||||
if product_folder_path:
|
||||
pkgs = glob.glob(os.path.join(product_folder_path, "*.pkg")) + glob.glob(os.path.join(product_folder_path, "SharedSupport", "*.pkg"))
|
||||
if pkgs: basesystem_source_dmg_or_pkg = max(pkgs, key=os.path.getsize, default=None)
|
||||
if not basesystem_source_dmg_or_pkg:
|
||||
raise RuntimeError("Could not find BaseSystem.dmg, InstallAssistant.pkg, or SharedSupport.dmg in expected locations.")
|
||||
|
||||
self._report_progress("Windows USB installer preparation (EFI automated, macOS content manual guidance provided) initiated.")
|
||||
self._report_progress(f"Selected source for HFS extraction: {basesystem_source_dmg_or_pkg}")
|
||||
if not self._extract_hfs_from_dmg_or_pkg(basesystem_source_dmg_or_pkg, self.temp_basesystem_hfs_path):
|
||||
raise RuntimeError(f"Failed to extract HFS+ image from '{basesystem_source_dmg_or_pkg}'. Check 7z output above.")
|
||||
|
||||
# --- Guidance for Manual Steps ---
|
||||
abs_hfs_path_win = os.path.abspath(self.temp_basesystem_hfs_path).replace("/", "\\")
|
||||
abs_download_path_win = os.path.abspath(self.macos_download_path).replace("/", "\\")
|
||||
physical_drive_path_win = self.physical_drive_path # Already has escaped backslashes for \\.\
|
||||
|
||||
# Try to find specific assets for better guidance
|
||||
install_info_plist_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=False) or "InstallInfo.plist (find in product folder)"
|
||||
basesystem_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=False) or "BaseSystem.dmg"
|
||||
basesystem_chunklist_src = self._find_gibmacos_asset("BaseSystem.chunklist", product_folder_path, search_deep=False) or "BaseSystem.chunklist"
|
||||
main_installer_pkg_src = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, search_deep=False) or \
|
||||
self._find_gibmacos_asset("InstallESD.dmg", product_folder_path, search_deep=False) or \
|
||||
"InstallAssistant.pkg OR InstallESD.dmg (main installer package)"
|
||||
apple_diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=False) or "AppleDiagnostics.dmg (if present)"
|
||||
|
||||
|
||||
guidance_message = (
|
||||
f"AUTOMATED EFI SETUP COMPLETE on drive {self.assigned_efi_letter}: (USB partition 1).\n"
|
||||
f"TEMPORARY BaseSystem HFS image prepared at: '{abs_hfs_path_win}'.\n\n"
|
||||
f"MANUAL STEPS REQUIRED FOR MAIN macOS PARTITION (USB partition {macos_partition_number_str} - '{installer_vol_label}'):\n"
|
||||
f"TARGET DISK: Disk {self.disk_number} ({physical_drive_path_win})\n"
|
||||
f"TARGET PARTITION FOR HFS+ CONTENT: Partition {macos_partition_number_str} (Offset from disk start: {macos_partition_offset_str}).\n\n"
|
||||
|
||||
f"1. WRITE BaseSystem IMAGE:\n"
|
||||
f" You MUST use a 'dd for Windows' utility. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
|
||||
f" Example command (VERIFY SYNTAX & TARGETS for YOUR dd tool! Incorrect use can WIPE OTHER DRIVES!):\n"
|
||||
f" `dd if=\"{abs_hfs_path_win}\" of={physical_drive_path_win} bs=8M --progress` (if targeting whole disk with offset for partition 2)\n"
|
||||
f" OR (if your dd supports writing directly to a partition by its number/offset, less common for \\\\.\\PhysicalDrive targets):\n"
|
||||
f" `dd if=\"{abs_hfs_path_win}\" of=\\\\?\\Volume{{GUID_OF_PARTITION_2}}\ bs=8M --progress` (more complex to get GUID)\n"
|
||||
f" It's often SAFER to write to the whole physical drive path ({physical_drive_path_win}) if your `dd` version calculates offsets correctly or if you specify the exact starting sector/byte offset for partition 2.\n"
|
||||
f" The BaseSystem HFS image is approx. {os.path.getsize(self.temp_basesystem_hfs_path)/(1024*1024):.2f} MB.\n\n"
|
||||
|
||||
f"2. COPY OTHER INSTALLER FILES (CRITICAL FOR OFFLINE INSTALLER):\n"
|
||||
f" After `dd`-ing BaseSystem.hfs, the '{installer_vol_label}' partition on the USB needs more files from your download path: '{abs_download_path_win}'.\n"
|
||||
f" This requires a tool that can WRITE to HFS+ partitions from Windows (e.g., TransMac, Paragon HFS+ for Windows, HFSExplorer with write capabilities if any), OR perform this step on macOS/Linux.\n\n"
|
||||
f" KEY FILES/FOLDERS TO COPY from '{abs_download_path_win}' (likely within a subfolder named like '{os.path.basename(product_folder_path if product_folder_path else '')}') to the ROOT of the '{installer_vol_label}' USB partition:\n"
|
||||
f" a. Create folder: `Install macOS {self.target_macos_version}.app` (this is a directory)\n"
|
||||
f" b. Copy '{os.path.basename(install_info_plist_src)}' to the root of '{installer_vol_label}' partition.\n"
|
||||
f" c. Copy '{os.path.basename(basesystem_dmg_src)}' AND '{os.path.basename(basesystem_chunklist_src)}' into: `System/Library/CoreServices/` (on '{installer_vol_label}')\n"
|
||||
f" d. Copy '{os.path.basename(main_installer_pkg_src)}' into: `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/`\n"
|
||||
f" (Alternatively, for older macOS, sometimes into: `System/Installation/Packages/`)\n"
|
||||
f" e. Copy '{os.path.basename(apple_diag_src)}' (if found) into: `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/` (or a similar recovery/diagnostics path if known for your version).\n"
|
||||
f" f. Ensure `boot.efi` (from the OpenCore EFI, often copied from `usr/standalone/i386/boot.efi` inside BaseSystem.dmg or similar) is placed at `System/Library/CoreServices/boot.efi` on the '{installer_vol_label}' partition. (Your EFI setup on partition 1 handles OpenCore booting, this is for the macOS installer itself).\n\n"
|
||||
|
||||
f"3. (Optional but Recommended) Create `.IAProductInfo` file at the root of the '{installer_vol_label}' partition. This file is a symlink to `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/InstallInfo.plist` in real installers. On Windows, you may need to copy the `InstallInfo.plist` to this location as well if symlinks are hard.\n\n"
|
||||
|
||||
"IMPORTANT:\n"
|
||||
"- Without step 2 (copying additional assets), the USB will likely NOT work as a full offline installer and may only offer Internet Recovery (if OpenCore is correctly configured for network access).\n"
|
||||
"- The temporary BaseSystem HFS image at '{abs_hfs_path_win}' will be DELETED when you close this program or this message.\n"
|
||||
)
|
||||
self._report_progress(f"GUIDANCE FOR MANUAL STEPS:\n{guidance_message}")
|
||||
# Use the QMessageBox mock or actual if available
|
||||
QMessageBox.information(None, f"Manual Steps Required for Windows USB - {self.target_macos_version}", guidance_message)
|
||||
|
||||
self._report_progress("Windows USB installer preparation (EFI automated, macOS content requires manual steps as detailed).")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._report_progress(f"Error during Windows USB writing: {e}"); self._report_progress(traceback.format_exc())
|
||||
self._report_progress(f"FATAL ERROR during Windows USB writing: {e}"); self._report_progress(traceback.format_exc())
|
||||
# Show error in QMessageBox as well if possible
|
||||
QMessageBox.critical(None, "USB Writing Failed", f"An error occurred: {e}\n\n{traceback.format_exc()}")
|
||||
return False
|
||||
finally:
|
||||
if self.assigned_efi_letter:
|
||||
self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit")
|
||||
self._report_progress(f"Attempting to remove drive letter assignment for {self.assigned_efi_letter}:")
|
||||
# Run silently, don't check for errors as it's cleanup
|
||||
self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit", capture_output_for_parse=False)
|
||||
|
||||
# Cleanup of self.temp_dir_base will handle all sub-temp-dirs and files within it.
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
self._report_progress("Temporary files cleanup attempted.")
|
||||
|
||||
# Standalone test block
|
||||
if __name__ == '__main__':
|
||||
import traceback
|
||||
from constants import MACOS_VERSIONS # Needed for _get_gibmacos_product_folder
|
||||
if platform.system() != "Windows": print("This script is for Windows standalone testing."); exit(1)
|
||||
import platform
|
||||
if platform.system() != "Windows":
|
||||
print("This script's standalone test mode is intended for Windows.")
|
||||
# sys.exit(1) # Use sys.exit for proper exit codes
|
||||
|
||||
print("USB Writer Windows Standalone Test - Installer Method Guidance")
|
||||
mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
|
||||
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
|
||||
mock_product_name = f"000-00000 - macOS {target_version_cli} 14.x.x"
|
||||
|
||||
# Mock constants if not available (e.g. running totally standalone)
|
||||
try: from constants import MACOS_VERSIONS
|
||||
except ImportError: MACOS_VERSIONS = {"Sonoma": "sonoma", "Ventura": "ventura"} ; print("Mocked MACOS_VERSIONS")
|
||||
|
||||
pid_test = os.getpid()
|
||||
# Create a unique temp directory for this test run to avoid conflicts
|
||||
# Place it in user's Temp for better behavior on Windows
|
||||
test_run_temp_dir = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_test_run_{pid_test}")
|
||||
os.makedirs(test_run_temp_dir, exist_ok=True)
|
||||
|
||||
# Mock download directory structure within the test_run_temp_dir
|
||||
mock_download_dir = os.path.join(test_run_temp_dir, "mock_macos_downloads")
|
||||
os.makedirs(mock_download_dir, exist_ok=True)
|
||||
|
||||
# Example: Sonoma. More versions could be added for thorough testing.
|
||||
target_version_test = "Sonoma"
|
||||
version_tag_test = MACOS_VERSIONS.get(target_version_test, target_version_test.lower())
|
||||
|
||||
mock_product_name = f"012-34567 - macOS {target_version_test} 14.1" # Example name
|
||||
mock_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
|
||||
os.makedirs(os.path.join(mock_product_folder, "SharedSupport"), exist_ok=True)
|
||||
with open(os.path.join(mock_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg")
|
||||
mock_shared_support = os.path.join(mock_product_folder, "SharedSupport")
|
||||
os.makedirs(mock_shared_support, exist_ok=True)
|
||||
|
||||
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
|
||||
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
|
||||
with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML)
|
||||
# Create dummy files that would be found by _find_gibmacos_asset and _extract_hfs_from_dmg_or_pkg
|
||||
# 1. Dummy InstallAssistant.pkg (which contains BaseSystem.dmg)
|
||||
dummy_pkg_path = os.path.join(mock_product_folder, "InstallAssistant.pkg")
|
||||
with open(dummy_pkg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy PKG
|
||||
# For the _extract_hfs_from_dmg_or_pkg to work with 7z, it needs a real archive.
|
||||
# This test won't actually run 7z unless 7z is installed and the dummy files are valid archives.
|
||||
# The focus here is testing the script logic, not 7z itself.
|
||||
# So, we'll also create a dummy extracted BaseSystem.hfs for the guidance part.
|
||||
|
||||
disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). WIPES DISK: ")
|
||||
if not disk_id_input.isdigit(): print("Invalid disk number."); exit(1)
|
||||
# 2. Dummy files for the guidance message (these would normally be in mock_product_folder or mock_shared_support)
|
||||
with open(os.path.join(mock_product_folder, "InstallInfo.plist"), "w") as f: f.write("<plist><dict></dict></plist>")
|
||||
with open(os.path.join(mock_shared_support, "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(5*1024*1024)) # Dummy DMG
|
||||
with open(os.path.join(mock_shared_support, "BaseSystem.chunklist"), "w") as f: f.write("chunklist content")
|
||||
# AppleDiagnostics.dmg is optional
|
||||
with open(os.path.join(mock_shared_support, "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1*1024*1024))
|
||||
|
||||
if input(f"Sure to wipe disk {disk_id_input}? (yes/NO): ").lower() == 'yes':
|
||||
writer = USBWriterWindows(disk_id_input, mock_download_dir, print, True, target_version_cli)
|
||||
writer.format_and_write()
|
||||
else: print("Cancelled.")
|
||||
shutil.rmtree(mock_download_dir, ignore_errors=True)
|
||||
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template
|
||||
print("Mock download dir cleaned up.")
|
||||
|
||||
# Ensure OC_TEMPLATE_DIR (EFI_template_installer) exists for the test or use the minimal creation.
|
||||
# Relative path from usb_writer_windows.py to EFI_template_installer
|
||||
abs_oc_template_dir = OC_TEMPLATE_DIR
|
||||
if not os.path.exists(abs_oc_template_dir):
|
||||
print(f"Warning: Test OC_TEMPLATE_DIR '{abs_oc_template_dir}' not found. Minimal EFI will be created by script if needed.")
|
||||
# Optionally, create a dummy one for test if you want to test the copy logic:
|
||||
# os.makedirs(os.path.join(abs_oc_template_dir, "EFI", "OC"), exist_ok=True)
|
||||
# with open(os.path.join(abs_oc_template_dir, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"TestTemplate":True}, f)
|
||||
else:
|
||||
print(f"Using existing OC_TEMPLATE_DIR for test: {abs_oc_template_dir}")
|
||||
|
||||
|
||||
disk_id_input = input("Enter target PHYSICAL DISK NUMBER for test (e.g., '1' for PhysicalDrive1). WARNING: THIS DISK WILL BE MODIFIED/WIPED by diskpart. BE ABSOLUTELY SURE. Enter 'skip' to not run diskpart stage: ")
|
||||
|
||||
if disk_id_input.lower() == 'skip':
|
||||
print("Skipping disk operations. Guidance message will be shown with placeholder disk info.")
|
||||
# Create a writer instance with a dummy disk ID for logic testing without diskpart
|
||||
writer = USBWriterWindows("disk 0", mock_download_dir, print, True, target_version_test)
|
||||
# We need to manually create a dummy temp_basesystem.hfs for the guidance message part
|
||||
os.makedirs(writer.temp_dir_base, exist_ok=True)
|
||||
with open(writer.temp_basesystem_hfs_path, "wb") as f: f.write(os.urandom(1024*1024)) # 1MB dummy HFS
|
||||
# Manually call parts of format_and_write that don't involve diskpart
|
||||
writer.check_dependencies() # Still check other deps
|
||||
# Simulate EFI setup success for guidance
|
||||
writer.assigned_efi_letter = "X"
|
||||
# ... then generate and show guidance (this part is inside format_and_write)
|
||||
# This is a bit clunky for 'skip' mode. Full format_and_write is better if safe.
|
||||
print("Test in 'skip' mode is limited. Full test requires a dedicated test disk.")
|
||||
|
||||
elif not disk_id_input.isdigit():
|
||||
print("Invalid disk number.")
|
||||
else:
|
||||
actual_disk_id_str = f"\\\\.\\PhysicalDrive{disk_id_input}" # Match format used by class
|
||||
confirm = input(f"ARE YOU ABSOLUTELY SURE you want to test on {actual_disk_id_str}? This involves running 'diskpart clean'. Type 'YESIDO' to confirm: ")
|
||||
if confirm == 'YESIDO':
|
||||
writer = USBWriterWindows(actual_disk_id_str, mock_download_dir, print, True, target_version_test)
|
||||
try:
|
||||
writer.format_and_write()
|
||||
print(f"Test run completed. Check disk {disk_id_input} and console output.")
|
||||
except Exception as e:
|
||||
print(f"Test run failed: {e}")
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print("Test cancelled by user.")
|
||||
|
||||
# Cleanup the test run's unique temp directory
|
||||
print(f"Cleaning up test run temp directory: {test_run_temp_dir}")
|
||||
shutil.rmtree(test_run_temp_dir, ignore_errors=True)
|
||||
|
||||
print("Standalone test finished.")
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue